Deployment Architecture
Cloudflare Workers via Vinext: Vite-based Next.js 16.2.6 on React 19, CF Images binding, force-dynamic rendering, in-memory corpus caching, Anthropic dark mode, and responsive breakpoints.
Deployment Architecture
The Wikifita Atlas deploys to Cloudflare Workers via the Vinext framework, which adapts Next.js 16's App Router to run on Vite's build system and Cloudflare's edge runtime. Every page is server-side rendered on every request -- there is no static generation.
This page documents the full deployment stack, from the Vite configuration to the worker entry point, caching strategy, client-side architecture, and visual design system.
Stack Overview
flowchart TD
A["Developer workstation"] -->|"npm run build"| B["Vite + Vinext"]
B -->|"SSR + RSC bundles"| C["Cloudflare Worker"]
C --> D["Worker Entry (worker/index.ts)"]
D --> E{"/_vinext/image?"}
E -->|yes| F["CF Images Binding"]
E -->|no| G["Next.js App Router"]
G --> H["React 19 Server Components"]
H --> I["lib/wiki.ts (corpus)"]
I --> J["lib/markdown.ts (render)"]
J --> K["HTML Response"]
F --> L["Optimized Image Response"]
Technology Matrix
| Layer | Technology | Version | Role |
|---|---|---|---|
| UI Framework | Next.js (App Router) | 16.2.6 | File-based routing, Server Components, metadata API |
| Build System | Vinext | 0.0.50 | Adapts Next.js App Router to Vite |
| Bundler | Vite | 8.0.13 | Module bundling, asset optimization, HMR (dev) |
| Worker Plugin | @cloudflare/vite-plugin | 1.37.1 | Integrates Cloudflare Workers into Vite build |
| Runtime Bundler | Wrangler | 4.92.0 | Cloudflare Workers CLI for deploy/preview |
| React | React | 19.x | Server Components, Suspense, concurrent features |
| Language | TypeScript | 5.9.3 | Type safety across the entire codebase |
| Node Requirement | Node.js | >=22.13.0 | Required for modern APIs (crypto, TextEncoder) |
| CSS | Tailwind CSS | 4.2.1 | Utility-first styling via @tailwindcss/postcss |
Vite Configuration
The vite.config.ts file configures two plugins:
import { vinext } from 'vinext/vite';
import { cloudflare } from '@cloudflare/vite-plugin';
export default defineConfig({
plugins: [
vinext(), // Next.js App Router on Vite
cloudflare({ // Cloudflare Workers integration
viteEnvironment: {
name: 'rsc', // React Server Components (primary)
childEnvironments: ['ssr'] // Server-Side Rendering (child)
},
config: {
main: './worker/index.ts',
compatibility_flags: ['nodejs_compat']
}
})
]
});
Environment Model
The Vinext/Cloudflare plugin defines two Vite environments:
| Environment | Role | Description |
|---|---|---|
rsc (primary) | React Server Components | The main rendering environment. Server Components run here. |
ssr (child) | Server-Side Rendering | Traditional SSR for client component hydration. Child of rsc. |
This two-environment model enables React 19's server component architecture on the edge. Server Components execute in the rsc environment and can directly access the corpus (in-memory). Client Components are serialized and hydrated in the browser.
Worker Entry Point
The Cloudflare Worker (worker/index.ts) handles two types of requests:
Image Optimization
Requests to /_vinext/image are routed through Cloudflare's Images Binding:
if (url.pathname === '/_vinext/image') {
return handleImageOptimization(request, {
deviceSizes: DEFAULT_DEVICE_SIZES,
imageSizes: DEFAULT_IMAGE_SIZES,
formats: ['avif', 'webp']
});
}
The Images Binding provides:
- On-the-fly resizing based on device width
- Format negotiation (AVIF, WebP, original)
- Quality adjustment
- SVG bypass (
.svgfiles are served directly without transformation)
SSR Dispatch
All other requests pass to the Next.js App Router handler:
return handler.fetch(request, env, ctx);
The handler is the Vinext server entry point (vinext/server/app-router-entry), which invokes the Next.js App Router to resolve the route, execute Server Components, and return the HTML response.
Worker Configuration
# wrangler.toml (generated by Vinext)
compatibility_flags = ["nodejs_compat"]
[[services]]
binding = "ASSETS" # Static asset fetcher
service = "assets"
[services.images]
binding = "IMAGES" # Cloudflare Images transformation
The nodejs_compat flag enables Node.js API polyfills on Cloudflare Workers, including:
crypto(for SHA-256 hashing)pathandpath/posix(for path normalization)TextEncoder/TextDecoder(for UTF-8 byte counting)fs(limited, for reading snapshot files)
Rendering Model: Force-Dynamic
Every page in the Wikifita Atlas is force-dynamic. There is no static generation (generateStaticParams returns nothing useful), no incremental static regeneration, and no build-time rendering.
Why Force-Dynamic
The corpus is loaded live from GitHub on each request. The wiki content can change between deployments -- a push to the aleffita/wikifita repository immediately affects the next page view without redeploying the site.
Request Lifecycle
Every request follows this sequence:
sequenceDiagram
participant B as Browser
participant W as CF Worker
participant R as Next.js Router
participant C as lib/wiki.ts
participant M as lib/markdown.ts
B->>W: GET /wiki/memorias/MEMORY.md
W->>R: App Router dispatch
R->>C: loadWikiCorpus()
alt Cache hit (same SHA)
C->>C: Return cached corpus (<1ms)
else Cache miss (new SHA)
C->>C: Fetch from GitHub API
C->>C: buildCorpus() — 7 phases
C->>C: Cache result
end
C-->>R: WikiCorpus
R->>M: renderMarkdown(page, corpus)
M-->>R: HTML string
R->>R: Serialize WikiClientData
R-->>B: SSR HTML + client data
Performance Characteristics
| Operation | Latency | Notes |
|---|---|---|
| Cache hit (same SHA) | 1-3ms | Corpus lookup only |
| Cache miss (new SHA) | 100-500ms | Full GitHub API + build |
| Markdown render | 3-10ms | Per page |
| Total (cache hit) | 5-15ms | Full SSR response |
| Total (cache miss) | 150-600ms | First request after commit |
Caching Strategy
In-Memory Corpus Caching
The primary caching mechanism is module-level, keyed by commit SHA:
// lib/wiki.ts
let liveCache: { key: string; corpus: WikiCorpus } | null = null;
async function loadGitHubCorpus(): Promise<WikiCorpus> {
const sha = await fetchLatestCommitSha();
// Cache hit
if (liveCache?.key === `${repo}:${branch}:${sha}`) {
return { ...liveCache.corpus, status: { ...status, state: 'cached' } };
}
// Cache miss: full rebuild
const corpus = await buildCorpusFromGitHub(sha);
liveCache = { key: `${repo}:${branch}:${sha}`, corpus };
return { ...corpus, status: { ...status, state: 'live' } };
}
Blob Cache
Individual git blob content is cached in a Map<string, string>:
const blobCache = new Map<string, string>(); // blobSha -> UTF-8 content
This cache persists for the lifetime of the worker process. When a new commit changes only some files, unchanged files are served from the blob cache, reducing GitHub API calls.
HTTP Caching
All responses use cache-control: no-store:
headers.set('cache-control', 'no-store');
headers.set('ETag', computeETag(commit, source, state, variant));
The no-store directive prevents Cloudflare's CDN from caching responses. This is deliberate: the corpus is already cached in-memory per worker instance. CDN caching would introduce staleness issues when the corpus updates.
ETag Computation
function computeETag(commit: string, source: string, state: string, variant: string): string {
return sha256(`${commit}:${source}:${state}:${variant}`).slice(0, 16);
}
The ETag changes when any of: commit SHA, data source, state, or response variant changes. Browsers can use If-None-Match for conditional requests, but the no-store directive means intermediate caches will not store the response.
Response Headers
Every response includes corpus metadata headers:
| Header | Example | Description |
|---|---|---|
X-Wikifita-Source | github | Data source used |
X-Wikifita-Source-State | live | Whether data is live, cached, or degraded |
X-Wikifita-Commit | abc123def | Git commit SHA |
X-Wikifita-Loaded-At | 2026-07-21T10:00:00Z | When corpus was built |
Client-Side Architecture
The AtlasClient.tsx component is a single React component that handles all three page modes:
Mode Detection
| Mode | Route | Layout |
|---|---|---|
home | / | Full-screen graph + centered search box + 3 suggestion cards |
search | /search?q= | Background graph + search column + context rail sidebar |
document | /wiki/<path> | Background graph + article + context rail sidebar |
Component Structure
flowchart TD
A[AtlasClient] --> B{Mode?}
B -->|home| C[GraphCanvas + SearchBox + SuggestionCards]
B -->|search| D[BackgroundGraph + SearchColumn + ContextRail]
B -->|document| E[BackgroundGraph + Article + ContextRail]
C --> F[useGraph hook]
D --> F
E --> F
F --> G["D3-force simulation"]
G --> H["Canvas rendering"]
E --> I[useMermaid hook]
I --> J["mermaid.render() (client-side)"]
Graph Rendering
The D3 force-directed graph is rendered on an HTML5 canvas. The simulation configuration:
| Force | Parameters | Global Mode | Background Mode |
|---|---|---|---|
forceLink | distance: 72, strength: 0.16 | Yes | Yes |
forceManyBody | strength | -92 | -56 |
forceCollide | radius: nodeRadius + 10, strength: 0.8 | Yes | Yes |
forceX | per-community X targets | Yes | Yes |
forceY | center Y, strength: 0.04 | Yes | Yes |
forceCenter | canvas center | Yes | Yes |
| Simulation ticks | 240 | 180 |
The simulation runs for a fixed number of ticks then stops (no continuous animation). Layout positions are cached by {commit}:{variant}:{selectedPath}:{pageCount}:{linkCount}.
Node Color System
Each category has a base color:
| Category | Base Color | Visual |
|---|---|---|
memory | Warm amber | Knowledge and memory pages |
project | Blue-gray | Infrastructure and tools |
research | Teal-green | Research and investigations |
directive | Purple | Rules and guidelines |
person | Pink | People and contacts |
other | Neutral gray | Uncategorized |
Per-page colors are derived by mixing the category base color with a tag-derived accent color. The accent is computed using an FNV-1a hash of the page's first tag, producing deterministic per-tag color variation.
Design System
Color Palette (Anthropic Dark Mode)
The site uses a single dark mode color scheme defined in globals.css:
:root {
/* Backgrounds */
--bg-primary: #171717;
--bg-secondary: #1e1e1e;
--bg-surface: #232329;
/* Text */
--text-primary: #e8e8ed;
--text-secondary: #b8b8c0;
--text-tertiary: #9a9aa3;
/* Accent */
--accent: #fc0d65; /* Pink-red (primary accent) */
/* Semantic */
--warm: #d4a574; /* Warm (amber) */
--success: #66bfa2; /* Green */
--error: #e8706a; /* Red */
}
Typography
Three font families loaded via next/font/google with CSS variable injection:
| Font | CSS Variable | Usage |
|---|---|---|
| Inter | --font-inter | Body text, UI elements, navigation |
| JetBrains Mono | --font-mono | Code blocks, metadata, tags, paths |
| Source Serif 4 | --font-serif | Article headings, article body text |
Responsive Breakpoints
| Breakpoint | Layout | Description |
|---|---|---|
>= 1280px | Two-column with sticky sidebar | Full desktop: article + context rail |
>= 1180px | Two-column document layout | Medium desktop: article + narrower sidebar |
<= 840px | Single column, nav hidden | Tablet: article only, sidebar collapsed |
<= 480px | Compact mobile layout | Phone: reduced padding, smaller fonts |
Responsive Behavior
flowchart LR
A["Desktop >=1280px"] -->|"Article + Sidebar + Graph"| B[Full layout]
C["Tablet 840-1180px"] -->|"Article only"| D[Collapsed sidebar]
E["Mobile <=840px"] -->|"Stacked single column"| F[Compact layout]
G["Phone <=480px"] -->|"Minimal padding"| H[Phone layout]
The graph visualization adapts to screen size:
- Desktop: Full interactive graph with labels and hover effects
- Mobile: Background graph (dimmed, non-interactive) with reduced force simulation
Fonts and Assets
Font Loading
Fonts are loaded via next/font/google which:
- Downloads fonts at build time (no runtime Google Fonts requests)
- Self-hosts them from the Cloudflare Worker's static assets
- Injects CSS variables via
classNameon the root layout element
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });
const jetbrainsMono = JetBrains_Mono({ subsets: ['latin'], variable: '--font-mono' });
const sourceSerif = Source_Serif_4({ subsets: ['latin'], variable: '--font-serif' });
Static Assets
Static assets (CSS, fonts, JS bundles) are served via the Cloudflare ASSETS binding. The Vinext build process:
- Compiles TypeScript to JavaScript bundles
- Processes CSS through Tailwind CSS + PostCSS
- Outputs optimized assets to the
dist/directory - The
ASSETSbinding serves these at/_vinext/static/...
SEO
Dynamic Sitemap
app/sitemap.ts generates a dynamic sitemap from the corpus:
export default async function sitemap() {
const corpus = await loadWikiCorpus();
return corpus.pages.map(page => ({
url: page.canonicalUrl,
lastModified: page.timestamp || corpus.status.commitDate,
}));
}
Robots.txt
app/robots.ts allows all crawlers and references the sitemap:
User-agent: *
Allow: /
Sitemap: https://wikifita.aleffita.dev/sitemap.xml
OpenGraph and Twitter Cards
Per-page metadata is generated by generateMetadata in the page components. Global defaults (site name, description, image) are set in layout.tsx.
Canonical URLs
Every page has alternates.canonical set to the full URL, preventing duplicate content issues.
Health Check
/healthz returns corpus health information:
{
"status": "ok",
"pages": 87,
"links": 342,
"source": "github",
"state": "live",
"commit": "abc123",
"loadedAt": "2026-07-21T10:00:00Z"
}
Returns HTTP 503 if no pages are loaded (corpus failed to initialize).
Build and Deploy Pipeline
# 1. Install dependencies
npm install
# 2. Generate snapshot (pre-build step)
npm run wiki:snapshot
# 3. Build (Vite + Vinext + Cloudflare)
npm run build
# 4. Test
npm run test # secret:scan + unit tests + integration tests
# 5. Deploy
wrangler deploy
Build Output
The Vinext build produces:
- Worker JavaScript bundle (SSR + RSC)
- Static assets (CSS, fonts, JS chunks)
wrangler.toml(Cloudflare Workers configuration)
Deployment Target
Cloudflare Workers with:
nodejs_compatcompatibility flagASSETSbinding for static filesIMAGESbinding for image optimization- Environment variables for GitHub token, source configuration
Environment Variables
| Variable | Default | Description |
|---|---|---|
SITE_URL | https://wikifita.aleffita.dev | Canonical site URL |
WIKIFITA_REPOSITORY | aleffita/wikifita | GitHub repo |
WIKIFITA_BRANCH | main | Branch to track |
WIKIFITA_GITHUB_TOKEN | — | GitHub PAT (secret) |
WIKIFITA_SOURCE | github | github or snapshot |
WIKIFITA_FALLBACK | snapshot | Fallback on failure |
Testing Strategy
| Test Type | Runner | Scope |
|---|---|---|
| Unit | tsx --test tests/wiki-pipeline.test.ts | Frontmatter, links, search, PageRank, rendering |
| Integration | node --test tests/rendered-html.test.mjs | Full build + HTTP request to rendered pages |
| UI QA | node tests/ui-qa.mjs | Playwright + axe-core accessibility checks |
| Secret scan | node scripts/secret-scan.mjs | Detects committed GitHub PATs |
The pipeline runs as: secret:scan -> test:unit -> test:integration. Integration tests require a running wrangler dev instance.
Proxy Middleware
The proxy.ts middleware rewrites per-page LLM endpoint URLs:
/wiki/{path}/llms.txt -> /api/page-llms/llms/{path}
/wiki/{path}/llms-full.txt -> /api/page-llms/llms-full/{path}
This allows LLM endpoints to live under the canonical wiki URL hierarchy without separate route files. See wikifita-site-llm-integration for endpoint details.
Limitations
-
No static generation: Every request triggers SSR. This is acceptable for the current traffic level but would need optimization at scale (edge caching, ISR).
-
In-memory corpus: The entire wiki is held in memory per worker instance. Cloudflare Workers have a 128MB memory limit. The current corpus (50-200 pages) uses <10MB, leaving ample headroom.
-
Single worker region: Cloudflare Workers run in the nearest data center, but the corpus is built per-instance. A request from Tokyo and a request from Sao Paulo build independent corpus instances (unless the same SHA is cached).
-
GitHub API rate limits: The live source depends on the GitHub REST API. The GitHub PAT's rate limit (5000 requests/hour for authenticated requests) is sufficient for the current traffic but could become a bottleneck at scale.
-
No image optimization for external images: The Cloudflare Images Binding only optimizes images served through
/_vinext/image. External images (the only kind, per the wikifita directives) are served directly from their origin URLs.
Related Pages
- wikifita-site-architecture -- Full technical architecture overview
- wikifita-site-pipeline -- The wiki processing pipeline that runs per-request
- wikifita-site-corpus-pipeline -- How the corpus is built and cached
- wikifita-site-rendering-pipeline -- Server-side HTML rendering
- wikifita-site-llm-integration -- LLM endpoint design