Contributing a Documentation Site
Contributing a Documentation Site
This guide explains how to add a new project to the Circle documentation infrastructure (docs.circle-cyber.com). Each project is deployed as an independent Docker container under its own path (e.g. /my-project/), routed by Traefik, and built automatically by GitHub Actions.
Overview
docs.circle-cyber.com/ ← this portal (circle-rd/documentation)
docs.circle-cyber.com/upki-ca/ ← uPKI CA docs (circle-rd/upki-ca → docs-site/)
docs.circle-cyber.com/upki-ra/ ← uPKI RA docs (circle-rd/upki-ra → docs-site/)
docs.circle-cyber.com/my-proj/ ← your project
Each documentation site is a Docus project (Nuxt 4 layer) that lives inside its own repository under a docs-site/ subdirectory.
Step 1 — Create the docs-site/ directory
Inside your project repository, create the following structure:
docs-site/
├── .gitignore
├── Dockerfile
├── nuxt.config.ts
├── package.json
├── app/
│ └── app.config.ts
├── content/
│ ├── index.md # Landing page
│ └── docs/ # Documentation pages
│ └── getting-started/
│ └── 1.introduction.md
└── server/
└── routes/
└── _health.get.ts
Step 2 — package.json
{
"name": "my-project-docs",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "nuxt dev",
"build": "nuxt build",
"generate": "nuxt generate",
"preview": "nuxt preview"
},
"dependencies": {
"docus": "latest",
"nuxt": "^4.0.0",
"better-sqlite3": "^12.2.0"
}
}
Step 3 — nuxt.config.ts
Replace my-project with your actual path slug:
export default defineNuxtConfig({
extends: ["docus"],
app: {
baseURL: process.env.NUXT_APP_BASE_URL ?? "/my-project/",
},
site: {
url: process.env.NUXT_SITE_URL ?? "https://docs.circle-cyber.com/my-project",
},
robots: {
robotsTxt: false,
},
llms: {
domain: "https://docs.circle-cyber.com",
title: "My Project",
description: "Short description of my project.",
},
})
Step 4 — app/app.config.ts
export default defineAppConfig({
seo: {
title: "My Project",
description: "Short description of my project.",
},
header: {
title: "My Project",
},
socials: {
github: "https://github.com/circle-rd/my-project",
},
github: {
url: "https://github.com/circle-rd/my-project",
branch: "main",
rootDir: "docs-site",
},
})
Step 5 — Dockerfile
# Stage 1 — build
FROM node:24-alpine AS builder
WORKDIR /app
RUN apk add --no-cache python3 make g++
ARG NUXT_APP_BASE_URL=/my-project/
ARG NUXT_SITE_URL=https://docs.circle-cyber.com/my-project
ENV NUXT_APP_BASE_URL=${NUXT_APP_BASE_URL}
ENV NUXT_SITE_URL=${NUXT_SITE_URL}
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npx nuxt build
# Stage 2 — runtime
FROM node:24-alpine
WORKDIR /app
COPY --from=builder /app/.output ./.output
ARG NUXT_APP_BASE_URL=/my-project/
ARG NUXT_SITE_URL=https://docs.circle-cyber.com/my-project
ENV HOST=0.0.0.0
ENV PORT=3000
ENV NODE_ENV=production
ENV NUXT_APP_BASE_URL=${NUXT_APP_BASE_URL}
ENV NUXT_SITE_URL=${NUXT_SITE_URL}
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD wget -qO /dev/null http://127.0.0.1:3000/_health || exit 1
CMD ["node", ".output/server/index.mjs"]
Step 6 — server/routes/_health.get.ts
Required for the Docker healthcheck:
export default defineEventHandler(() => ({ status: "ok" }))
Step 7 — .gitignore
.nuxt/
.output/
.data/
dist/
node_modules/
pnpm-lock.yaml
yarn.lock
.env
.env.*
npm-debug.log*
.DS_Store
.vercel
Step 8 — Write content
Landing page (content/index.md)
Important — MDC syntax rule:
u-page-featurecomponents must use inline props fortitleanddescription. Do not use named slots (#title,#description) inside::::u-page-feature— this ambiguates the MDC parser and causes the entire file to be silently ignored, resulting in a 404.
---
seo:
title: My Project — Tagline
description: Short description.
---
:::u-page-hero
#title
My Project headline
#description
Longer description of the project.
#links
::::u-button{to="/docs/getting-started/introduction" size="xl" trailing-icon="i-lucide-arrow-right" color="neutral"}
Get Started
::::
:::
:::u-page-section
#title
Key Features
#features
::::u-page-feature{icon="i-lucide-shield" title="Feature A" description="Description of feature A."}
::::
::::u-page-feature{icon="i-lucide-zap" title="Feature B" description="Description of feature B."}
::::
:::
Documentation pages (content/docs/)
Files are ordered by numeric prefix (1.introduction.md, 2.installation.md). Docus generates navigation automatically. Use standard Markdown — MDC components are available for callouts, code groups, etc.
Step 9 — GitHub Actions workflow
Create .github/workflows/docs.yml in your repository:
name: Deploy Documentation
on:
push:
branches:
- main
paths:
- "docs-site/**"
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
build-and-push:
name: Build and push docs image
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: docs-site
file: docs-site/Dockerfile
push: true
tags: |
ghcr.io/${{ github.repository_owner }}/my-project-docs:latest
ghcr.io/${{ github.repository_owner }}/my-project-docs:${{ github.sha }}
build-args: |
NUXT_APP_BASE_URL=/my-project/
NUXT_SITE_URL=https://docs.circle-cyber.com/my-project
cache-from: type=gha
cache-to: type=gha,mode=max
Step 10 — Add to compose.yml
In the production compose.yml (managed in circle-rd/documentation), add a new service:
my-project-docs:
image: ghcr.io/circle-rd/my-project-docs:latest
restart: unless-stopped
networks:
- docs
labels:
- traefik.enable=true
- traefik.http.routers.my-project-docs.rule=Host(`docs.circle-cyber.com`) && PathPrefix(`/my-project`)
- traefik.http.routers.my-project-docs.entrypoints=websecure
- traefik.http.routers.my-project-docs.tls.certresolver=letsencrypt
- traefik.http.routers.my-project-docs.middlewares=docs-headers
- traefik.http.services.my-project-docs.loadbalancer.server.port=3000
Then add a link to the landing page (content/index.md) in this portal:
::::u-page-feature{icon="i-lucide-box" title="My Project" description="Short description." to="https://docs.circle-cyber.com/my-project/"}
::::
Local development
cd docs-site
npm install
npx nuxt dev --port 3002 # pick any free port
# → http://localhost:3002/my-project/

