DEV Community

Cover image for How We Moved Our Docs to the Edge (And Stopped Rebuilding for Typos)

How We Moved Our Docs to the Edge (And Stopped Rebuilding for Typos)

At Tenders SA, we recently undertook a migration that sounds minor on paper but had a massive impact on our developer experience and site performance: we moved our entire documentation system from local file reads to the Cloudflare Edge.

Here is why we did it, how we architected it using Cloudflare Workers and KV, and the code that makes it work.

The Problem: The "Monolithic" Content Trap

Like many Next.js projects, our documentation started as a folder of JSON files living in our repo (data/docs/*.json).

Our app would read these files at runtime (or build time) using standard file system calls:

// The old way (src/lib/docs/load-documentation.ts)
import fs from 'fs';
import path from 'path';

export function getDoc(slug) {
  // ๐Ÿšซ This couples content to the build artifact!
  const filePath = path.join(process.cwd(), 'data/docs', `${slug}.json`);
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
Enter fullscreen mode Exit fullscreen mode

The issues with this approach:

  1. Rebuilds for Typos: Correcting a spelling mistake in the docs required a full CI/CD pipeline run and redeployment of the main application.
  2. Platform Limitations: As we looked to move certain services to edge runtimes (like Cloudflare Workers), we hit a wall. Edge workers generally cannot read from the local file system.

The Solution: Cloudflare KV + Workers

We decided to decouple the content from the application code.

  • Storage: Cloudflare KV (Key-Value storage). It is eventually consistent, extremely fast for reads, and perfect for text data like documentation.
  • Delivery: A lightweight Cloudflare Worker acting as the docs-api.

The Architecture

Instead of fs.readFile, our frontend now fetches data from an API.

  • Namespace: TENDERS_DOCS
  • Key Strategy:
    • doc:{slug} -> Stores the full content of a specific page.
    • docs:tree -> Stores the navigation structure (sidebar categories, order, etc.).

Step 1: The Migration Script (Seeding KV)

We couldn't just drag-and-drop files. We needed a script to transform our local JSON files into a format Cloudflare KV accepts for bulk uploading.

We created scripts/generate-docs-kv-data.ts:

// scripts/generate-docs-kv-data.ts
import fs from 'fs';
import path from 'path';

const DOCS_DIR = path.join(__dirname, '../data/docs');
const OUTPUT_FILE = path.join(__dirname, 'docs_kv_bulk.json');

const generateKVData = () => {
  const files = fs.readdirSync(DOCS_DIR);
  const kvPairs = [];
  const navigationTree = [];

  files.forEach(file => {
    if (!file.endsWith('.json')) return;

    const content = fs.readFileSync(path.join(DOCS_DIR, file), 'utf8');
    const docData = JSON.parse(content);
    const slug = file.replace('.json', '');

    // 1. Prepare the content key
    kvPairs.push({
      key: `doc:${slug}`,
      value: JSON.stringify(docData) // Store as string
    });

    // 2. Build the navigation tree metadata (simplified)
    navigationTree.push({
      title: docData.title,
      slug: slug,
      category: docData.category
    });
  });

  // 3. Add the tree key
  kvPairs.push({
    key: `docs:tree`,
    value: JSON.stringify(navigationTree)
  });

  fs.writeFileSync(OUTPUT_FILE, JSON.stringify(kvPairs, null, 2));
  console.log(`โœ… Generated ${kvPairs.length} KV pairs.`);
};

generateKVData();
Enter fullscreen mode Exit fullscreen mode

Once generated, we upload the data via Wrangler:

npx wrangler kv:bulk put --namespace-id <YOUR_KV_ID> scripts/docs_kv_bulk.json
Enter fullscreen mode Exit fullscreen mode

Step 2: The Worker Logic

The Worker is incredibly simple. It sits on the edge, intercepts the request, and queries KV.

// worker.ts
export interface Env {
  TENDERS_DOCS: KVNamespace;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    // 1. Handle Navigation Request
    if (url.pathname === '/docs/tree') {
      const tree = await env.TENDERS_DOCS.get('docs:tree', 'json');
      return new Response(JSON.stringify(tree), {
        headers: { 'Content-Type': 'application/json' }
      });
    }

    // 2. Handle Individual Pages
    // Pattern: /docs/{slug}
    const match = url.pathname.match(/^\/docs\/(.+)$/);
    if (match) {
      const slug = match[1];
      // Fetch directly from KV
      const doc = await env.TENDERS_DOCS.get(`doc:${slug}`, 'json');

      if (!doc) {
        return new Response('Doc not found', { status: 404 });
      }

      return new Response(JSON.stringify(doc), {
        headers: { 
          'Content-Type': 'application/json',
          'Cache-Control': 'public, max-age=3600' // Cache heavily!
        }
      });
    }

    return new Response('Not found', { status: 404 });
  },
};
Enter fullscreen mode Exit fullscreen mode

Step 3: Decoupling the Frontend

Finally, we refactored our Next.js frontend components. Instead of importing a server-side file reader, we just fetch.

Before (Server Component):

const doc = await loadDocumentationData(slug); // โŒ Reads disk
Enter fullscreen mode Exit fullscreen mode

After (Server Component):

const res = await fetch(`https://docs-api.tenders-sa.org/docs/${slug}`, {
  next: { revalidate: 3600 } 
});
const doc = await res.json(); // โœ… Fetches from Edge
Enter fullscreen mode Exit fullscreen mode

The Benefits

  1. Instant Content Updates: We can now update a specific KV key (via API or dashboard) and the documentation updates instantly without a redeploy.
  2. Performance: Cloudflare KV is distributed globally. Our documentation data is now physically closer to the user than our origin server ever was.
  3. Microservices Ready: Our documentation data is no longer trapped in the Next.js repo. Other services (like our chatbots or CLI tools) can hit the docs-api to retrieve the same standardized content.

Have you moved static content to the Edge? Let me know in the comments!

Tenders SA Documentations

Top comments (0)