Docs-Site Platform
Parallel evaluation of VitePress and Next.js as documentation site renderers ā same content, two implementations
Docs-Site Platform
A demonstration and evaluation platform that runs two documentation site implementations ā VitePress and Next.js ā side-by-side, both rendering the exact same content from a single canonical source.
The goal: compare documentation tooling in practice before committing to one long-term approach for the Registry Platform.
What It Is
The Docs-Site Platform is not a production split between "different sites for different audiences". It is an engineering experiment with a clear question:
Which documentation site technology best fits our workflow and content structure?
Two sites run in parallel, fed by the same Markdown files, so differences in rendering, navigation, search, and developer experience are directly observable under identical content conditions.
| Site | Technology | URL |
|---|---|---|
| Site A | VitePress | vitepress.docs.registry.hochguertel.work |
| Site B | Next.js | nextjs.docs.registry.hochguertel.work |
Both sites display the same pages. Neither is "for a specific audience". The distinction is purely the rendering technology under evaluation.
How It Works
Content is written once in docs/docs-site/. The docs-sync.py tool reads a
configuration file (docs-sync.yml) and generates service-specific content roots
under .service-specific/. Each docs-site container reads from its own subdirectory:
flowchart LR
canonical["docs/docs-site/\n(canonical .md files)"]
config["docs-sync.yml\n(per-service rules)"]
script["docs-sync.py\n(sync tool)"]
vp[".service-specific/vitepress/\n(symlinks ā .md files)"]
nx[".service-specific/nextjs/\n(symlinks ā .md as .mdx)"]
canonical --> script
config --> script
script --> vp
script --> nx
vp --> VitePress["VitePress container\n(registry-docs-vitepress)"]
nx --> NextJS["Next.js container\n(registry-docs-nextjs)"]
In most cases docs-sync.py creates symlinks ā the canonical file is read directly,
with zero duplication. A real copy is only written when frontmatter must be transformed
(e.g. adding the title field that Next.js requires but is optional in VitePress):
graph TD
canonical["canonical file\nservices/npm/developer.md"]
subgraph decision ["docs-sync.py decision"]
check{"frontmatter\ntransforms\nneeded?"}
end
symlink["symlink\n.service-specific/vitepress/ā¦/developer.md\nā canonical file"]
realfile["real file\n.service-specific/nextjs/ā¦/developer.mdx\nwith added frontmatter"]
canonical --> check
check -- "No change needed" --> symlink
check -- "title/description\nmust be injected" --> realfile
When canonical files gain proper frontmatter the tool automatically switches back to
symlinks on the next sync run.
Adding a New Docs-Site Implementation
Because the sync layer is fully configuration-driven, adding a third implementation (Docusaurus, Astro, Starlight, ā¦) requires only:
- Add a new
servicesentry todocs-sync.ymlwith the desiredoutput_dir,includepatterns, and anyfrontmatter_transforms. - Run
task syncto generate the new content root. - Create the container service (Dockerfile + compose file) pointing at the new
.service-specific/<name>/directory.
No changes to canonical docs are needed.
Key Components
Canonical Source (docs/docs-site/services/)
Authoritative Markdown files. Written and edited by humans, never by tooling.
Each service has its own subdirectory; content under services/registries/ contains
cross-cutting guides (quickstart, authentication, architecture, troubleshooting).
.service-specific/ (generated)
Per-service content roots built by docs-sync.py. Never edit by hand ā fully
regenerated on every task sync run.
docs-sync.py
The sync engine at scripts/docs-sync.py. A self-contained
PEP 723 script:
| Command | Purpose |
|---|---|
sync | Build/rebuild .service-specific/ from canonical sources |
generate | Generate site-specific files (homepage, config) from site-config.yml |
build | generate + sync in one pass |
status | Show per-service sync state table |
check | Validate all symlinks resolve (CI-friendly, exits 1 on failure) |
docs-sync.yml
Drives the sync engine. Defines for each service: which files to include/exclude,
whether to rename extensions (.md ā .mdx), and what frontmatter fields to
add, override, or strip.
site-config.yml
Central metadata (site name, domain, registry URLs). The generate command reads
this and produces the VitePress homepage (index.md) and the Next.js runtime
config.yaml automatically.
Directory Layout
docs/docs-site/
ā
āāā docs-sync.yml ā sync configuration
āāā site-config.yml ā site metadata (used by `generate`)
ā
āāā services/
āāā index.md ā services landing page
āāā registries/ ā cross-cutting registry guides
ā āāā index.md
ā āāā guides/
ā āāā architecture.md
ā āāā authentication.md
ā āāā quickstart.md
ā āāā troubleshooting.md
āāā npm/ ā npm Registry (Verdaccio) docs
āāā docker/ ā Docker Registry docs
āāā maven/ ā Maven (Reposilite) docs
āāā pypi/ ā PyPI (DevPI) docs
āāā docs-site-platform/ ā this documentation (you are here)
āāā index.md
āāā developer.md ā contributing to docs
āāā devops.md ā operating the containers
āāā technical-writer.md
āāā guides/
āāā content-structure.md
.service-specific/ ā GENERATED ā never edit
āāā vitepress/services/ ā symlinks for VitePress srcDir
āāā nextjs/services/ ā .mdx symlinks for Next.js CONTENT_DIR
Quick Start ā Adding Content
::: tip New doc in 4 steps
-
Create your
.mdfile in the appropriate canonical directory:# Registry-specific page: touch docs/docs-site/services/npm/my-topic.md # Cross-cutting guide: touch docs/docs-site/services/registries/guides/my-guide.md -
Write content with at least
titleanddescriptionin the frontmatter:--- title: My Topic description: What this covers --- # My Topic -
Sync to generate service-specific entries:
task sync -
Commit the canonical file and the regenerated
.service-specific/entries:git add docs/docs-site/ git commit -m "docs: add my-topic":::
Content changes are picked up at runtime ā no container rebuild required.
The Two Rendered Sites
Both sites serve the same content from the same canonical source. The only differences are in how they render and present it.
VitePress
- Static site generator: builds all pages at container startup
- Sidebar navigation configured in
docs-site-vitepress/.vitepress/config.mts - Container:
registry-docs-vitepress
Next.js
- App Router with dynamic MDX loading: renders pages at request time
- Sidebar and navigation generated from the
services/directory structure - Container:
registry-docs-nextjs
The docs-sync.yml include patterns and extension_override handle the only
technical difference between the two content roots:
services:
vitepress:
include: ["**/*.md"] # all canonical Markdown files
nextjs:
include: ["services/**/*.md"]
extension_override: ".mdx" # Next.js MDX loader requires .mdx extension
Further Reading
| Topic | Document |
|---|---|
| Contributing and editing docs | Developer Guide |
| Operating and maintaining services | DevOps Guide |
| Content conventions and style | Technical Writer Guide |
| Directory structure reference | Content Structure Guide |