Skip to content

Latest commit

 

History

History
84 lines (64 loc) · 4.11 KB

File metadata and controls

84 lines (64 loc) · 4.11 KB

Khao Pad (ข้าวผัด) — Project Guide

What is this?

A modular CMS built with SvelteKit for Cloudflare. One repo, two subdomains:

  • www.example.com — public website (SSR)
  • cms.example.com — admin panel (authenticated)

Tech stack

  • Package manager: pnpm (CI uses pnpm 9; lockfile is pnpm-lock.yaml)
  • Framework: SvelteKit 2 + Svelte 5
  • Bundler: Vite 5.4.x (~5.4.21) — pinned because Vite 6 currently trips SvelteKit’s vite-plugin-sveltekit-guard during production builds (client bundle / hooks.server.ts boundary). Prefer unpinning after upgrading SvelteKit when the ecosystem clearly supports Vite 6 for Cloudflare builds.
  • Styling: Tailwind CSS 4 + shadcn/ui (bits-ui)
  • Database: Cloudflare D1 via Drizzle ORM
  • Media: Cloudflare R2
  • Cache: Cloudflare KV
  • Auth: Better Auth (email/password, D1-backed sessions)
  • i18n: Paraglide JS 2.0 (inlang) — compile-time, type-safe translations
  • Deployment: Cloudflare Workers via wrangler

Architecture

  • Single SvelteKit app with route groups: (www) for public, (cms) for admin
  • Path-based surface routing via hooks.server.ts/cms/* is admin, everything else is public
  • Content storage is D1-backed (D1ContentProvider); the ContentProvider interface is kept as a seam for tests
  • Media always stored in R2, metadata in D1

Key directories

  • src/lib/server/content/ — Content provider interface, D1 provider, Drizzle schema
  • src/lib/server/auth/ — Better Auth setup, permissions
  • src/lib/server/media/ — R2 media service
  • src/lib/i18n/ — Locale helpers
  • src/lib/paraglide/ — Auto-generated by Paraglide (gitignored)
  • messages/ — Translation JSON files (th.json, en.json)
  • project.inlang/ — Inlang project settings
  • src/routes/(www)/ — Public site routes
  • src/routes/(cms)/ — CMS admin routes
  • src/routes/api/auth/ — Auth API endpoints
  • drizzle/ — D1 migration files

Commands

pnpm dev               # Local dev server
pnpm build             # Build for production
pnpm run db:generate   # Generate D1 migration from schema changes
pnpm run db:migrate    # Apply migrations locally
pnpm run db:migrate:remote # Apply migrations to production D1
pnpm run deploy        # Build + deploy to Cloudflare Workers

Content model

  • Default locale is English (en). Thai (th) is the secondary locale.
  • Articles have shared slug/media across languages, separate markdown per locale (EN/TH)
  • Localizations stored in separate tables (article_localizations, category_localizations, etc.)
  • Roles: super_admin > admin > editor > author

Slug rules (important)

  • Slugs are always English-only ASCII (^[a-z0-9]+(?:-[a-z0-9]+)*$)
  • Slugs are shared across all locales — there is no per-language slug
  • Slugs are auto-generated from the English title via slugify() in $lib/utils
  • The English (en) localization is required when creating an article — the slug is derived from localizations.en.title
  • Non-ASCII characters are stripped, not transliterated. A Thai-only title cannot produce a slug; the user must supply an English title (or an explicit slug)
  • Admins may rename the slug via updateArticle({ slug }), but it is then re-normalized through slugify() before being stored

Important patterns

  • All IDs use nanoid()
  • Dates stored as ISO strings in D1 (SQLite text)
  • Content provider is injected into locals via hooks
  • Auth guard is in (cms)/+layout.server.ts
  • Route access control is enforced in hooks.server.ts (subdomain check)

i18n — Two layers

  1. Paraglide JS ($lib/paraglide/messages) — UI strings (buttons, labels, navigation). Compile-time, type-safe, tree-shakable. Add messages to messages/th.json and messages/en.json, import as import * as m from '$lib/paraglide/messages', use as m.key_name().
  2. Content localizations — Article/category/tag body text stored per locale in D1 (article_localizations table). These are user-generated content, not UI translations.

Do not mix these two layers. Paraglide is for the app shell; content localizations are for CMS data.