[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-81608":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":15,"subscribersCount":15,"size":15,"stars1d":13,"stars7d":13,"stars30d":13,"stars90d":15,"forks30d":15,"starsTrendScore":16,"compositeScore":17,"rankGlobal":10,"rankLanguage":10,"license":18,"archived":19,"fork":19,"defaultBranch":20,"hasWiki":19,"hasPages":19,"topics":21,"createdAt":10,"pushedAt":10,"updatedAt":29,"readmeContent":30,"aiSummary":31,"trendingCount":15,"starSnapshotCount":15,"syncStatus":13,"lastSyncTime":32,"discoverSource":33},81608,"cms","skelpo\u002Fcms","skelpo","A blazingly fast, opinionated, native TypeScript CMS. Designed for Perry AOT, runs on Node and Bun.","https:\u002F\u002Fskelpo.com",null,"TypeScript",25,2,23,0,6,1.43,"MIT License",false,"main",[5,22,23,24,25,26,27,28],"headless-cms","hono","htmx","mysql","native","perry","typescript","2026-06-12 02:04:17","# Skelpo CMS\n\n> A blazingly fast, opinionated, native TypeScript CMS for agencies and small businesses.\n> Designed for [Perry](https:\u002F\u002Fgithub.com\u002FPerryTS\u002Fperry) AOT compilation. Runs on Node and Bun too.\n\n**Status:** v0.1 (2026-05-20). Backend + HTMX admin + `@skelpo\u002Fcms-client` + `@skelpo\u002Fsite-kit` + CLI all implemented and end-to-end verified; perry.land is the proven first sample case (see `docs\u002Fperry-landing-integration.md`).\n\n[![License: MIT](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002FLicense-MIT-yellow.svg)](.\u002FLICENSE)\n[![@skelpo\u002Fcms-client](https:\u002F\u002Fimg.shields.io\u002Fnpm\u002Fv\u002F@skelpo\u002Fcms-client?label=%40skelpo%2Fcms-client)](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002F@skelpo\u002Fcms-client)\n[![@skelpo\u002Fsite-kit](https:\u002F\u002Fimg.shields.io\u002Fnpm\u002Fv\u002F@skelpo\u002Fsite-kit?label=%40skelpo%2Fsite-kit)](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002F@skelpo\u002Fsite-kit)\n\n**License:** MIT. **Maintained by** [Skelpo GmbH](https:\u002F\u002Fskelpo.com).\n\n---\n\n## Table of contents\n\n- [What is Skelpo CMS](#what-is-skelpo-cms)\n- [Philosophy](#philosophy)\n- [Architecture](#architecture)\n- [Performance budget](#performance-budget)\n- [Data model](#data-model)\n- [Permissions](#permissions)\n- [Schema evolution](#schema-evolution)\n- [Cache & invalidation](#cache--invalidation)\n- [SEO & agent optimization](#seo--agent-optimization)\n- [Customer frontend (the public site)](#customer-frontend-the-public-site)\n- [Upgradability](#upgradability)\n- [Multi-runtime support](#multi-runtime-support)\n- [Feature scope](#feature-scope)\n- [v0.1 deliverables](#v01-deliverables)\n- [Repository layout](#repository-layout)\n- [References](#references)\n\n---\n\n## What is Skelpo CMS\n\nSkelpo CMS is a content management system for the kind of websites most of the web actually consists of: agency homepages, small business sites, marketing sites, documentation portals, blogs. **Not** a platform, not a SaaS, not e-commerce, not a forum — those have their own better-suited tools (e.g. [Medusa](https:\u002F\u002Fmedusajs.com) for commerce).\n\nIt is:\n\n- **Headless** — backend API + admin UI only; the public website is a separate codebase owned by the customer.\n- **API-first** — every admin action goes through the same REST API a mobile app or external integration would use.\n- **Opinionated** — one rich-text editor, one set of field types, one set of email backends. We make the choices so the user doesn't have to.\n- **Blazingly fast** — designed for Perry AOT compilation. Sub-2ms cached responses. 100K+ RPS on commodity hardware. \u003C50ms cold start.\n- **Multi-runtime** — runs on Perry (recommended), Node, or Bun. Same code, three artifacts.\n- **MySQL-backed** — one database, no choice paralysis.\n- **Single-tenant** — one deploy per site. No shared-state surprises. Multi-tenancy is out of scope.\n- **Upgrade-safe** — the CMS binary and the customer's frontend binary upgrade independently. No file migrations. No theme merges. No `wp-content\u002F` dance.\n\nIt is **not**:\n\n- A page builder with drag-drop block editing (TipTap rich text is the editor)\n- A theme marketplace (no themes — the customer's frontend is the theme)\n- A plugin platform with arbitrary code execution (custom content types + webhooks are the extension surface)\n- An e-commerce platform (use Medusa)\n- A membership\u002Fsubscription system\n- A multi-tenant SaaS\n\n---\n\n## Philosophy\n\nThe opinionated stances, stated plainly. These are non-negotiable in v1; they're load-bearing for the design.\n\n1. **MySQL only.** No \"supports MySQL\u002FPostgres\u002FSQLite.\" MySQL via [`@perryts\u002Fmysql`](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002F@perryts\u002Fmysql) — a pure-TypeScript wire-protocol driver, zero native deps. Runs on Perry, Node, and Bun. AOT-compiles cleanly.\n2. **No themes inside the CMS.** The customer's frontend is the theme. The CMS doesn't render public pages.\n3. **No plugins.** Custom content types + webhooks are the only extension surface. Arbitrary code execution is a security and upgrade nightmare we explicitly avoid.\n4. **TipTap rich text.** No Gutenberg blocks. No alternate editors. No raw HTML pasting (TipTap JSON only — keeps cache + render invariants safe).\n5. **Meta description and image alt text are required.** Publish is blocked without them. SEO + accessibility + LLM-friendliness aren't optional.\n6. **Forms have a fixed set of 11 field types.** That's all. Don't ask.\n7. **Email sending is always async.** No synchronous send. Failures retry. Submissions always persist regardless of mail success.\n8. **One email backend at a time.** Configure SMTP *or* Resend *or* Postmark *or* SES. No \"fallback chains.\"\n9. **Server-side analytics, no client JS.** GDPR-compliant by default (anonymized IP hashes, no cookies, no PII).\n10. **Single-tenant, single-binary CMS.** One deploy per site. No multi-tenant mode.\n11. **Performance is a CI gate.** The perf budget below is enforced. PRs that break it fail.\n12. **The CMS admin is uniform.** Customers don't theme it. Editors get a consistent experience across all Skelpo sites.\n13. **Editors never rebuild.** Content, menus, settings, forms, redirects — all changes go live via webhook+cache, no recompilation.\n14. **Developers always control structure.** HTML, CSS, JS — fully owned by the customer's frontend codebase. Recompile + deploy on design changes.\n15. **Native deps in core: zero.** No Sharp, no native argon2, no tree-sitter, no `node-gyp`. Image processing via imgproxy sidecar.\n\nIf you disagree with any of these, Skelpo CMS is probably not your tool. That's fine — pick one of the many other excellent CMSes.\n\n---\n\n## Architecture\n\nSkelpo CMS is a **two-binary system**: a backend and a frontend. They communicate over HTTP.\n\n```\n┌──────────────────────────────────────┐    ┌──────────────────────────────────┐\n│ skelpo-cms (backend)                  │    │ skelpo-site-\u003Ccustomer> (frontend)│\n│                                       │    │                                   │\n│  - REST API (\u002Fapi\u002Fv1\u002F*)               │    │ - Customer-owned codebase        │\n│  - Admin UI (HTMX, \u002Fadmin\u002F*)          │◄───┤ - Full HTML\u002FCSS\u002FJS control       │\n│  - Auth (sessions + bearer tokens)    │    │ - Their own JSX templates        │\n│  - Media uploads + imgproxy signing   │    │ - Their own design system        │\n│  - Forms + email + jobs               │    │ - Perry-compiled (or Node\u002FBun)   │\n│  - Webhooks (outbound)                │───►│ - Receives webhooks for live     │\n│  - Single Perry binary, ~15 MB        │    │   cache invalidation             │\n└──────────────────────────────────────┘    └──────────────────────────────────┘\n                  │                                          │\n              MySQL                              uses @skelpo\u002Fcms-client\n              media (S3\u002Flocal + imgproxy)              and @skelpo\u002Fsite-kit\n```\n\n### What the CMS binary does\n\n- Serves the REST API on `\u002Fapi\u002Fv1\u002F*` — content, types, media, users, menus, forms, settings, webhooks, search, analytics\n- Serves the HTMX admin UI on `\u002Fadmin\u002F*` — uniform across all Skelpo sites\n- Handles authentication (sessions for browser, bearer tokens for SDK\u002Fmobile)\n- Runs background jobs (publish scheduled content, send emails, fire webhooks, regen sitemaps)\n- Validates all writes; enforces required SEO\u002Faccessibility fields at publish time\n- Never renders public-facing HTML\n\n### What the customer's frontend does\n\n- Renders **every public page** with full HTML\u002FCSS\u002FJS control\n- Calls the CMS API via `@skelpo\u002Fcms-client` to fetch content, menus, settings\n- Handles its own caching (the SDK provides this)\n- Receives webhook notifications from the CMS on content changes → invalidates cache → next visitor sees fresh data\n- Implements public routes (catchall pattern resolves any content URL → API → template)\n- Owned and deployed by the customer; recompiled when design changes\n\n### Three published artifacts per release\n\n| Artifact | Format | Target use |\n|---|---|---|\n| **Perry binary** | Single executable (~15 MB) | Recommended production |\n| **Docker image** | `skelpo\u002Fcms:1.x.y` | Container deploys |\n| **npm package** | `@skelpo\u002Fcms` (CJS+ESM) | Users on Node\u002FBun who want their existing runtime |\n\nSame source code produces all three.\n\n---\n\n## Performance budget\n\nThese are CI-gated. PRs that break them fail. Numbers are on Perry; Node\u002FBun are slower but still beat WordPress and Strapi.\n\n| Metric | Target (Perry) | Target (Bun) | Target (Node) |\n|---|---|---|---|\n| Binary \u002F package size | \u003C20 MB | \u003C50 MB | \u003C100 MB |\n| Cold start | \u003C50 ms | \u003C200 ms | \u003C800 ms |\n| Cached page TTFB (p99 local) | \u003C2 ms | \u003C5 ms | \u003C10 ms |\n| Uncached page TTFB (p99 local) | \u003C10 ms | \u003C30 ms | \u003C60 ms |\n| Memory idle RSS | \u003C50 MB | \u003C100 MB | \u003C200 MB |\n| Cached RPS (single instance) | 100K+ | 30-50K | 10-20K |\n| DB queries per cached request | **0** | 0 | 0 |\n| DB queries per uncached page | ≤3 | ≤3 | ≤3 |\n| Cache hit ratio (public traffic) | >95% | >95% | >95% |\n\n### How we hit these numbers\n\n- **Render at write, not at read.** Published content is rendered into a compressed HTML buffer on publish; cached buffer is what's served on read.\n- **Compiled JSX templates.** No template engine, no AST walking per request. (Customer's site benefits the same way via Perry compilation.)\n- **Brotli pre-compressed cache.** Cached buffer is already wire bytes; `write(2)` directly to socket.\n- **Zero allocations on cache hit.** Return a pointer to the cached buffer.\n- **Native syscalls.** `sendfile(2)` for static assets and cached HTML; `splice`-style zero-copy where possible (on Perry).\n- **In-process cache with surgical dependency graph.** No Redis required for single-tenant deploys.\n- **Single-binary horizontal scaling.** \u003C50ms cold start means autoscale-from-zero is real.\n- **One indexed query per content fetch.** Custom fields in a JSON column; relations fetched in one second query if `?include=` is used.\n\n### Comparison vs. existing CMSes (target)\n\n| | WordPress | Strapi 5 | Directus | Payload v3 | **Skelpo (Perry)** |\n|---|---|---|---|---|---|\n| Cold start | ~500 ms | ~2000 ms | ~3000 ms | ~3000 ms | **\u003C50 ms** |\n| RPS (cached) | ~500 | ~5K | ~5K | ~5K | **>100K** |\n| RPS (uncached) | ~50 | ~1K | ~1K | ~1K | **>10K** |\n| Memory idle | 100MB×N workers | ~300 MB | ~400 MB | ~500 MB | **\u003C50 MB** |\n| Native deps | PHP + ext | Node + 1000 npm | Node + 800 npm + Sharp | Node + Next + 1500 npm | **just imgproxy** |\n| Static export built-in | No | No | No | No | **Yes** |\n\n### Measured — Node vs Perry (direct head-to-head)\n\nIdentical Fastify source, same machine (M-series Mac), same harness\n(`autocannon`, 50 conns × 20 s). Full writeup +\n[scripts\u002Fbench-twin\u002F](scripts\u002Fbench-twin\u002F) reproducer in\n[docs\u002Fbenchmarks-perry-vs-node.md](docs\u002Fbenchmarks-perry-vs-node.md);\ndeployed-CMS end-to-end numbers in [docs\u002Fbenchmarks.md](docs\u002Fbenchmarks.md).\n\n| Axis | Node + tsx | Perry native | Δ |\n|---|---:|---:|---:|\n| Cold start (spawn → 200) | 730 ms | **44 ms** | ≈17× faster |\n| RPS, \u002Floop (CPU bound) | 49,947 | **67,197** | +35% |\n| RPS, \u002Fjson (1KB serialize) | 53,498 | **65,766** | +23% |\n| RPS, \u002Fhealthz (tiny JSON) | 58,522 | **65,723** | +12% |\n| RSS, idle | 86 MB | **11 MB** | ≈8× smaller |\n| Distributable | ~105 MB (node + node_modules) | **3.5 MB** binary | ≈30× smaller |\n\nResponses are byte-identical (md5-verified). Throughput is +20% on\naverage at this concurrency; the lopsided wins are cold start (≈17×),\nidle memory (≈8×), and deployable size (≈30×) — the axes that matter\nfor autoscale-from-zero, FaaS, and edge\u002FCLI shapes.\n\n---\n\n## Data model\n\nThe full SQL schema lives at [`docs\u002Fschema.md`](docs\u002Fschema.md) (next deliverable). High-level overview:\n\n### Core tables\n\n- **`contentTypes`** — type definitions including the JSON `fieldsSchema`\n- **`contentTypeRevisions`** — schema history, enables lazy migration\n- **`content`** — every piece of content (built-in types + custom), with JSON `fields`, `seo`, `ai` columns\n- **`contentRelations`** — many-to-many relation links\n- **`contentRevisions`** — content edit history per row\n\n### Auth & permissions\n\n- **`users`** — with bcryptjs password hashes + optional TOTP\n- **`roles`** — capability bundles (JSON)\n- **`sessions`** — DB-backed sessions for admin browser\n\n### Operations\n\n- **`media`** — uploaded files (alt text per locale, focal points)\n- **`menus`** + **`menuItems`** — drag-drop-buildable in admin\n- **`settings`** — flat key-value store (`site.name`, `seo.organizationSchema`, etc.)\n- **`redirects`** — 301\u002F302 management (critical for SEO when URLs change)\n- **`emailTemplates`** — editable templates with variable interpolation\n- **`formSubmissions`** — every form submission persists, regardless of email success\n- **`jobs`** — DB-backed background queue (sendEmail, preRender, webhookDispatch, regenSitemap, etc.)\n- **`webhooks`** + **`webhookDeliveries`** — outbound webhook config + audit log\n- **`analyticsEvents`** — server-side pageviews, partitioned monthly, GDPR-safe (no PII)\n- **`auditLog`** — who did what when\n\n### Field types in the ACF-style schema\n\nStored in `contentTypes.fieldsSchema` as JSON. Field types in v1:\n\n`text, textarea, richtext, number, boolean, date, datetime, email, url, color, select, multiselect, image, gallery, file, relation, repeater, json`\n\nEach field declares `name`, `type`, `label`, `translatable`, `required`, `validation`, and an optional `admin` block for editor hints.\n\n---\n\n## Permissions\n\nRole-based with per-content-type granularity. A user has one role; a role has a JSON capability bundle:\n\n```json\n{\n  \"global\": [\"manageUsers\", \"manageRoles\", \"viewAnalytics\"],\n  \"types\": {\n    \"page\":    [\"read\", \"create\", \"update\", \"delete\", \"publish\", \"readDrafts\"],\n    \"post\":    [\"read\", \"create\", \"update\", \"delete\", \"publish\", \"readDrafts\"],\n    \"service\": [\"read\", \"create\", \"updateOwn\", \"deleteOwn\"],\n    \"*\":       [\"read\"]\n  }\n}\n```\n\n**Per-type capabilities:** `read, create, update, updateOwn, delete, deleteOwn, publish, readDrafts, readOthersDrafts`.\n\n**Global capabilities:** `manageUsers, manageRoles, manageTypes, manageSettings, manageMenus, manageRedirects, manageMedia, manageForms, viewAnalytics, viewAuditLog, exportData, manageJobs`.\n\n**Built-in roles seeded at install:**\n\n- `admin` — everything\n- `editor` — full content CRUD + publish on all types; no user\u002Frole\u002Fsettings management\n- `author` — CRU + publish on own posts; read on pages\n- `contributor` — CRU + `updateOwn` on assigned types; no publish (sends to review)\n- `viewer` — read-only admin\n\nThe single `can(user, action, type?, ownerId?)` function gates every admin\u002FAPI action. Per-request memoized.\n\n---\n\n## Schema evolution\n\nAdding fields to a content type without downtime, without batch-updating all existing rows. The mechanism: **versioned schemas + lazy migration on read**.\n\n- Each content type has a `currentRevision` integer\n- Every schema change creates a row in `contentTypeRevisions` with the new `fieldsSchema` and a `changes` JSON describing the diff (`added`, `removed`, `renamed`, `retyped`)\n- Every `content` row stores the `schemaRevision` it was saved against\n- On read, if `content.schemaRevision \u003C type.currentRevision`, walk revisions forward and apply changes to the `fields` JSON in memory\n- On next save, the migrated state is persisted; `schemaRevision` is bumped\n\n**Operation safety:**\n\n- Add optional field → silent; existing rows get default on read\n- Add required field → modal asks for default value OR \"mark existing as needs-review (block re-publish)\" OR cancel\n- Remove field → data preserved in `_legacy` namespace; 30-day grace before hard purge\n- Rename field → silent if same type; auto-copy\n- Change type → explicit transform required; rows that fail conversion flagged\n\n**Cache invalidation:** schema-revision bump invalidates `type-list:\u003Cslug>:*` and all `content:*` of that type; admin can dry-run to see affected rows first.\n\n---\n\n## Cache & invalidation\n\nThe core perf strategy. The cache is not a plugin — it's the primary code path.\n\n### Two in-memory structures\n\n```\ncache:  Map\u003CcacheKey, CacheEntry>          \u002F\u002F LRU-bounded\ndeps:   Map\u003CdepKey, Set\u003CcacheKey>>          \u002F\u002F reverse index for invalidation\n```\n\n`cacheKey` = canonical request signature, e.g. `GET:\u002Fen\u002Fabout:guest`.\n`depKey` examples: `content:42`, `type-list:post:en`, `setting:site.name`, `menu:main`.\n\n### Render path\n\n1. Request → compute cache key → cache hit? Yes: return cached buffer.\n2. Miss → resolve route → fetch content + relations (≤3 queries) → render JSX → record dep-keys → compress (brotli) → store → return.\n3. Subsequent identical requests hit cache (target \u003C2ms TTFB).\n\n### Invalidation\n\nOn content publish\u002Fupdate\u002Fdelete, on menu change, on setting change, on schema change:\n\n1. Compute affected `depKey`s\n2. Look up reverse-deps → set of `cacheKey`s\n3. Delete those cache entries + their reverse-index entries\n4. (Optional) Pre-render hot pages on background thread\n5. Fire webhook with same `depKeys` so customer's frontend invalidates *its* cache the same way\n\n### CDN integration\n\nSurrogate-Key headers carry the same `depKeys` so Fastly\u002FCloudflare can do surgical purges with the same vocabulary. No invalidation drift between layers.\n\n---\n\n## SEO & agent optimization\n\nWe enforce SEO data quality at the API layer and provide ready-made markup helpers in `@skelpo\u002Fsite-kit`. The split:\n\n### Enforced in `skelpo-cms` (data quality)\n\nRequired to publish — the publish endpoint returns `validationError` listing all failures on one response:\n\n1. `seo.metaDescription` present, 70-160 chars\n2. `title` ≤ 60 chars (warn 60-70, block at 70+)\n3. Every image in rich text content has `altText` set for the published locale\n4. Hero\u002FOG image is present (auto-uses first content image as fallback)\n5. Slug is URL-safe + ≤ 75 chars\n6. Canonical URL points to self unless explicitly overridden\n7. No two published rows share `(type, slug, locale)`\n\n### Provided in `@skelpo\u002Fsite-kit` (markup helpers, opt-in)\n\nComponents the customer's frontend can drop in to get the SEO contract:\n\n- `\u003CHead content={x} site={settings} locale={l} \u002F>` — emits title, meta description, canonical, hreflang alternates, OG, Twitter Card\n- `\u003CJsonLd type=\"Article\" content={x} \u002F>` — emits schema.org JSON-LD per content type\n- `\u003CImage src={mediaId} alt={...} sizes={...} priority \u002F>` — emits `\u003Cpicture>` with srcset, AVIF\u002FWebP\u002FJPEG sources, correct width\u002Fheight\u002Floading\u002Ffetchpriority\n- `\u003CForm name=\"contact\" \u002F>` — renders form from CMS definition; POSTs to `\u002Fapi\u002Fv1\u002Fforms\u002Fcontact\u002Fsubmit`\n- `Sitemap.respond({ cms })` — generates `sitemap.xml` route handler\n- `Robots.respond({ settings })` — generates `robots.txt`\n- `Llms.respond({ cms })` — generates `llms.txt` from per-content `ai.summary` fields\n- `Feed.respond({ cms, type: 'post' })` — generates RSS\u002FAtom\n\nIf the customer uses these defaults, they get the same SEO output the original \"one binary\" design would have produced. They can replace any of them without losing the data-layer guarantees.\n\n### Schema.org types per content type\n\n| Content type | Default schema.org type |\n|---|---|\n| Page | `WebPage` |\n| Post | `Article` \u002F `BlogPosting` |\n| Doc | `TechArticle` |\n| Service (custom) | `Service` |\n| Person (custom) | `Person` |\n| Event (custom) | `Event` |\n| Product (custom) | `Product` |\n| Home page | `WebSite` + `Organization` always present |\n\nOverridable per content row via `seo.schemaType`.\n\n---\n\n## Customer frontend (the public site)\n\nThe customer's site is a **separate Perry codebase** (or Node\u002FBun) that:\n\n- Has its own git repo (`skelpo-site-\u003Ccustomer>`)\n- Owns all HTML, CSS, JS, layout, design\n- Uses `@skelpo\u002Fcms-client` to call the CMS API\n- Uses `@skelpo\u002Fsite-kit` (optional) for SEO helpers\n- Receives webhooks from the CMS on content changes\n- Is recompiled and redeployed by developers when templates change\n- Is **not** rebuilt when editors change content\u002Fmenus\u002Fsettings — those update live\n\n### Catchall routing pattern\n\nThe customer's frontend has one catchall route:\n\n```tsx\n\u002F\u002F src\u002Froutes\u002F[...path].tsx\nimport { cms } from '.\u002Flib\u002Fcms'\nimport { PageTemplate, PostTemplate, DocTemplate, DefaultTemplate } from '.\u002Ftemplates'\n\nexport default async function CatchallRoute({ path, locale }) {\n  const resolved = await cms.content.byPath(path.join('\u002F'), { locale })\n  if (resolved.redirect) return Response.redirect(resolved.redirect.to, resolved.redirect.status)\n  if (!resolved.content) return notFound()\n\n  switch (resolved.content.type) {\n    case 'page':    return \u003CPageTemplate    content={resolved.content} \u002F>\n    case 'post':    return \u003CPostTemplate    content={resolved.content} \u002F>\n    case 'doc':     return \u003CDocTemplate     content={resolved.content} \u002F>\n    default:        return \u003CDefaultTemplate content={resolved.content} \u002F>\n  }\n}\n```\n\nAdding a page in admin = new content row → catchall resolves → template renders → live. No rebuild.\n\n### Webhook handler (one line of wiring)\n\n```ts\nimport { createClient, webhookHandler } from '@skelpo\u002Fcms-client'\n\nconst cms = createClient({\n  url: process.env.CMS_URL,\n  token: process.env.CMS_TOKEN,\n  cache: 'auto',\n  webhookSecret: process.env.WEBHOOK_SECRET\n})\n\napp.post('\u002Fwebhook\u002Fcms', webhookHandler(cms))\n```\n\nThat's it. Cache invalidation is wired up; content changes propagate in ~100-500ms.\n\n### Live vs. rebuild — the divide\n\n| Change | Live (no rebuild) | Requires rebuild |\n|---|---|---|\n| Page text, title, body | ✅ | |\n| Menu items, order, nesting | ✅ | |\n| New pages, new posts | ✅ | |\n| Redirects | ✅ | |\n| Settings (site name, social, contact) | ✅ | |\n| Forms (fields, success message) | ✅ | |\n| Media uploads, logo change | ✅ | |\n| New custom content type fields | ✅ (visible in admin instantly; on site if template references) | |\n| HTML structure \u002F layout | | ✅ |\n| CSS \u002F colors \u002F fonts | | ✅ |\n| New page templates \u002F routes | | ✅ |\n| Brand-new content type with dedicated rendering | (admin: live) | (frontend: yes, add case branch) |\n\n---\n\n## Upgradability\n\nTwo release cycles, fully decoupled.\n\n### Upgrading the CMS\n\n```bash\n# Docker\ndocker pull skelpo\u002Fcms:1.2.3 && docker compose up -d\n\n# Bare binary\ncurl -L https:\u002F\u002Freleases.skelpo.com\u002Fcms\u002F1.2.3\u002Fskelpo-cms-linux-x64 -o skelpo-cms.new\nmv skelpo-cms.new skelpo-cms && systemctl restart skelpo-cms\n```\n\nOn first boot of new version:\n\n1. Run pending migrations from `migrations\u002F*.sql` (tracked in `migrations` table; idempotent)\n2. Reconcile built-in content types (schema evolution applied via revision system)\n3. Reconcile built-in roles + capabilities (new caps added, never overwrites custom)\n4. Reconcile built-in email templates (only seeds missing ones — user edits preserved)\n5. Reconcile default settings (only seeds missing keys)\n6. Bump static asset version stamp\n7. Boot HTTP server\n\nWhole sequence is \u003C500ms on warm DB. Behind a load balancer with two instances: zero downtime.\n\n### Upgrading the customer's frontend\n\n```bash\n# In the customer's site repo\ngit pull && npm ci && perry build && deploy\n```\n\nThis is the customer's release cycle, on the customer's schedule. The CMS doesn't care.\n\n### Semver contract\n\n- **Patch (1.2.x):** bug + perf + security. Always safe.\n- **Minor (1.x.0):** new features, additive only at the DB\u002FAPI level. Schema evolution keeps old content readable. Always backwards-compatible.\n- **Major (x.0.0):** may change `@skelpo\u002Fcms-client` or `@skelpo\u002Fsite-kit` API. Migration guide published. Shipped ~yearly.\n\nThe CMS REST API has its own version path (`\u002Fapi\u002Fv1`); breaking API changes bump to `\u002Fapi\u002Fv2` with the old version supported alongside for a deprecation window.\n\n### No files to manage\n\nThe deployment is:\n\n```\n\u002Fsrv\u002Fskelpo\u002F\n├── skelpo-cms        ← the binary (upgrade target)\n├── .env              ← config (rarely changed)\n└── uploads\u002F          ← media (if local storage; alternatively S3)\n```\n\n- **DB stays put** during upgrades — never touched\n- **Media stays put** — never touched\n- **No `wp-content\u002F` to merge**\n- **No theme files to back up**\n- **No plugins to update**\n\nBackup: `skelpo-cms backup > site.skelpo-backup` produces a single file (DB dump + media tarball). Restore: `skelpo-cms restore site.skelpo-backup`. Done.\n\n---\n\n## Multi-runtime support\n\nDesigned for Perry. Supported on Node 22+ and Bun 1.2+ — same source code, three artifacts.\n\n### What works the same on all three\n\n- Hono HTTP framework\n- `@perryts\u002Fmysql` (pure-TS wire-protocol driver, zero native deps; runs on Perry\u002FNode\u002FBun)\n- `node:fs\u002Fpromises`, `node:zlib` (brotli\u002Fgzip)\n- Web APIs: `fetch`, `Request`, `Response`, `URL`, `Headers`, `crypto.subtle`, `crypto.getRandomValues`, `TextEncoder`, `TextDecoder`, `ReadableStream`\n- bcryptjs (pure JS) for password hashing\n- Shiki (pure JS) for syntax highlighting\n- TipTap JSON → HTML renderer (pure TS)\n- Pure-TS SMTP client; HTTP-based clients for Resend\u002FPostmark\u002FSES\n\n### What needs runtime detection (the small platform layer)\n\n- **HTTP server boot** — entry point detects Perry\u002FBun\u002FNode and uses the appropriate Hono adapter\n- **Static asset embedding** — Perry can embed via compile-time include; Node\u002FBun load from `dist\u002Fstatic\u002F` at boot\n- **Background workers (future)** — `perry\u002Fthread` on Perry, `node:worker_threads` on Node\u002FBun. V1 uses in-process polling on all three.\n\nThe platform-specific surface is ~5 small files. Everywhere else is standard TypeScript.\n\n### What we explicitly avoid\n\n- ❌ `sharp` (native C++) — use imgproxy sidecar\n- ❌ Native bindings (`@node-rs\u002F*`, `node-gyp`-required packages)\n- ❌ `node:child_process` — use HTTP services\n- ❌ `node:cluster` — use external process manager\n- ❌ `require()` (ESM only)\n- ❌ `__dirname`, `__filename` (use `import.meta.url`)\n- ❌ Tree-sitter native bindings (use Shiki)\n- ❌ Native `argon2` (use bcryptjs)\n\n### CI matrix\n\n```yaml\nstrategy:\n  matrix:\n    runtime: [perry, bun-1.2, node-22, node-24]\n```\n\nSame test suite runs against all four. PRs need green on all to merge.\n\n---\n\n## Feature scope\n\n### Tier 1 — ships in v0.1 (the curated set)\n\n**Content & publishing**\n\n- Built-in types: `Page`, `Post`, `Media`, `User`, `Role`, `Menu`, `Setting`, `Form`, `FormSubmission`, `EmailTemplate`, `Redirect`\n- Custom content types (ACF-style field schema)\n- Drafts + scheduled publish\n- Preview URLs (signed-token)\n- Revision history with one-click rollback\n- Bulk actions (status change, delete)\n- TipTap rich text (no raw HTML pasting)\n\n**Forms & email**\n\n- Built-in forms pre-seeded: Contact, Newsletter signup, Quote request\n- 11 fixed field types: `text, email, phone, textarea, checkbox, radio, select, multiselect, file, hidden, consent`\n- Submissions persist regardless of email success\n- Spam protection: honeypot + timing check + per-IP rate limit\n- Async email via SMTP \u002F Resend \u002F Postmark \u002F SES\n- Editable email templates with variable interpolation + i18n\n\n**SEO & agent**\n\n- Mandatory `metaDescription` and image `altText` at publish\n- Auto sitemap, robots, llms.txt, RSS\u002FAtom (via site-kit helpers)\n- Auto schema.org JSON-LD per content type (via site-kit)\n- OpenGraph + Twitter Card meta\n- 301\u002F302 redirect management\n- Canonical + hreflang for i18n\n\n**Admin**\n\n- HTMX-based admin UI (server-rendered, no SPA build)\n- First-run wizard (admin user, site name, locale, branding)\n- 2FA (TOTP)\n- Brute-force protection \u002F rate limiting\n- Password reset via email\n- Activity log \u002F audit trail\n- Maintenance mode toggle\n\n**Media**\n\n- Upload + organize (alt text required, focal point)\n- imgproxy-backed transforms with signed URLs\n- oEmbed for YouTube\u002FVimeo\u002FTwitter (cached at publish)\n\n**Operations**\n\n- CLI (`skelpo-cms user create`, `export`, `import`, `migrate`, `backup`, `restore`)\n- `\u002Fhealthz`, `\u002Freadyz`, `\u002Fmetrics` (Prometheus)\n- Structured JSON logs\n- Single-file backup + restore\n- Custom 404\u002F500 pages (content type)\n\n**Performance & cache**\n\n- In-memory cache with dependency graph\n- Brotli pre-compression\n- Surrogate-Key headers for CDN integration\n- Static export mode (`skelpo-cms export --out dist\u002F`)\n\n**i18n**\n\n- Row-per-locale model with `translationGroupId`\n- Per-locale slugs (`\u002Fde\u002Fueber-uns`, `\u002Fen\u002Fabout-us`)\n- Default-locale fallback\n- Admin UI translated via Crowdin\n\n**Search**\n\n- Site-wide MySQL FTS indexed at publish\n\n**Analytics**\n\n- Server-side pageview tracking, no client JS\n- Admin dashboard: top pages, referrers, timeseries\n- GDPR-safe by design\n\n**Webhooks**\n\n- Outbound webhooks with HMAC signing\n- Configurable events: `content.published`, `content.updated`, `menu.updated`, `setting.changed`, `form.submitted`, etc.\n- Delivery audit log with retry\n\n**SDK**\n\n- `@skelpo\u002Fcms-client` — typed REST client with auto-cache + webhook handler\n- `@skelpo\u002Fsite-kit` — opt-in SEO helpers (Head, JsonLd, Image, Form, Sitemap, Robots, Llms, Feed)\n- `skelpo-cms types-codegen` — emits typed bindings from current schema\n\n### Tier 2 — v0.2+\n\n- Newsletter campaigns (compose + send to list)\n- Per-post comments (opt-in per content type)\n- Auto OG image generation (server-side composition)\n- Search backend swap (Meilisearch \u002F Tantivy)\n- Multi-step forms\n- Per-content access control (member-only pages)\n- IndieAuth \u002F WebMentions\n- Calendar\u002Fevents as first-class type\n- Block-based page builder\n\n### Tier 3 — hard no, out of scope\n\n- E-commerce (use Medusa)\n- Memberships \u002F paid subscriptions\n- Forums, LMS, wiki, CRM\n- Plugins with arbitrary code execution (use webhooks)\n- Custom themes (frontend is the theme)\n- Headless-only mode (it already is headless)\n- Multi-tenant SaaS mode\n\n---\n\n## v0.1 deliverables\n\nConcrete list, in build order:\n\n1. **Scaffold** the `skelpo\u002Fcms` repo: `package.json`, `tsconfig.json`, `perry.config.json`, `.gitignore`, CI workflow.\n2. **Schema migration runner** + first migration (the full schema from `docs\u002Fschema.md`).\n3. **Boot loop**: Hono app, MySQL pool, `\u002Fhealthz`, `\u002Freadyz`, structured logs.\n4. **Auth**: sessions + tokens, bcryptjs password hashing, login\u002Flogout\u002Fme, brute-force rate limit.\n5. **Content read API**: `GET \u002Fcontent`, `by-id`, `by-slug`, `by-path` with `include` expansion.\n6. **Content write API**: POST\u002FPATCH\u002FDELETE\u002Fpublish\u002Fschedule\u002Frevert.\n7. **Content types API**: CRUD + schema revisions + lazy migration.\n8. **Cache layer**: in-memory LRU + dep graph + ETag + Surrogate-Key emission.\n9. **Media**: upload, imgproxy URL signing, alt text enforcement.\n10. **Menus, Settings, Redirects, Roles, Users**.\n11. **Forms** + form submissions + email backends (start with Resend).\n12. **Jobs queue** (DB-backed polling worker).\n13. **Webhooks** outbound + HMAC signing + delivery log.\n14. **Admin UI** (HTMX) for: login, dashboard, content list\u002Fedit, types, menus, settings, users, forms, media, redirects, jobs, audit.\n15. **First-run wizard**.\n16. **`@skelpo\u002Fcms-client`** SDK + auto-cache + webhook handler + types-codegen.\n17. **`@skelpo\u002Fsite-kit`** Head, JsonLd, Image, Form, Sitemap, Robots, Llms, Feed.\n18. **`skelpo-cms init`** CLI → generates starter site repo.\n19. **Static export mode** (`skelpo-cms export`).\n20. **Analytics ingest + dashboard**.\n21. **CLI**: backup, restore, user-create, migrate, export, import.\n22. **CI matrix**: Perry + Node + Bun.\n23. **Three distribution artifacts**: Perry binary, Docker image, npm package.\n24. **perry.land** built on Skelpo CMS as proof-of-concept.\n\n---\n\n## Repository layout\n\nThe planned source tree (subject to refinement as code lands):\n\n```\nskelpo-cms\u002F\n├── README.md                  ← this file\n├── docs\u002F\n│   ├── api-spec.md            ← REST API contract (v1)\n│   ├── schema.md              ← full SQL schema\n│   ├── architecture.md        ← deeper architecture notes\n│   └── ops.md                 ← deployment \u002F backup \u002F restore\n├── migrations\u002F                ← *.sql files, applied in order\n├── src\u002F\n│   ├── server.ts              ← entry, runtime detection\n│   ├── config.ts              ← env → typed config\n│   ├── app.ts                 ← Hono root app\n│   ├── routes\u002F\n│   │   ├── api\u002F\n│   │   │   ├── content.ts | types.ts | media.ts | users.ts | roles.ts\n│   │   │   ├── menus.ts | settings.ts | forms.ts | redirects.ts\n│   │   │   ├── webhooks.ts | search.ts | analytics.ts | auth.ts\n│   │   │   ├── jobs.ts | audit.ts | schema.ts\n│   │   ├── admin\u002F\n│   │   │   ├── routes.ts\n│   │   │   └── views\u002F         ← JSX server-rendered fragments\n│   │   ├── healthz.ts | metrics.ts | preview.ts\n│   ├── db\u002F\n│   │   ├── client.ts          ← @perryts\u002Fmysql pool\n│   │   ├── content.ts | users.ts | roles.ts | media.ts | jobs.ts | …\n│   │   └── migrate.ts\n│   ├── cache\u002F\n│   │   ├── lru.ts | deps.ts | invalidate.ts | persist.ts\n│   ├── render\u002F\n│   │   ├── richtext.tsx       ← TipTap JSON → HTML\n│   │   ├── highlight.ts       ← Shiki at publish time\n│   ├── auth\u002F\n│   │   ├── session.ts | totp.ts | password.ts | ratelimit.ts | tokens.ts\n│   ├── permissions\u002Fcheck.ts\n│   ├── forms\u002F\n│   ├── email\u002F\n│   │   ├── adapter.ts | smtp.ts | resend.ts | postmark.ts | ses.ts\n│   ├── jobs\u002F\n│   │   ├── queue.ts | worker.ts | kinds\u002F\n│   ├── media\u002F\n│   │   ├── upload.ts | imgproxy.ts | storage\u002Flocal.ts | storage\u002Fs3.ts\n│   ├── search\u002F\n│   ├── analytics\u002F\n│   ├── webhooks\u002F\n│   ├── cli\u002F\n│   │   └── main.ts            ← `skelpo-cms \u003Csubcommand>`\n│   └── platform\u002F              ← runtime-specific shims\n│       ├── serve.ts | assets.ts | worker.ts\n├── packages\u002F\n│   ├── cms-client\u002F            ← @skelpo\u002Fcms-client (SDK)\n│   └── site-kit\u002F              ← @skelpo\u002Fsite-kit (helpers)\n├── starter\u002F                   ← `skelpo-cms init` copies this\n├── tests\u002F\n├── docker\u002F\n│   └── Dockerfile\n├── .github\u002Fworkflows\u002F\n├── package.json\n├── tsconfig.json\n└── perry.config.json\n```\n\n---\n\n## Testing\n\n`node:test` via `tsx` — zero extra test deps, runs on Node\u002FBun\u002FPerry.\n\n```bash\nnpm run test:unit          # pure logic, no DB — runs anywhere (47 tests)\nnpm run test:integration   # full API + admin UI vs a MySQL test DB (34 tests)\nnpm test                   # both — 81 total\n```\n\n- **Unit** (`tests\u002Funit\u002F`): permissions, cache (LRU + dep-graph + ETag),\n  datetime normalization, password hashing, content-writer validation,\n  all of `@skelpo\u002Fsite-kit`, and the `@skelpo\u002Fcms-client` cache\u002Fclient.\n- **Integration** (`tests\u002Fintegration\u002F`): `api.test.ts` — auth\u002Fratelimit,\n  content CRUD + publish + SEO-gate + cache + 304, schema evolution,\n  menus\u002Fsettings\u002Fredirects, users\u002Froles, form spam, media\n  alt-enforcement, webhook dispatch, full backup→wipe→restore\n  FK-integrity round-trip. `admin.test.ts` — the HTMX admin: auth gate,\n  login\u002Flogout, dashboard, content editor (create\u002Fpublish\u002FSEO-gate\u002F\n  delete), and every secondary screen incl. their form posts. Each file\n  resets the DB + boots a server; run serially. Auto-skips when no MySQL.\n  (The perry-landing scripts are thin glue over the SDK + site-kit, both\n  exhaustively covered by the suites above.)\n- CI: `.github\u002Fworkflows\u002Ftest.yml` — Node 22 & 24, MySQL 8 service,\n  typecheck (all 3 packages) + unit + integration.\n\nThe suite has already caught and fixed three real bugs: an `updateOwn`\nauthorization bypass, a `TRUNCATE`-on-FK-referenced-table restore failure,\nand an empty-JSON-string restore crash.\n\n---\n\n## References\n\n- **`docs\u002Fapi-spec.md`** — REST API specification (v1)\n- **`docs\u002Fschema.md`** — full SQL schema (to be written next)\n- [Perry](https:\u002F\u002Fgithub.com\u002FPerryTS\u002Fperry) — the native TypeScript compiler this is designed for\n- [Hono](https:\u002F\u002Fhono.dev) — the HTTP framework\n- [@perryts\u002Fmysql](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002F@perryts\u002Fmysql) — the MySQL driver (pure-TS wire protocol)\n- [TipTap](https:\u002F\u002Ftiptap.dev) — the rich text editor\n- [HTMX](https:\u002F\u002Fhtmx.org) — the admin UI mechanism\n- [imgproxy](https:\u002F\u002Fimgproxy.net) — image transforms sidecar\n- [Shiki](https:\u002F\u002Fshiki.style) — syntax highlighting\n\n---\n\n**Next step:** approve this plan, then start scaffolding the `skelpo-cms` package + first migration.\n","Skelpo CMS 是一个专为代理机构和小型企业设计的高性能、基于 TypeScript 的内容管理系统。其核心功能包括通过 REST API 提供无头 CMS 服务，支持 Perry AOT 编译以实现亚毫秒级响应时间，并且能够运行在 Node 和 Bun 等多运行时环境中。技术上采用 HTMX 构建管理界面，MySQL 作为后端数据库，确保了系统的简洁性和速度。适用于构建如公司官网、营销网站或文档门户等非电商平台类网站。此外，该系统提供独立部署选项，保证每个站点的数据隔离性，适合追求高性能与灵活性的小规模项目使用。","2026-06-11 04:05:41","CREATED_QUERY"]