Living on the Edge
Web apps with SvelteKit and Cloudflare Pages
Nils Röhrig | REWE digital
Agenda
What is Edge Computing?
What use does Edge Computing have in web development?
What are SvelteKit & Cloudflare Pages?
How could an app be built with these tools?
What is Edge Computing?
Edge computing is computing that takes place at or near the physical location of either the user or the source of the data.
Parts of an edge network
Core
End user
Core
Service provider edge
End user
Core
End-user premises edge
Service provider edge
End user
Core
End-user premises edge
Device edge
Service provider edge
End user
Core
End-user premises edge
Device edge
Service provider edge
End user
Core
End-user premises edge
Device edge
Service provider edge
End user
Core
End-user premises edge
Device edge
Service provider edge
End user
Core
End-user premises edge
Device edge
Service provider edge
End user
What use does Edge Computing have in web development?
Core
End-user premises edge
Device edge
Service provider edge
End user
Core
End-user premises edge
Device edge
Service provider edge
End user
Core
CDN
Core
Web Frontend
Web Frontend
Web Frontend
Service Provider Edge
Edge Node
Parts of
Application
Edge Node
Parts of
Application
Edge Node
Parts of
Application
Core
What are SvelteKit & Cloudflare Pages?
SvelteKit is a framework for building extremely high-performance web apps.
Meta framework for
- Server-side rendering
- Static site generation
- File-based routing
- Client-side navigation
- Platform independence
- etc.
Meta framework for
- Server-side rendering
- Static site generation
- File-based routing
- Client-side navigation
- Platform independence
- etc.
Cloudflare Pages is a JAMstack platform for frontend developers to collaborate and deploy websites.
CDN
deploys to
-Providers
CDN
integrates with
deploys to
-Providers
CDN
integrates with
deploys to
uses
-Providers
CDN
integrates with
deploys to
uses
runs code at
Caution...
Caution...
How could an app be built with these tools?
Set it up in 4 simple steps...
Set it up in 4 simple steps...
1. Create a GitHub repository
1. Create a GitHub repository
2. Create a Cloudflare Pages project from repository
Set it up in 4 simple steps...
1. Create a GitHub repository
3. Create a SvelteKit app
2. Create a Cloudflare Pages project from repository
Set it up in 4 simple steps...
3. Create a SvelteKit app
4. Push app to repository
1. Create a GitHub repository
2. Create a Cloudflare Pages project from repository
Set it up in 4 simple steps...
Introducing svekom
Insights from developing svekom
Routing in SvelteKit
Routing in SvelteKit
- File-based routing
- Default path:
src/routes
- Route names and parameters:
src/routes/name/[param]
- Partition into pages, endpoints, layouts & error pages
Example: products
src/routes/products/[id]
Example: products
<script lang="ts">
export let data: PageData;
let product: Product, category: Category | string | undefined;
$: ({ product } = data);
$: ({ category } = product);
</script>
<article>
<Card>
<Section first>
<img class="image" alt="" src="/products/{product.filename}"/>
</Section>
</Card>
<div class="info">
{#if category && typeof category !== 'string'}
<Badge>{category.name}</Badge>
{/if}
<h2 class="title">{product.brand} {product.name}</h2>
<p class="short-description">{product.shortDescription}</p>
<div class="price">{formatPrice(product.price)}</div>
<AddToCart {product}/>
<div class="description">
<p class="origin">Country of origin: {product.origin}</p>
{@html product.description}
</div>
</div>
</article>
const converter = new showdown.Converter();
export const load: PageServerLoad = async ({ params, platform }) => {
const productService = createProductService(platform);
const categoryService = createCategoryService(platform);
const product = await productService
.getSingleProduct(params.id)
.then((option) => option.getOrThrow(error(404)));
const { description, category: categoryId, ...rest } = product;
const category = await categoryService
.getSingleCategory(categoryId)
.then((category) => category.getOrUndefined());
return {
product: {
...rest,
category,
description: converter.makeHtml(product.description),
},
};
};
+page.svelte
+page.server.ts
src/routes/products/[id]
<script lang="ts">
export let data: PageData;
let product: Product, category: Category | string | undefined;
$: ({ product } = data);
$: ({ category } = product);
</script>
<article>
<Card>
<Section first>
<img class="image" alt="" src="/products/{product.filename}"/>
</Section>
</Card>
<div class="info">
{#if category && typeof category !== 'string'}
<Badge>{category.name}</Badge>
{/if}
<h2 class="title">{product.brand} {product.name}</h2>
<p class="short-description">{product.shortDescription}</p>
<div class="price">{formatPrice(product.price)}</div>
<AddToCart {product}/>
<div class="description">
<p class="origin">Country of origin: {product.origin}</p>
{@html product.description}
</div>
</div>
</article>
const converter = new showdown.Converter();
export const load: PageServerLoad = async ({ params, platform }) => {
const productService = createProductService(platform);
const categoryService = createCategoryService(platform);
const product = await productService
.getSingleProduct(params.id)
.then((option) => option.getOrThrow(error(404)));
const { description, category: categoryId, ...rest } = product;
const category = await categoryService
.getSingleCategory(categoryId)
.then((category) => category.getOrUndefined());
return {
product: {
...rest,
category,
description: converter.makeHtml(product.description),
},
};
};
+page.svelte
+page.server.ts
Example: products
src/routes/products/[id]
<script lang="ts">
export let data: PageData;
let product: Product, category: Category | string | undefined;
$: ({ product } = data);
$: ({ category } = product);
</script>
<article>
<Card>
<Section first>
<img class="image" alt="" src="/products/{product.filename}"/>
</Section>
</Card>
<div class="info">
{#if category && typeof category !== 'string'}
<Badge>{category.name}</Badge>
{/if}
<h2 class="title">{product.brand} {product.name}</h2>
<p class="short-description">{product.shortDescription}</p>
<div class="price">{formatPrice(product.price)}</div>
<AddToCart {product}/>
<div class="description">
<p class="origin">Country of origin: {product.origin}</p>
{@html product.description}
</div>
</div>
</article>
const converter = new showdown.Converter();
export const load: PageServerLoad = async ({ params, platform }) => {
const productService = createProductService(platform);
const categoryService = createCategoryService(platform);
const product = await productService
.getSingleProduct(params.id)
.then((option) => option.getOrThrow(error(404)));
const { description, category: categoryId, ...rest } = product;
const category = await categoryService
.getSingleCategory(categoryId)
.then((category) => category.getOrUndefined());
return {
product: {
...rest,
category,
description: converter.makeHtml(product.description),
},
};
};
+page.svelte
+page.server.ts
Example: products
src/routes/products/[id]
const converter = new showdown.Converter();
export const load: PageServerLoad = async ({ params, platform }) => {
const productService = createProductService(platform);
const categoryService = createCategoryService(platform);
const product = await productService
.getSingleProduct(params.id)
.then((option) => option.getOrThrow(error(404)));
const { description, category: categoryId, ...rest } = product;
const category = await categoryService
.getSingleCategory(categoryId)
.then((category) => category.getOrUndefined());
return {
product: {
...rest,
category,
description: converter.makeHtml(product.description),
},
};
};
+page.svelte
+page.server.ts
<script lang="ts">
export let data: PageData;
let product: Product, category: Category | string | undefined;
$: ({ product } = data);
$: ({ category } = product);
</script>
<article>
<Card>
<Section first>
<img class="image" alt="" src="/products/{product.filename}"/>
</Section>
</Card>
<div class="info">
{#if category && typeof category !== 'string'}
<Badge>{category.name}</Badge>
{/if}
<h2 class="title">{product.brand} {product.name}</h2>
<p class="short-description">{product.shortDescription}</p>
<div class="price">{formatPrice(product.price)}</div>
<AddToCart {product}/>
<div class="description">
<p class="origin">Country of origin: {product.origin}</p>
{@html product.description}
</div>
</div>
</article>
Example: products
src/routes/products/[id]
const converter = new showdown.Converter();
export const load: PageServerLoad = async ({ params, platform }) => {
const productService = createProductService(platform);
const categoryService = createCategoryService(platform);
const product = await productService
.getSingleProduct(params.id)
.then((option) => option.getOrThrow(error(404)));
const { description, category: categoryId, ...rest } = product;
const category = await categoryService
.getSingleCategory(categoryId)
.then((category) => category.getOrUndefined());
return {
product: {
...rest,
category,
description: converter.makeHtml(product.description),
},
};
};
+page.svelte
+page.server.ts
<script lang="ts">
export let data: PageData;
let product: Product, category: Category | string | undefined;
$: ({ product } = data);
$: ({ category } = product);
</script>
<article>
<Card>
<Section first>
<img class="image" alt="" src="/products/{product.filename}"/>
</Section>
</Card>
<div class="info">
{#if category && typeof category !== 'string'}
<Badge>{category.name}</Badge>
{/if}
<h2 class="title">{product.brand} {product.name}</h2>
<p class="short-description">{product.shortDescription}</p>
<div class="price">{formatPrice(product.price)}</div>
<AddToCart {product}/>
<div class="description">
<p class="origin">Country of origin: {product.origin}</p>
{@html product.description}
</div>
</div>
</article>
Example: products
src/routes/products/[id]
Storage in Workers KV
Storage in Workers KV
Worker Code </>
Worker Code </>
uses
Runtime API
list()
get()
put()
delete()
Storage in Workers KV
Worker Code </>
Runtime API
list()
get()
put()
delete()
uses
updates
Edge Nodes
KV Cache
KV Cache
KV Cache
Storage in Workers KV
Worker Code </>
Runtime API
list()
get()
put()
delete()
uses
updates
updates
Edge Nodes
KV Cache
KV Cache
KV Cache
Storage in Workers KV
Worker Code </>
Edge Nodes
KV Cache
KV Cache
KV Cache
Runtime API
list()
get()
put()
delete()
uses
replicates
replicates
updates
updates
Storage in Workers KV
- Pairs are eventually-consistent
- Optimized for frequent reading
- Suboptimal frequent writing
Storage in Workers KV
Example: product data
Example: product data
interface Product {
id: string;
name: string;
shortDescription: string;
description: string;
price: number;
brand: string;
origin: string;
category: Category | string | undefined;
filename: string;
}
export function createProductService(platform: App.Platform) {
const store = platform.env.PRODUCTS;
return {
getProductsByCategory(category: Category): Promise<Product[]> {
return store
.list()
.then(prop('keys'))
.then(map(prop('name'))).
.then(keyNames => Promise.all(
map(keyName => store.get<Product>(keyName, { type: 'json'}))
))
.then(filter(propEq('category', category.id)))
}
};
}
Product.ts
ProductService.ts
Example: product data
interface Product {
id: string;
name: string;
shortDescription: string;
description: string;
price: number;
brand: string;
origin: string;
category: Category | string | undefined;
filename: string;
}
export function createProductService(platform: App.Platform) {
const store = platform.env.PRODUCTS;
return {
getProductsByCategory(category: Category): Promise<Product[]> {
return store
.list()
.then(prop('keys'))
.then(map(prop('name'))).
.then(keyNames => Promise.all(
map(keyName => store.get<Product>(keyName, { type: 'json'}))
))
.then(filter(propEq('category', category.id)))
}
};
}
Product.ts
ProductService.ts
Example: product data
interface Product {
id: string;
name: string;
shortDescription: string;
description: string;
price: number;
brand: string;
origin: string;
category: Category | string | undefined;
filename: string;
}
export function createProductService(platform: App.Platform) {
const store = platform.env.PRODUCTS;
return {
getProductsByCategory(category: Category): Promise<Product[]> {
return store
.list()
.then(prop('keys'))
.then(map(prop('name'))).
.then(keyNames => Promise.all(
map(keyName => store.get<Product>(keyName, { type: 'json'}))
))
.then(filter(propEq('category', category.id)))
}
};
}
Product.ts
ProductService.ts
SvelteKit adapters
application
Target platforms
application
Target platforms
Platform-specific adapter
application
Target platforms
Platform-specific adapter
uses
application
Target platforms
Platform-specific adapter
uses
adapts to
Example: adapter-cloudflare
Example: adapter-cloudflare
svelte.config.js
app.d.ts
import adapter from '@sveltejs/adapter-cloudflare';
import preprocess from 'svelte-preprocess';
const config = {
preprocess: preprocess(),
kit: {
adapter: adapter(),
},
};
export default config;
/// <reference types="@sveltejs/adapter-cloudflare" />
declare namespace App {
interface Platform {
env?: {
PRODUCTS: KVNamespace;
CATEGORIES: KVNamespace;
CONTENT: KVNamespace;
};
}
}
svelte.config.js
app.d.ts
import adapter from '@sveltejs/adapter-cloudflare';
import preprocess from 'svelte-preprocess';
const config = {
preprocess: preprocess(),
kit: {
adapter: adapter(),
},
};
export default config;
/// <reference types="@sveltejs/adapter-cloudflare" />
declare namespace App {
interface Platform {
env?: {
PRODUCTS: KVNamespace;
CATEGORIES: KVNamespace;
CONTENT: KVNamespace;
};
}
}
Example: adapter-cloudflare
svelte.config.js
app.d.ts
import adapter from '@sveltejs/adapter-cloudflare';
import preprocess from 'svelte-preprocess';
const config = {
preprocess: preprocess(),
kit: {
adapter: adapter(),
},
};
export default config;
/// <reference types="@sveltejs/adapter-cloudflare" />
declare namespace App {
interface Platform {
env?: {
PRODUCTS: KVNamespace;
CATEGORIES: KVNamespace;
CONTENT: KVNamespace;
};
}
}
Example: adapter-cloudflare
Findings
Findings
Edge web apps run close to the user
Edge web apps run close to the user
Tools like SvelteKit can ease the development
Findings
Tools like SvelteKit can ease the development
Platforms like Cloudflare can ease deployment & execution
Edge web apps run close to the user
Findings
Kontakt
Twitter:
LinkedIn:
Xing:
Thank you!
Code:
Live:
Guide:
Auf ein Wort zur lokalen Entwicklung...
Auf ein Wort zur lokalen Entwicklung...
import { dev } from '$app/environment';
import type { Handle } from '@sveltejs/kit';
import { isNil } from 'ramda';
export const handle: Handle = async ({ event, resolve }) => {
if (dev && isNil(event.platform)) {
const { categoryStore, contentStore, productStore } = await import(
'./kv-mock'
);
event.platform = {
env: {
PRODUCTS: productStore,
CATEGORIES: categoryStore,
CONTENT: contentStore,
},
};
}
return resolve(event);
};
src/hooks/index.ts
src/hooks/kvMock.ts
import { KVNamespace } from '@miniflare/kv';
import { MemoryStorage } from '@miniflare/storage-memory';
export const productStore = new KVNamespace(new MemoryStorage());
export const categoryStore = new KVNamespace(new MemoryStorage());
export const contentStore = new KVNamespace(new MemoryStorage());
Auf ein Wort zur lokalen Entwicklung...
import { dev } from '$app/environment';
import type { Handle } from '@sveltejs/kit';
import { isNil } from 'ramda';
export const handle: Handle = async ({ event, resolve }) => {
if (dev && isNil(event.platform)) {
const { categoryStore, contentStore, productStore } = await import(
'./kv-mock'
);
event.platform = {
env: {
PRODUCTS: productStore,
CATEGORIES: categoryStore,
CONTENT: contentStore,
},
};
}
return resolve(event);
};
src/hooks/index.ts
src/hooks/kvMock.ts
import { KVNamespace } from '@miniflare/kv';
import { MemoryStorage } from '@miniflare/storage-memory';
export const productStore = new KVNamespace(new MemoryStorage());
export const categoryStore = new KVNamespace(new MemoryStorage());
export const contentStore = new KVNamespace(new MemoryStorage());
Auf ein Wort zur lokalen Entwicklung...
src/hooks/index.ts
src/hooks/kvMock.ts
import { KVNamespace } from '@miniflare/kv';
import { MemoryStorage } from '@miniflare/storage-memory';
export const productStore = new KVNamespace(new MemoryStorage());
export const categoryStore = new KVNamespace(new MemoryStorage());
export const contentStore = new KVNamespace(new MemoryStorage());
import { dev } from '$app/environment';
import type { Handle } from '@sveltejs/kit';
import { isNil } from 'ramda';
export const handle: Handle = async ({ event, resolve }) => {
if (dev && isNil(event.platform)) {
const { categoryStore, contentStore, productStore } = await import(
'./kv-mock'
);
event.platform = {
env: {
PRODUCTS: productStore,
CATEGORIES: categoryStore,
CONTENT: contentStore,
},
};
}
return resolve(event);
};
Auf ein Wort zur lokalen Entwicklung...
src/hooks/index.ts
src/hooks/kvMock.ts
import { KVNamespace } from '@miniflare/kv';
import { MemoryStorage } from '@miniflare/storage-memory';
export const productStore = new KVNamespace(new MemoryStorage());
export const categoryStore = new KVNamespace(new MemoryStorage());
export const contentStore = new KVNamespace(new MemoryStorage());
import { dev } from '$app/environment';
import type { Handle } from '@sveltejs/kit';
import { isNil } from 'ramda';
export const handle: Handle = async ({ event, resolve }) => {
if (dev && isNil(event.platform)) {
const { categoryStore, contentStore, productStore } = await import(
'./kv-mock'
);
event.platform = {
env: {
PRODUCTS: productStore,
CATEGORIES: categoryStore,
CONTENT: contentStore,
},
};
}
return resolve(event);
};
Living on the Edge (Cologne.js 10/2022)
By Nils Röhrig
Living on the Edge (Cologne.js 10/2022)
- 1,962