|
| 1 | +# Documentation System Architecture |
| 2 | + |
| 3 | +This page describes the technical design of the DevDogs documentation system — how it fetches and caches content, how pages are rendered, how the preview tool works, and how changes propagate from GitHub to the live site. |
| 4 | + |
| 5 | +## Rendering pipeline |
| 6 | + |
| 7 | +All documentation pages use a single shared component: `DocPageContent` (`src/components/DocPageContent/`). It accepts raw markdown source and pre-computed headings, then renders using [Fumadocs UI](https://fumadocs.vercel.app) with the following plugins: |
| 8 | + |
| 9 | +| Plugin | Purpose | |
| 10 | +| --- | --- | |
| 11 | +| `remark-gfm` | GitHub Flavored Markdown (tables, task lists, strikethrough) | |
| 12 | +| `remark-heading` | Heading ID generation for anchor links | |
| 13 | +| `remark-admonition` | `> [!NOTE]` / `> [!WARNING]` / `> [!TIP]` callouts | |
| 14 | +| `remark-code-tab` | Code tab blocks | |
| 15 | +| `rehype-code` | Shiki syntax highlighting with dual light/dark themes | |
| 16 | + |
| 17 | +`DocPageContent` is a pure renderer — it does not parse headings. Heading data (id, title, depth) comes from the cached manifest, ensuring the table of contents, search index, and sidebar tree all derive from the same source. |
| 18 | + |
| 19 | +## No front matter |
| 20 | + |
| 21 | +Markdown files are pure content. There is no YAML front matter. The page title is always the first `# ` heading; sidebar ordering and labels come from `meta.json` files placed alongside the markdown files. |
| 22 | + |
| 23 | +## URL structure |
| 24 | + |
| 25 | +Documentation URLs follow the pattern `/docs/{repo}/{branch}/{slug}`: |
| 26 | + |
| 27 | +``` |
| 28 | +/docs/devdogs-website/main/getting-started |
| 29 | +/docs/devdogs-website/main/contributing |
| 30 | +``` |
| 31 | + |
| 32 | +Only files under the `docs/` directory of a repository are served. Branch names can contain slashes — the routing resolves the branch via longest-prefix matching against known branch names. |
| 33 | + |
| 34 | +## Content delivery and caching |
| 35 | + |
| 36 | +The docs system uses Next.js [Cache Components](https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents) (`"use cache"` directive) for granular, per-file caching. Four independently cached layers compose at read time: |
| 37 | + |
| 38 | +| Layer | Function | Cache tag | TTL | |
| 39 | +| --- | --- | --- | --- | |
| 40 | +| Per-file entries | `getDocFileEntries()` | `docs-search-{repo}-{branch}-{slug}` | `cacheLife("days")` | |
| 41 | +| Per-branch tree | `getDocTreeCached()` | `docs-tree-{repo}-{branch}` | `cacheLife("days")` | |
| 42 | +| Per-repo branches | `getDocBranchesCached()` | `docs-branches-{repo}` | `cacheLife("days")` | |
| 43 | +| Per-repo search index | `getDocsSearchIndex()` | `docs-search-index-{repo}` | `cacheLife("days")` | |
| 44 | + |
| 45 | +All cache tag strings are defined in `src/server/manifest/cache-tags.ts` and shared between `cacheTag()` and `revalidateTag()` call sites — a typo becomes a compile-time error rather than a silent cache miss. |
| 46 | + |
| 47 | +The per-file layer caches each markdown file's parsed headings, title, description, and breadcrumbs. When only one file changes, only that file's cache entry is invalidated — all other files remain cached. |
| 48 | + |
| 49 | +Doc pages themselves use `"use cache"` at the page level, caching the entire rendered RSC payload. See the [caching overview](../caching) for the full picture. |
| 50 | + |
| 51 | +**References:** |
| 52 | +- [Next.js `"use cache"` directive](https://nextjs.org/docs/app/api-reference/directives/use-cache) |
| 53 | +- [Next.js `cacheTag`](https://nextjs.org/docs/app/api-reference/functions/cacheTag) |
| 54 | +- [Next.js `cacheLife`](https://nextjs.org/docs/app/api-reference/functions/cacheLife) |
| 55 | + |
| 56 | +## GitHub webhook (ISR) |
| 57 | + |
| 58 | +When a push is made to a repository, GitHub sends a webhook to `POST /api/github/docs-webhook`. The handler (`src/server/docs/revalidate.ts`): |
| 59 | + |
| 60 | +1. Verifies the `X-Hub-Signature-256` HMAC signature using `GITHUB_WEBHOOK_SECRET`. |
| 61 | +2. Parses the push event to extract the repo name, branch, and changed file paths. |
| 62 | +3. Calls `revalidateTag()` for the affected cache tags. |
| 63 | + |
| 64 | +| Event | Tags invalidated | |
| 65 | +| --- | --- | |
| 66 | +| Any push | `docsBranchesTag`, `docsSearchIndexTag` | |
| 67 | +| File added/removed under `docs/` | `docsTreeTag`, `docsSearchIndexTag` | |
| 68 | +| File modified under `docs/` | `docsFileSearchTag`, `docsSearchIndexTag` | |
| 69 | +| `meta.json` changed | `docsTreeTag`, `docsSearchIndexTag` | |
| 70 | + |
| 71 | +### Registering the webhook |
| 72 | + |
| 73 | +In GitHub → Organization Settings → Webhooks: |
| 74 | + |
| 75 | +- **Payload URL**: `https://devdogsuga.uga.edu/api/github/docs-webhook` |
| 76 | +- **Content type**: `application/json` |
| 77 | +- **Secret**: value of `GITHUB_WEBHOOK_SECRET` in your environment |
| 78 | +- **Events**: **Push** (covers branch pushes and PR merges) |
| 79 | + |
| 80 | +### Environment variable |
| 81 | + |
| 82 | +``` |
| 83 | +GITHUB_WEBHOOK_SECRET="<random string, at least 20 characters>" |
| 84 | +``` |
| 85 | + |
| 86 | +Generate a secret with `openssl rand -hex 32`. |
| 87 | + |
| 88 | +## Code organization |
| 89 | + |
| 90 | +Documentation server code is consolidated in `src/server/docs/`: |
| 91 | + |
| 92 | +| File | Responsibility | |
| 93 | +| --- | --- | |
| 94 | +| `github.ts` | Raw GitHub API calls (branches, tree, file content) | |
| 95 | +| `tree.ts` | Tree processing: meta.json parsing, ordering | |
| 96 | +| `utils.ts` | Shared string utilities (`toTitleCase`, `stripExt`) | |
| 97 | +| `actions.ts` | Server actions for sidebar (branch list, tree nodes, branch resolution) | |
| 98 | +| `revalidate.ts` | Webhook-based cache invalidation | |
| 99 | + |
| 100 | +The `"use cache"` functions live in `src/server/manifest/docs-cache.ts` and compose the functions above. |
| 101 | + |
| 102 | +## Sidebar metadata (`meta.json`) |
| 103 | + |
| 104 | +`buildDocTree()` in `src/server/docs/tree.ts` processes the git tree. From it: |
| 105 | + |
| 106 | +1. Collects all markdown files under `docs/` |
| 107 | +2. Fetches `docs/meta.json` and `docs/**/meta.json` |
| 108 | +3. Applies ordering, label overrides, and visibility filtering |
| 109 | +4. Returns `{ entries, metaByFolder }` |
| 110 | + |
| 111 | +The sidebar component receives `DocTreeNode[]` — a data-source-agnostic tree type shared between the remote docs sidebar and the local preview sidebar. |
| 112 | + |
| 113 | +## Local preview |
| 114 | + |
| 115 | +The preview server (`@devdogsuga/docs-preview`) is a lightweight Node.js HTTP + WebSocket server that: |
| 116 | + |
| 117 | +- Returns a structured `DocTreeNode[]` from `GET /tree` (no client-side tree building needed) |
| 118 | +- Serves raw file content from `GET /file?path=...` |
| 119 | +- Broadcasts `{ type: "change" }` events over WebSocket when any `docs/` file changes |
| 120 | + |
| 121 | +The preview pages (`/tools/docs`) call `fetchFile()` from `@devdogsuga/docs-preview/client`. A client component (`PreviewRefreshClient`) connects the WebSocket and calls `router.refresh()` on each change event. |
0 commit comments