From 02c138af086560ef75fd782d192e4da8aac9c92a Mon Sep 17 00:00:00 2001 From: Thunpisit Amnuaikiatloet Date: Sun, 3 May 2026 18:59:17 +0700 Subject: [PATCH] docs(readme): reframe as demo + starter, drop verbatim copy of upstream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The example README was a verbatim duplicate of the upstream README — same intro, same Setup section, same everything. That gives a visitor zero signal about what makes the example different from the template, and forces every doc fix to land in two places (and stay synchronized). This rewrite: - **Top**: explains what the live demo IS (history of khao pad as a 5-essay series in EN + TH) and what's wired up (every milestone through v2.0). - **What's different from upstream**: enumerates the 4 reskinned files, the seed content, the example-specific i18n keys, and the wrangler config. Future cherry-pick PRs reference the same list. - **Two paths to use it**: A) start clean from upstream template (recommended for new projects); B) fork this example if you want the paypers brand polish as a starting design. Step-by-step for each. - **Staying in sync with upstream**: the standard cherry-pick recipe + the two manual rules (field-merge i18n; preserve the 4 paypers files). Encoded the same lesson that's been in personal memory since the v1.6 cherry-pick wiped the reskin. - **Architecture/bindings/deployment**: linked to upstream README instead of duplicating. One source of truth. Net: 299 → 95 lines, zero duplicated content with upstream. Co-Authored-By: Claude Opus 4.7 --- README.md | 311 +++++++++++------------------------------------------- 1 file changed, 61 insertions(+), 250 deletions(-) diff --git a/README.md b/README.md index ad236bb..5108f70 100644 --- a/README.md +++ b/README.md @@ -1,299 +1,110 @@ -# Khao Pad (ข้าวผัด) +# Khao Pad — example deployment -**A modular CMS for Cloudflare** — lightweight, multilingual, and built for Thai software houses. +The official live demo of [Khao Pad](https://github.com/codustry/khaopad), the open-source website platform for Cloudflare. This repo is a real fork of the template, deployed to Cloudflare Workers with the same pipeline you'd use for your own project. -> ข้าวผัด = Fried rice. Everyone wants something slightly different, but in the end it's the same core dish — just with different sauces and ingredients. +🌐 **Live**: [khaopad-example.codustry.workers.dev](https://khaopad-example.codustry.workers.dev) -## Why Khao Pad? +## What this demo is -We kept running into the same CMS problem: +A content-rich showcase, not a generic placeholder. Read it to see what a finished Khao Pad site looks like before you commit to the platform. -| Solution | Problem | -| ------------------ | ----------------------------------------------------------------------------------- | -| Supabase | Great ecosystem, but $25/mo is heavy for small sites when Cloudflare is nearly free | -| Self-hosted Strapi | Too large, too many resources, needs separate deployment | -| Pages CMS | Great UI, but doesn't scale to D1/R2 when you need it | +- **Subject**: a short history of khao pad — Thailand's most ubiquitous dish — told as a 5-essay series across origin, royal kitchens, regional variants, day-old rice, and the modern global diaspora +- **Bilingual**: every essay published in English and Thai (well, English fully; Thai partial — exactly the editorial reality of most multilingual sites) +- **Brand polish**: paypers-style visual reskin on the public surface — sticky topbar with a `ข` glyph mark, IBM Plex Sans Thai + Inter Tight typography, radial gradient background, story-led hero, numbered essay-list blog index, generous reading column +- **All eleven Khao Pad milestones live**: SEO (full meta + JSON-LD + sitemap + RSS), analytics (privacy-friendly D1 page-views), comments (dual-toggle), forms, newsletter (single-opt-in with no provider configured), webhooks, public REST API. Everything you can do upstream is wired up here. -Khao Pad fills the gap: **start lightweight, scale when needed, stay on Cloudflare.** +## What's different from upstream -## Features +This fork carries: -- **One repo, one host, two surfaces** — public site at `/`, admin CMS at `/cms/*` -- **Multilingual first** — shared slug and media, separate content per language (TH/EN) -- **D1 + R2 storage** — articles in D1 (SQLite at the edge), media in R2, KV for caching. Sub-10ms reads from anywhere -- **Cloudflare-native** — D1 database, R2 media, KV caching, Workers deployment -- **Better Auth** — email/password auth with role-based access (Super Admin, Admin, Editor, Author) -- **Paraglide JS** — compile-time i18n with type-safe translations via inlang -- **SvelteKit + Tailwind + shadcn/ui** — modern, fast, beautiful +1. **Brand polish on the public surface** (4 files): `(www)/+layout.svelte`, `(www)/[locale]/+page.svelte`, `(www)/[locale]/blog/+page.svelte`, `(www)/[locale]/blog/[slug]/+page.svelte`. Same shadcn admin reskin as upstream — no example-specific changes inside `(cms)/`. +2. **Seed content**: the history-of-khao-pad essay series in EN + TH, the categories, tags, and the cover images (in R2). +3. **Custom i18n keys** for the home and blog intro (`home_eyebrow`, `home_title_a`, `home_title_b`, `home_subtitle`, `home_chip_*`, `blog_subtitle`). +4. **Wrangler config** points at the example's own D1 / R2 / KV bindings + the `khaopad-example.codustry.workers.dev` route. -## Architecture +Everything else flows from upstream via cherry-pick PRs. A typical upstream feature lands as one PR per milestone (e.g. PR #14 = v2.0d webhooks + REST API), with field-merged i18n keys and the brand-polish files preserved. -``` -┌──────────────────────────────────────────┐ -│ Single SvelteKit App │ -│ │ -│ hooks.server.ts (path-based surface) │ -│ │ -│ /* → (www)/ public site │ -│ /cms/* → (cms)/ admin panel │ -│ /api/auth/* → Better Auth handler │ -│ │ -│ ContentProvider → D1ContentProvider │ -│ │ -│ Cloudflare: D1 · R2 · KV · Workers │ -└──────────────────────────────────────────┘ -``` +## Use this fork as a starting point for your own project -## Tech Stack +Two paths. -- [SvelteKit](https://svelte.dev) — Full-stack framework -- [Tailwind CSS](https://tailwindcss.com) — Utility-first styling -- [shadcn/ui (svelte)](https://shadcn-svelte.com) — Component library -- [Drizzle ORM](https://orm.drizzle.team) — Type-safe SQL for D1 -- [Better Auth](https://better-auth.com) — Authentication -- [Paraglide JS](https://inlang.com/m/gerre34r/library-inlang-paraglideJs) — Compile-time i18n (inlang) -- [Cloudflare Workers](https://workers.cloudflare.com) — Edge deployment -- [Cloudflare D1](https://developers.cloudflare.com/d1/) — SQLite database -- [Cloudflare R2](https://developers.cloudflare.com/r2/) — Object storage -- [Cloudflare KV](https://developers.cloudflare.com/kv/) — Key-value cache +### A — Fork the upstream template (recommended for new projects) -## Using Khao Pad in your project +If you want a clean slate with no example-specific copy: -Khao Pad is a **template**, not a hosted service. Fork or clone this repo, provision your own Cloudflare resources, and deploy to your own account. Every project gets its own isolated D1 database, R2 bucket, and KV namespace — nothing is shared between installations. +1. Click **"Use this template"** on [`codustry/khaopad`](https://github.com/codustry/khaopad) +2. Follow the [Setup section in the upstream README](https://github.com/codustry/khaopad#setup) +3. Run `pnpm setup` to provision your own D1 / R2 / KV +4. Sign up at `/cms/signup` — first user becomes `super_admin`, signup then locks +5. Start writing -### Prerequisites +You get every feature, no demo content to delete. -- [Node.js](https://nodejs.org/) 22+ (for local tooling parity with CI) -- [pnpm](https://pnpm.io/) 9+ -- Cloudflare account -- Wrangler CLI (`pnpm add -g wrangler`) and `wrangler login` +### B — Fork this example (if you want the brand polish as a starting design) -### Setup +If you like the paypers-style public reskin and want to start from it: ```bash -# 1. Fork on GitHub (or clone directly) -git clone https://github.com/your-org/your-project.git -cd your-project +git clone https://github.com/codustry/khaopad-example.git my-site +cd my-site -# 2. Install dependencies +# Provision your own Cloudflare resources pnpm install - -# 3. Provision Cloudflare resources (D1 + R2 + KV) in one command pnpm setup -# Prints the database_id and KV id you need. Paste them into wrangler.toml -# (replace LOCAL_DB_ID and LOCAL_KV_ID). - -# 4. Set your Better Auth secret (any long random string) -wrangler secret put BETTER_AUTH_SECRET - -# 5. Apply migrations and seed sample data into local D1 -pnpm db:migrate -pnpm db:seed - -# 6. Start the dev server (Wrangler, uses local D1/R2/KV simulators) -pnpm wrangler:dev -# or plain Vite without bindings (shows a friendly 503 "Configuration required") -pnpm dev ``` -### How D1, R2, and KV connect to Khao Pad - -Cloudflare bindings are **not auto-generated** — they must be provisioned once per project, then bound to your Worker by ID in `wrangler.toml`: +Then in the CMS: -| Binding | Resource | Created by | Referenced in `wrangler.toml` | -| --------------- | ------------ | ---------------------------------- | ----------------------------- | -| `DB` | D1 database | `wrangler d1 create ` | `database_id` | -| `MEDIA_BUCKET` | R2 bucket | `wrangler r2 bucket create ` | `bucket_name` | -| `CONTENT_CACHE` | KV namespace | `wrangler kv namespace create` | `id` | +- Sign up your first user at `/cms/signup` (first becomes super_admin, signup then locks) +- Delete the seed articles via `/cms/articles` +- Delete the seed categories + tags +- Delete the seed media files in R2 (or via `/cms/media`) +- Update site name + locales in `/cms/settings` +- Edit the home/blog intro copy in `messages/en.json` + `messages/th.json` (search for `home_*` and `blog_subtitle`) -`pnpm setup` runs all three for you and prints the IDs to paste in. Your code never hardcodes account IDs or credentials — Cloudflare injects the bindings into `platform.env` at runtime. +You keep: the paypers shell, font choices, reading-column layout, and all eleven milestones of features. -For **local dev** with `pnpm wrangler:dev`, Wrangler spins up local simulators for D1/R2/KV automatically — the production IDs only matter when you deploy with `pnpm deploy`. +## Staying in sync with upstream -### Local Development +Khao Pad ships features in numbered milestones. To pull a new release into your fork: ```bash -pnpm dev -``` - -- Public site: `http://localhost:5173` -- CMS admin: `http://localhost:5173/cms` -- First admin signup: `http://localhost:5173/cms/signup` (one-shot, before any user exists) -- Login: `http://localhost:5173/cms/login` - -No `/etc/hosts` editing needed — both surfaces share one host. Locale switches via `/en/blog` ↔ `/th/blog` on the public side; the CMS reads locale from a cookie so admin URLs stay clean. - -## Content Model +git remote add upstream https://github.com/codustry/khaopad.git +git fetch upstream +git checkout -b cherry-pick/v2.X +git checkout upstream/main -- ``` -Article (shared) -├── id, slug (English ASCII), status, coverMedia, category, tags, author -├── Localization (EN) ← required, slug is derived from this title -│ └── title, excerpt, body (markdown), SEO fields -└── Localization (TH) - └── title, excerpt, body (markdown), SEO fields -``` - -Articles share the same slug and media across languages. Only the text content differs per locale. - -**Slugs are always English ASCII** (`^[a-z0-9]+(?:-[a-z0-9]+)*$`) and auto-generated from the English title via `slugify()`. The same slug serves every locale — there is no per-language slug. - -## Storage - -Articles, categories, tags, and user/session data live in **Cloudflare D1** (SQLite at the edge — sub-10ms reads worldwide). Media files (uploads, cover images) live in **Cloudflare R2**. Cached read-throughs sit in **KV**. - -Database access is mediated by a `ContentProvider` interface (`src/lib/server/content/types.ts`). The default and only shipped implementation is D1-backed; the interface is kept so test fixtures or alternate backends can slot in without rewriting call sites. - -## User Roles - -| Role | Create | Edit Own | Edit Any | Publish | Delete Any | Manage Users/Settings | -| ----------- | :----: | :------: | :------: | :-----: | :--------: | :-------------------: | -| Author | yes | yes | - | - | - | - | -| Editor | yes | yes | yes | yes | - | - | -| Admin | yes | yes | yes | yes | yes | yes | -| Super Admin | yes | yes | yes | yes | yes | yes | - -## Deployment - -Deploys automatically to Cloudflare Workers on push to `main` via GitHub Actions (`.github/workflows/deploy.yml`). The workflow runs `pnpm install --frozen-lockfile`, `pnpm build`, applies pending D1 migrations to the remote database, then deploys the Worker. - -### Config reference - -Khao Pad reads everything through Cloudflare's binding/env model. There are four layers — know which goes where: -| Layer | Where it lives | Scope | Example | -| ---------------------- | --------------------------------------- | ----------- | ----------------------------- | -| Bindings | `wrangler.toml` `[[d1_databases]]` etc. | Per project | `DB`, `MEDIA_BUCKET` | -| Plain vars | `wrangler.toml` `[vars]` | Per project | `CONTENT_MODE`, locales, URLs | -| Cloudflare secrets | `wrangler secret put` | Per project | `BETTER_AUTH_SECRET` | -| GitHub Actions secrets | GitHub repo/org → Settings → Secrets | CI only | `CLOUDFLARE_API_TOKEN` | +Two manual rules during the cherry-pick: -Secrets are **never** committed to `wrangler.toml`. Plain vars can be, and are safe to read in both server and client code (treat them like public config). +1. **`messages/*.json`**: field-merge new upstream keys into your file — never wholesale-overwrite. Your example-specific copy lives in the same JSON. +2. **The 4 paypers-reskinned files** (the `(www)` svelte files listed above): take upstream's diff against the **upstream** version of the file, and apply only the new wiring (Seo mount, CookieBanner, nav-menu iteration, beacon, etc.) into your existing paypers shell. -### 1. GitHub Actions secrets (for CI deploy) +Apply any new D1 migrations via `pnpm db:migrate:remote`, then PR into `main`: -Configured at the GitHub **org or repo** level. At Codustry they're already set on the organization and inherited by every repo: - -| Secret | Purpose | -| ----------------------- | -------------------------------------------------------- | -| `CLOUDFLARE_API_TOKEN` | Lets the CI runner call the Cloudflare API (deploy, D1). | -| `CLOUDFLARE_ACCOUNT_ID` | Tells `wrangler` which account to deploy into. | - -Token permissions required: **Workers Scripts — Edit**, **Account D1 — Edit**, **Account Workers KV Storage — Edit**, **Workers R2 Storage — Edit**, **Zone DNS — Read** (for routes). Create at `dash.cloudflare.com/profile/api-tokens` → "Edit Cloudflare Workers" template, then narrow to your account. - -### 2. Cloudflare secrets (Worker runtime, encrypted) - -Set once per environment via `wrangler secret put `: - -| Secret | Purpose | How to generate | -| -------------------- | ------------------------------------------------------------------ | ------------------------- | -| `BETTER_AUTH_SECRET` | Signs/encrypts Better Auth session cookies. Must be long + random. | `openssl rand -base64 32` | - -> **Never** put these in `[vars]` — they leak to the dashboard and CI logs. - -### 3. Cloudflare bindings (wrangler.toml) - -Provisioned once per project via `pnpm setup`, then referenced by ID: - -```toml -[[d1_databases]] -binding = "DB" # exposed as platform.env.DB -database_name = "khaopad-db" -database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # from `wrangler d1 create` -migrations_dir = "drizzle" - -[[r2_buckets]] -binding = "MEDIA_BUCKET" # exposed as platform.env.MEDIA_BUCKET -bucket_name = "khaopad-media" - -[[kv_namespaces]] -binding = "CONTENT_CACHE" # exposed as platform.env.CONTENT_CACHE -id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # from `wrangler kv namespace create` +```bash +gh pr create --title "feat(v2.X): cherry-pick upstream PR #N" ``` -### 4. Plain environment variables (wrangler.toml `[vars]`) +The full sync history is in the merged PRs of this repo — every cherry-pick PR's title links back to the upstream PR number. -Non-secret config that ships with the Worker: +## Local dev -| Var | Required | Default | Purpose | -| ------------------- | :------: | --------------------- | ------------------------------------------------------------------------------- | -| `SUPPORTED_LOCALES` | yes | `en,th` | Comma-separated. Must match `project.inlang/settings.json`. | -| `DEFAULT_LOCALE` | yes | `en` | Fallback locale. Must be in `SUPPORTED_LOCALES`. | -| `PUBLIC_SITE_URL` | yes | `https://example.com` | Canonical origin for both surfaces (one host). | -| `CMS_SITE_URL` | yes | = `PUBLIC_SITE_URL` | Deprecated alias kept for media URL generation; same host as public since v1.1. | -| `BETTER_AUTH_URL` | yes | = `PUBLIC_SITE_URL` | Base URL Better Auth uses in callbacks/cookies. | - -### 5. Routes & DNS (production only) - -Uncomment the `routes` block in `wrangler.toml` after your domain is on Cloudflare: - -```toml -routes = [ - { pattern = "example.com/*", zone_name = "example.com" }, -] +```bash +pnpm install +pnpm wrangler:dev ``` -A single proxied (orange-cloud) DNS record pointing at the Worker is enough — Cloudflare terminates TLS and the `surfaceHook` in `hooks.server.ts` decides whether each request is the public site (`/`) or the admin CMS (`/cms/*`). - -### 6. Local dev - -- `pnpm wrangler:dev` (recommended) — Wrangler spins up local simulators for D1/R2/KV. Reads `wrangler.toml` `[vars]`; secrets come from `.dev.vars` (gitignored). The production `database_id`/KV `id` are ignored locally — a local SQLite file is used instead. -- `pnpm dev` — plain Vite, no Cloudflare runtime. Renders the 503 "Configuration required" screen so missing bindings are obvious. - -Create `.dev.vars` (gitignored) for local-only secrets: - -``` -BETTER_AUTH_SECRET=dev-local-only-not-a-real-secret -``` +- Public: `http://localhost:5173` +- CMS: `http://localhost:5173/cms` -### Deployment checklist - -- [ ] `pnpm setup` ran and `wrangler.toml` has real `database_id` + KV `id` -- [ ] `BETTER_AUTH_SECRET` set in Cloudflare (`wrangler secret put BETTER_AUTH_SECRET`) -- [ ] `PUBLIC_SITE_URL`, `CMS_SITE_URL`, `BETTER_AUTH_URL` updated to real domain in `[vars]` -- [ ] `routes` block uncommented with real domain + zone -- [ ] DNS record points to the Worker in Cloudflare -- [ ] GitHub org/repo has `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` -- [ ] `pnpm build` passes locally -- [ ] Migrations applied remote (`pnpm db:migrate:remote`) or CI will do it on first push - -## Scripts - -| Command | Description | -| ------------------------ | -------------------------------------- | -| `pnpm dev` | Start local dev server | -| `pnpm build` | Build for production | -| `pnpm db:generate` | Generate migration from schema changes | -| `pnpm db:migrate` | Apply migrations locally | -| `pnpm db:migrate:remote` | Apply migrations to production D1 | -| `pnpm deploy` | Build and deploy to Cloudflare Workers | - -## Roadmap - -Khao Pad started as a CMS. Through v1.5 it became a complete content layer (write, schedule, search, version, audit). v1.6 onward turns it into the **driver of a non-ecommerce website** — meaning a site owner installs Khao Pad and gets the content layer **plus** the surrounding machinery a real website needs (SEO, analytics, IA, performance, engagement). - -| Version | Theme | Status | Highlights | -| -------- | --------------------------- | ------------ | --------------------------------------------------------------------------------------------------- | -| **v1.0** | MVP | ✅ Shipped | M1–M7: scaffold, D1 migrations, Better Auth, media library, taxonomy, deploy pipeline, MD editor | -| **v1.1** | Path-prefix routing | ✅ Shipped | `/cms/*` instead of `cms.` subdomain, shadcn admin reskin, D1+Date binding fix, scope tightening | -| **v1.2** | Users & settings UIs | ✅ Shipped | `/cms/users` (roles, last-super-admin guard), `/cms/settings`, `canManageUser` permission helper | -| **v1.3** | Workflow trio | ✅ Shipped | Token invitations, audit-log viewer, scheduled publishing | -| **v1.4** | Full-text search | ✅ Shipped | SQLite FTS5 over per-locale localizations, public `/blog?q=`, CMS list filter | -| **v1.5** | Content versioning | ✅ Shipped | Per-article revision history, line diff, one-click restore, attribution | -| **v1.6** | SEO foundations | ✅ Shipped | Per-page meta, sitemap, robots, JSON-LD, RSS/Atom, slug redirects, SEO scoring hint | -| **v1.7** | Pages, navigation, IA | ✅ Shipped | Media folders, reusable blocks, cookie consent, static pages, navigation manager, seed:legal | -| **v1.8** | Analytics & insight | ✅ Shipped | Privacy-friendly D1 page-views, top articles, search-term insights, per-article sparkline, optional CWA | -| **v1.9** | Performance & trust | ✅ Shipped | Responsive `srcset` via /cdn-cgi/image, edge cache-control hook, branded 404/500, /api/health endpoint | -| **v2.0** | Engagement & growth | ✅ Shipped | a Forms · b Newsletter (optional) · c Comments · d Webhooks + Public REST API | - -**Backlog** (not committed): OAuth providers, block-based editor, AI-assisted authoring, multi-site / workspaces, A/B testing, member-only / paid content. - -See [docs/MILESTONES.md](docs/MILESTONES.md) for the detail block on every shipped and pending milestone, and [open issues](https://github.com/codustry/khaopad/issues) for the per-milestone tracking. +For full architecture, bindings, deployment, and config docs, see the [upstream README](https://github.com/codustry/khaopad#readme) — this fork doesn't repeat it. ## License -MIT — Codustry +MIT — Codustry. Same as upstream. --- -Built with 🍳 by [Codustry](https://codustry.com) +Built with 🍳 by [Codustry](https://codustry.com) · Powered by [Khao Pad](https://github.com/codustry/khaopad)