WikifitaGitHub live67e8de5
outro · wikifita-site-architecture/wikifita-site-deployment

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.

Baixar raw

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

LayerTechnologyVersionRole
UI FrameworkNext.js (App Router)16.2.6File-based routing, Server Components, metadata API
Build SystemVinext0.0.50Adapts Next.js App Router to Vite
BundlerVite8.0.13Module bundling, asset optimization, HMR (dev)
Worker Plugin@cloudflare/vite-plugin1.37.1Integrates Cloudflare Workers into Vite build
Runtime BundlerWrangler4.92.0Cloudflare Workers CLI for deploy/preview
ReactReact19.xServer Components, Suspense, concurrent features
LanguageTypeScript5.9.3Type safety across the entire codebase
Node RequirementNode.js>=22.13.0Required for modern APIs (crypto, TextEncoder)
CSSTailwind CSS4.2.1Utility-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:

EnvironmentRoleDescription
rsc (primary)React Server ComponentsThe main rendering environment. Server Components run here.
ssr (child)Server-Side RenderingTraditional 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 (.svg files 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)
  • path and path/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

OperationLatencyNotes
Cache hit (same SHA)1-3msCorpus lookup only
Cache miss (new SHA)100-500msFull GitHub API + build
Markdown render3-10msPer page
Total (cache hit)5-15msFull SSR response
Total (cache miss)150-600msFirst 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:

HeaderExampleDescription
X-Wikifita-SourcegithubData source used
X-Wikifita-Source-StateliveWhether data is live, cached, or degraded
X-Wikifita-Commitabc123defGit commit SHA
X-Wikifita-Loaded-At2026-07-21T10:00:00ZWhen corpus was built

Client-Side Architecture

The AtlasClient.tsx component is a single React component that handles all three page modes:

Mode Detection

ModeRouteLayout
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:

ForceParametersGlobal ModeBackground Mode
forceLinkdistance: 72, strength: 0.16YesYes
forceManyBodystrength-92-56
forceCollideradius: nodeRadius + 10, strength: 0.8YesYes
forceXper-community X targetsYesYes
forceYcenter Y, strength: 0.04YesYes
forceCentercanvas centerYesYes
Simulation ticks240180

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:

CategoryBase ColorVisual
memoryWarm amberKnowledge and memory pages
projectBlue-grayInfrastructure and tools
researchTeal-greenResearch and investigations
directivePurpleRules and guidelines
personPinkPeople and contacts
otherNeutral grayUncategorized

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:

FontCSS VariableUsage
Inter--font-interBody text, UI elements, navigation
JetBrains Mono--font-monoCode blocks, metadata, tags, paths
Source Serif 4--font-serifArticle headings, article body text

Responsive Breakpoints

BreakpointLayoutDescription
>= 1280pxTwo-column with sticky sidebarFull desktop: article + context rail
>= 1180pxTwo-column document layoutMedium desktop: article + narrower sidebar
<= 840pxSingle column, nav hiddenTablet: article only, sidebar collapsed
<= 480pxCompact mobile layoutPhone: 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:

  1. Downloads fonts at build time (no runtime Google Fonts requests)
  2. Self-hosts them from the Cloudflare Worker's static assets
  3. Injects CSS variables via className on 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:

  1. Compiles TypeScript to JavaScript bundles
  2. Processes CSS through Tailwind CSS + PostCSS
  3. Outputs optimized assets to the dist/ directory
  4. The ASSETS binding 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_compat compatibility flag
  • ASSETS binding for static files
  • IMAGES binding for image optimization
  • Environment variables for GitHub token, source configuration

Environment Variables

VariableDefaultDescription
SITE_URLhttps://wikifita.aleffita.devCanonical site URL
WIKIFITA_REPOSITORYaleffita/wikifitaGitHub repo
WIKIFITA_BRANCHmainBranch to track
WIKIFITA_GITHUB_TOKENGitHub PAT (secret)
WIKIFITA_SOURCEgithubgithub or snapshot
WIKIFITA_FALLBACKsnapshotFallback on failure

Testing Strategy

Test TypeRunnerScope
Unittsx --test tests/wiki-pipeline.test.tsFrontmatter, links, search, PageRank, rendering
Integrationnode --test tests/rendered-html.test.mjsFull build + HTTP request to rendered pages
UI QAnode tests/ui-qa.mjsPlaywright + axe-core accessibility checks
Secret scannode scripts/secret-scan.mjsDetects 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

  1. No static generation: Every request triggers SSR. This is acceptable for the current traffic level but would need optimization at scale (edge caching, ISR).

  2. 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.

  3. 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).

  4. 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.

  5. 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