Status: Phase 2a — In Progress
Chosen Approach: Hono JSX + API HTML Endpoint + CF Cache API
After evaluating multiple approaches (React SSR, Astro, raw HTML strings), we chose Hono JSX on Cloudflare Workers as the SSR engine. This leverages the existing CF Worker infrastructure with zero new services.
flowchart LR
Browser -->|"GET org.colbin.com/doc/slug"| CFWorker["CF Worker\n(Hono JSX)"]
CFWorker -->|"Cache HIT"| Browser
CFWorker -->|"Cache MISS"| API["API Server\n/documents/public/:slug/html"]
API -->|"crdtState → Y.Doc → PM JSON → HTML"| CFWorker
CFWorker -->|"Render JSX → Full HTML"| CFCache["CF Cache API\n(s-maxage=300)"]
CFCache --> Browser
Editor["User edits doc\n(Hocuspocus)"] -->|"onStoreDocument"| CachePurge["Cache Purge Service\n(debounced 60s)"]
CachePurge -->|"POST /purge_cache"| CFCacheWhy Hono JSX?
Criteria | Hono JSX | React SSR | Raw Strings |
|---|---|---|---|
Bundle size | ~14KB | ~130KB+ | 0KB |
JSX support | Native | Needs renderToString | None |
CF Worker native | Yes | Needs edge adapter | Yes |
Template composability | Excellent | Excellent | Poor |
Type safety | Full TSX | Full TSX | String only |
Streaming SSR | Built-in | Complex setup | Manual |
Architecture: Island Architecture
flowchart TD
subgraph SSR["Server-Side Rendered (CF Worker)"]
HTML["Full HTML Document\n+ Meta Tags + JSON-LD"]
Content["Article Content\n(ProseMirror → HTML)"]
NavBar["Navigation Bar"]
end
subgraph Client["Client-Side (Lazy)"]
AuthCheck["Auth Detection\n(~500B inline script)"]
EditBtn["'Edit' Button\n(shown if authenticated)"]
Editor["Full Editor Bundle\n(loaded on click)"]
end
SSR --> AuthCheck
AuthCheck -->|"has auth cookie"| EditBtn
EditBtn -->|"user clicks Edit"| EditorMilestone 1: API HTML Rendering Endpoint
1.1 ProseMirror JSON → HTML Serializer
File: apps/api/src/features/document/utils/renderProsemirrorToHtml.util.ts
Pure string-based recursive serializer (~250 lines). Maps all 28 node types and 11 mark types from packages/block-editor/src/core/schema.ts. Zero DOM dependencies — just string concatenation.
Pipeline:
crdtState (Buffer) → Y.applyUpdate(yDoc) → yDocToProsemirrorJSON(yDoc) → renderProsemirrorJsonToHtml(json) → HTML string1.2 HTML Renderer Service
File: apps/api/src/features/document/services/htmlRenderer.service.ts
Load document by slug (existing
documentService.getDocumentBySlug())Check
accessLevels for PUBLIC scopeLoad
crdtState → Y.Doc → ProseMirror JSON → HTMLGenerate description from first 160 chars of plainText
Return
{ html, title, description, type, language, updatedAt, org, document }
1.3 Public API Endpoint
GET /documents/public/:slug/htmlNo auth required — only serves PUBLIC documents (403 otherwise)
Cache headers:
Cache-Control: public, s-maxage=300, stale-while-revalidate=3600
Milestone 2: Worker Migration to Hono JSX
Routing
Route | Handler | Cache |
|---|---|---|
| SSR document page | CF Cache API (300s) |
| SSR org homepage | CF Cache API (300s) |
| Proxy to CF Pages | Unchanged |
| Proxy to CF Pages SPA | Existing bootstrap |
JSX Templates
Layout.tsx — Shared
<html><head><body> wrapperDocumentPage.tsx — Full document page with article content
OrgHomePage.tsx — Org homepage with document list
MetaTags.tsx — OG, Twitter, JSON-LD structured data
SEO Output
<title>Doc Title — Org Name</title>Open Graph meta tags (og:title, og:description, og:type=article, og:url)
Twitter Card meta tags
JSON-LD Article structured data
Inline prose CSS (~8KB) for fastest FCP
Milestone 3: Cache Invalidation
CF Cache Purge (Single File)
sequenceDiagram
participant User as User (Editor)
participant WS as Hocuspocus
participant API as API Server
participant CF as Cloudflare API
participant Cache as CF Edge Cache
User->>WS: Edit document
WS->>API: onStoreDocument (debounced 2s)
API->>API: cachePurgeService.purgeDocument()
Note over API: Debounced: max 1 purge/60s/doc
API->>CF: POST /zones/{id}/purge_cache\n{ files: ["https://org.colbin.com/doc/slug"] }
CF->>Cache: Purge cached response
Note over Cache: Next request = cache MISS → fresh SSRDebounced: max 1 purge per 60s per document (in-memory Map)
Fire-and-forget (non-blocking, log errors)
Triggered from
onStoreDocument hook in Hocuspocus
Milestone 4: Progressive Enhancement (Deferred)
Auth detection via inline script (~500B)
"Edit" button for authenticated users
Dynamic
import() of editor bundle from CF Pages CDNEditor replaces static HTML container (island architecture)
Performance Targets
Metric | Target | How |
|---|---|---|
TTFB | < 200ms | CF Cache hit at edge |
FCP | < 500ms | Inline CSS, no external requests |
SEO Score | 90+ | Full HTML, meta tags, JSON-LD |
Cache freshness | < 60s stale | Purge on edit + stale-while-revalidate |
Reference Articles
Topic | Source |
|---|---|
Hono JSX Guide | hono.dev/docs/guides/jsx |
CF Workers + Hono | developers.cloudflare.com |
CF Cache API | developers.cloudflare.com/workers/runtime-apis/cache |
CF Cache Purge | developers.cloudflare.com/cache/how-to/purge-cache |
CF Instant Purge (<150ms) | blog.cloudflare.com/instant-purge |
Islands Architecture | patterns.dev/vanilla/islands-architecture |
y-prosemirror | github.com/yjs/y-prosemirror |
Mintlify 72M edge-cached pages | mintlify.com/blog/page-speed-improvements |