Blog & Journal: SSR Content Architecture
How ArgoBox serves 200+ blog posts and journal entries without hitting Cloudflare Worker limits — on-demand Gitea fetching, build-time metadata indexes, and marked rendering.
Blog & Journal: SSR Content Architecture
ArgoBox serves 200+ blog posts and journal entries through a hybrid SSR architecture that keeps the Cloudflare Worker bundle small while scaling to thousands of entries.
The Problem
Astro 5's content collections bundle ALL registered collection data into the SSR Worker when any route imports from astro:content. With 200+ markdown files, the data layer grows by ~100 KB+ per collection — quickly exceeding Cloudflare's 3 MiB compressed Worker limit.
The Solution
Instead of registering blog and journal collections in Astro's config.ts, ArgoBox uses a three-layer pattern:
Layer 1: Build-Time Metadata Index
scripts/build-content-index.mjs scans content directories at build time and generates src/data/content-index.json — a metadata-only index containing title, date, tags, description, and slug for each entry. At ~600 bytes per entry, 3000 posts would only be ~1.8 MB.
Layer 2: SSR Route Pages
Four SSR pages handle all content rendering:
| Route | Page | Layout |
|---|---|---|
/blog/ |
Blog listing with tag filtering + Tendril knowledge graph | CosmicLayout |
/posts/[slug]/ |
Individual blog post with square hero image | BaseLayout |
/journal/ |
Journal listing with pstree terminal visualization | CosmicLayout |
/journal/[slug]/ |
Journal entry with prev/next navigation | CosmicLayout |
All pages use export const prerender = false — they're rendered on each request, not at build time.
Layer 3: Runtime Content Fetching
When a visitor loads a blog post or journal entry:
getCollectionMeta()reads the build-time metadata index (synchronous, ~0ms)readContentFile()fetches the full markdown body from Gitea API (production) or local filesystem (dev)markedconverts the markdown to HTML at request time
First load takes ~200-500ms (Gitea API round-trip), then Cloudflare caches the response.
Key Files
| File | Purpose |
|---|---|
src/lib/content-api.ts |
getCollectionMeta() — synchronous metadata from build-time index |
src/lib/content-backend.ts |
readContentFile() — Gitea API fetch with local filesystem fallback |
scripts/build-content-index.mjs |
Generates src/data/content-index.json at build time |
src/content/config.ts |
Journal and posts are explicitly NOT registered here |
Why Not astro:content?
Registering a collection in src/content/config.ts means Astro pre-processes all files and bundles them into _astro_data-layer-content_*.mjs. If even one SSR route imports getCollection(), the entire data layer ships in the Worker. For 200+ markdown files, this adds 100 KB+ to the Worker — and the pre-existing data layer was already 5.2 MB.
By keeping journal and posts out of config.ts, the Worker only grows by ~20 KB (the route files themselves).
Scaling Math
| Metric | 213 entries (current) | 3,000 entries (projected) |
|---|---|---|
| content-index.json | 127 KB | ~1.8 MB |
| Route files | ~20 KB | ~20 KB (unchanged) |
| Worker bundle impact | ~20 KB | ~20 KB (unchanged) |
| Build time impact | Negligible | Negligible (SSR, not pre-rendered) |
| First page load | ~200-500ms | ~200-500ms (same, per-entry fetch) |
Content Collections
| Collection | Count | Location | Registered in config.ts? |
|---|---|---|---|
posts |
104 | src/content/posts/ |
No |
journal |
97 | src/content/journal/ |
No |
docs |
~100+ | src/content/docs/ |
Yes (small, stable) |
configurations |
~10 | src/content/configurations/ |
Yes (small, stable) |
projects |
~15 | src/content/projects/ |
Yes (small, stable) |
Rule: Only register small, stable collections. Large or growing collections use the SSR + API pattern.
Blog Features
- Tag filtering — client-side JavaScript filters post cards by tag
- Tendril knowledge graph — interactive graph visualization with
obsidian-tagslayout - Square hero images —
aspect-ratio: 1/1withobject-fit: coveron both listing cards and post pages - Responsive grid — 2-column on desktop, single column on mobile
Journal Features
- Terminal-style UI — pstree visualization, colored dots, monospace fonts
- Chronological browsing — entries grouped by year/month in timeline view
- Prev/next navigation — browse between entries without returning to the listing
- Back button — quick return to journal index
- Admin review badges — visible with
?admin=truequery parameter - Mood indicators — FOCUSED, DEBUG, BUILD, TRIUMPHANT, etc.
Adding New Content
Create a markdown file in the appropriate directory:
src/content/journal/YYYY-MM-DD-slug.md
src/content/posts/YYYY-MM-DD-slug.md
The build script automatically picks up new files. No config changes needed. The content index regenerates on every build.