SvelteKit + Cloudflare Workers starter with Tailwind v4, shadcn/bits-ui, Superforms, Paraglide i18n, and Bun.
- Bun 1.x (
curl -fsSL https://bun.sh/install | bash
) - Cloudflare Wrangler 4.x (
bun add -g wrangler
) - Cloudflare account for deploy/preview
# 1) Install deps
bun i
# 2) Start dev server
bun run dev
# (alias) bun dev
# 3) Open the app
# Vite will print the local URL in the terminal
- If an
.env.example
exists, copy it to.env
and fill values:
cp .env.example .env
- Scripts (like
script/translate.ts
) expectDEEPL_API_KEY
in your shell or.env
when run locally. - Runtime app secrets for production (Cloudflare Workers) should be stored as Wrangler secrets:
wrangler secret put SLACK_WEBHOOK_URL
wrangler secret put EMAIL_FROM
wrangler secret put EMAIL_TO
# Dev
bun run dev
# Type-check / Lint / Format
bun run check
bun run lint
bun run format
# Build
bun run build
# Local worker preview (builds, then wrangler dev)
bun run preview
# Deploy to Cloudflare Workers
bun run deploy
# Generate Cloudflare types into src/ (optional)
bun run cf-typegen
# Translate missing messages using DeepL (requires DEEPL_API_KEY)
bun run translate
- Run the interactive setup to replace placeholders and configure Cloudflare:
bun run setup
This updates:
package.json
namewrangler.jsonc
→name
,account_id
,routes[0].pattern
,routes[0].zone_name
, and optional staging routesrc/routes/Header.svelte
→ logo alt text
For new projects, in wrangler.jsonc
you should change at minimum:
- name: Worker name
- account_id: Your Cloudflare account id
- routes.pattern: Your production domain (e.g.
example.com
) - routes.zone_name: Your Cloudflare zone (e.g.
example.com
) - Optionally set
env.staging.routes[0].pattern
for staging- The setup script also ensures
env.staging.name
and updates stagingzone_name
- The setup script also ensures
- Wrangler uses
wrangler.jsonc
and the Cloudflare adapter’s worker bundle at.svelte-kit/cloudflare/_worker.js
. - Update
routes
inwrangler.jsonc
to match your domain/zone. - For preview:
bun run preview
(builds and runswrangler dev
). - For deploy:
bun run deploy
.
This repo ships with a CI workflow in /.github/workflows/ci.yml
that:
- Deploys on pushes to
main
(production) andstaging
(staging) - Looks up/creates an Umami site for your domain and injects the
data-website-id
intosrc/app.html
- Runs
bun run deploy
(andbun run deploy --env staging
on thestaging
branch)
Add these in your GitHub repository under Settings → Secrets and variables → Actions → New repository secret:
CLOUDFLARE_API_TOKEN
: Cloudflare API token with the "Edit Cloudflare Workers" template (or equivalent scopes to deploy Workers and edit routes) for the target accountUMAMI_BASE_URL
: Base URL of your Umami instance (e.g.https://umami.example.com
)UMAMI_USERNAME
: Umami user name (for the above instance)UMAMI_PASSWORD
: Umami password
- Ensure
wrangler.jsonc
is configured with your Cloudflareaccount_id
and a production route underroutes[0].pattern
/zone_name
. The workflow reads this domain to manage the Umami site. - If you use a staging environment, set
env.staging.routes[0].pattern
andzone_name
inwrangler.jsonc
. Deploys from thestaging
branch will target this environment via--env staging
.
src/app.html
includes the Umami script with data-website-id="placeholder"
. The workflow replaces this placeholder at build time with the site ID from Umami and rewrites the script src
based on UMAMI_BASE_URL
. If you do not use Umami, remove the script tag from src/app.html
and optionally delete the related steps in /.github/workflows/ci.yml
.
- The workflow sets up Bun with
oven-sh/setup-bun@v1
and runsbun install
. - The workflow triggers on
push
tomain
andstaging
, and onpull_request
(PRs run but do not deploy).
- SvelteKit 2 + Svelte 5, Vite 7
- Cloudflare Workers via
@sveltejs/adapter-cloudflare
+ Wrangler - Tailwind CSS v4 (+
@tailwindcss/forms
,@tailwindcss/typography
,tailwind-variants
,tailwind-merge
,tw-animate-css
) - UI:
@shadcn/svelte
patterns viabits-ui
,@lucide/svelte
- Forms/validation:
sveltekit-superforms
,formsnap
,valibot
(seesrc/routes/contactForm/contactSchema.ts
) - i18n: inlang Paraglide (
@inlang/cli
,@inlang/paraglide-js
) - Media/UI extras:
embla-carousel-svelte
(+embla-carousel-autoplay
),@unpic/svelte
,mode-watcher
- Components are installed under
src/lib/shadcn/components/ui/*
percomponents.json
. - Example import:
<script lang="ts">
import { Button } from '$lib/shadcn/components/ui/button';
</script>
<button variant="secondary">Click me</button>
- To add more components via the CLI:
# Open the add UI and pick components
bunx shadcn-svelte@latest add
-
Files:
- Schema:
src/routes/contactForm/contactSchema.ts
- Client component:
src/routes/contactForm/contactForm.svelte
- Server load/actions:
src/routes/+page.server.ts
- Schema:
-
Server example:
// src/routes/+page.server.ts
import { superValidate } from 'sveltekit-superforms';
import { valibot } from 'sveltekit-superforms/adapters';
import { contactSchema } from './contactForm/contactSchema';
export const load = async () => {
const contactForm = await superValidate(valibot(contactSchema));
return { contactForm };
};
export const actions = {
default: async (event) => {
const form = await superValidate(event, valibot(contactSchema));
if (!form.valid) return { status: 400, form };
return { form };
}
};
- Client example:
<script lang="ts">
import * as Form from '$lib/shadcn/components/ui/form/index.js';
import { Input } from '$lib/shadcn/components/ui/input/index.js';
import { Textarea } from '$lib/shadcn/components/ui/textarea/index.js';
import { Button } from '$lib/shadcn/components/ui/button';
import { contactSchema, type ContactSchema } from './contactSchema';
import { type SuperValidated, type Infer, superForm } from 'sveltekit-superforms';
import { valibotClient } from 'sveltekit-superforms/adapters';
let { data }: { data: { form: SuperValidated<Infer<ContactSchema>> } } = $props();
const { form, enhance } = superForm(data.form, { validators: valibotClient(contactSchema) });
const { form: formData } = form;
</script>
<form method="POST" use:enhance>
<Form.Field {form} name="firstName">
<Form.Control>
{#snippet children({ props })}
<Form.Label>First name</Form.Label>
<input {...props} bind:value="{$formData.firstName}" />
{/snippet}
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Form.Button>Send</Form.Button>
<!-- See full example in `src/routes/contactForm/contactForm.svelte` -->
</form>
This wiring gives client-side validation via Valibot and server-side safety via Superforms, while using shadcn-svelte UI primitives.
- Svelte:
svelte.svelte-vscode
- Tailwind CSS IntelliSense:
bradlc.vscode-tailwindcss
- Paraglide (inlang / Sherlock):
inlang.vs-code-extension
- ESLint:
dbaeumer.vscode-eslint
- Prettier:
esbenp.prettier-vscode
Install all at once:
code --install-extension svelte.svelte-vscode bradlc.vscode-tailwindcss inlang.vs-code-extension dbaeumer.vscode-eslint esbenp.prettier-vscode
Opening the workspace will also prompt to install recommendations from .vscode/extensions.json
.
- SvelteKit: kit.svelte.dev/docs
- Svelte 5: svelte.dev/docs
- Vite: vitejs.dev/guide
- Tailwind CSS v4: tailwindcss.com/docs
- Cloudflare Workers: developers.cloudflare.com/workers
- Wrangler: developers.cloudflare.com/workers/wrangler
- Adapter Cloudflare: kit.svelte.dev/docs/adapter-cloudflare
- Paraglide i18n: inlang.com/m/gerre34r/library-inlang-paraglideJs
- Superforms: superforms.rocks
- bits-ui (shadcn-svelte): bits-ui.com
- @shadcn/svelte docs: shadcn-svelte.com
- Embla Carousel: embla-carousel.com
- DeepL Node: github.com/DeepLcom/deepl-node
- Valibot: valibot.dev