Skip to content

Previewing and building the docs site locally

This document is the canonical contributor guide for the laptop-only VitePress preview and build of the docs/ corpus. It maps the two opt-in make targets to the underlying VitePress binary, names the pinned tooling versions, walks through adding a new page to the sidebar, and points at the broader docs gates that already keep the corpus honest. The two targets are explicit opt-ins: they are deliberately not wired into make test or make lint so a contributor who never opens a markdown file never pays the npm-install cost.

The companion gate for this document is docs/contributing/ci.md, which catalogues the docs-related CI jobs (docs-check) and the make targets that reproduce them. This file is the operational read; ci.md is the gate-by-gate reference.

Prerequisites

  • Node.js. The pinned Node version lives in the repo-root .nvmrc and is the same one used by every npm-driven CI job. Match it locally with nvm use --install "$(cat .nvmrc)" before running either Make target.
  • Pinned dependencies. The VitePress runtime and its peers are pinned in tools/docs/package.json with a committed tools/docs/package-lock.json. Both Make targets run npm ci against that workspace before invoking the local node_modules/.bin/vitepress binary, so a fresh clone is sufficient and global npm state is never touched.
  • Disk layout. The build artefacts land in bin/docs/ and the VitePress on-disk cache lives under docs/.vitepress/cache/ — both are listed in the repo-root .gitignore so a stray git add cannot commit them.

Run the preview

bash
make docs-preview

make docs-preview runs tools/docs/node_modules/.bin/vitepress dev docs and prints the local URL — the dev server listens on http://localhost:5174. Port 5174 is pinned in docs/.vitepress/config.ts (with strictPort: true) so the documented preview URL is always the actual URL and never silently falls back to another port. The server has hot-module reload wired up: editing any markdown file under docs/, the navigation config in docs/.vitepress/config.ts, or the sidebar entries causes the open browser tab to refresh in place. Stop the server with Ctrl+C; nothing is written outside docs/.vitepress/cache/.

The preview is the fast iteration loop while authoring. It does NOT fail on broken intra-doc links — that gate fires on the build (make docs-build) and on the CI docs-check job. Treat the preview as a render-truth view, not as a validation pass.

Build for guard runs

bash
make docs-build

make docs-build runs tools/docs/node_modules/.bin/vitepress build docs --outDir <repo>/bin/docs and writes a static, deploy-ready bundle to bin/docs/. The build is the gate that surfaces broken intra-doc links and front-matter parse errors — exit-non-zero on either. Use it when:

  • you are about to push a docs change and want a local mirror of what the docs-check CI job will catch;
  • you are running the tests/docs/vitepress_build_smoke_test.go build-tag-gated integration test (go test -tags integration_docs ./tests/docs/...);
  • you want a static bin/docs/ tree to inspect with a plain HTTP server (e.g. python3 -m http.server -d bin/docs).

The output directory is excluded from git. All three docs targets (docs-build, docs-preview, docs-publish) are deliberately not part of the default make test or make lint chain — see internal/platform/make/make_targets_test.go for the drift gate that enforces that contract.

Publish to the SFTP webspace

bash
make docs-publish

make docs-publish rebuilds the site (it depends on docs-build, so you never ship a stale bin/docs/) and then rsyncs the three public surfaces to the SFTP webspace that fronts the plexsphere site. The remote layout is fixed by the hosting account and is not created by the target — only its contents are synced:

Remote pathSource
$(SFTP_REMOTE_DIR)/docs/bin/docs/ — the rendered VitePress bundle
$(SFTP_REMOTE_DIR)/api/contrib/api/index.html + api/openapi/plexsphere-v1.yaml
$(SFTP_REMOTE_DIR)/main/contrib/main/ — the standalone landing page

Each upload runs rsync with --delete over an SSH transport, so the remote tree is made byte-equal to its local source — a removed page or a renamed spec path cannot linger as a stale, still-reachable URL. The api/ payload is assembled in a bin/publish-api/ staging directory first: the Redoc viewer loads the spec through the relative URL ./plexsphere-v1.yaml, so the HTML and the YAML must land in the same remote directory and a single --delete pass must see both.

The connection defaults to the production webspace. Override any of SFTP_USER, SFTP_HOST, SFTP_REMOTE_DIR, or SFTP_SSH_PORT on the command line to push to a staging target, e.g.:

bash
make docs-publish SFTP_HOST=staging.example.net SFTP_REMOTE_DIR=plexsphere-staging

SSH key authentication is expected — the target invokes rsync -e ssh non-interactively and does not prompt for a password, so configure the webspace SSH key in your agent (or ~/.ssh/config) before running it.

Publish everything: the site and the CLI downloads

bash
make publish

make publish is the umbrella release-to-webspace entrypoint. It runs make docs-publish as a prerequisite — so the three website surfaces above (docs/, api/, main/) ship exactly as documented — and then adds a fourth surface: the prebuilt plexctl operator CLI an operator downloads from the webspace get/ directory. For that surface the target:

  1. cross-compiles cmd/plexctl/ for every entry in PLEXCTL_PLATFORMS (linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, and windows/amd64 by default) with CGO_ENABLED=0, into bin/publish-get/plexctl-<os>-<arch> (with a .exe suffix for Windows);
  2. writes a SHA256SUMS manifest over the binaries so a download can be verified before it is trusted;
  3. stages the static install page contrib/get/index.html next to them; and
  4. rsyncs the staging directory to $(SFTP_REMOTE_DIR)/get/ with the same --delete + SSH transport the other surfaces use, so a binary for a platform you removed from PLEXCTL_PLATFORMS cannot linger as a stale, still-downloadable URL.
Remote pathSource
$(SFTP_REMOTE_DIR)/get/cross-compiled plexctl-<os>-<arch> binaries, the SHA256SUMS manifest, and contrib/get/index.html

The install page links to each binary with a relative URL — the same host-agnostic pattern the Redoc viewer uses to load the OpenAPI spec — so it renders correctly no matter which domain or path the get/ directory is served under.

Because publish both cross-compiles Go binaries and rebuilds the docs site, it needs a Go toolchain matching /.go-version in addition to the Node/VitePress prerequisites above. The SFTP_* overrides apply unchanged, and PLEXCTL_PLATFORMS can be overridden to trim or extend the matrix:

bash
make publish PLEXCTL_PLATFORMS="linux/amd64 darwin/arm64"

Like the docs targets, make publish is an explicit operator entrypoint and is deliberately not part of the default make test or make lint chain.

Directory layout (docs/.vitepress/)

text
docs/
├── .vitepress/
│   ├── config.ts        # navigation + sidebar + Vite resolution overrides
│   └── cache/           # dev-server cache (gitignored)
├── tutorials/           # Tutorials quadrant (learning-oriented lessons)
├── how-to/              # How-to guides quadrant (task-oriented runbooks)
├── reference/           # Reference quadrant (typed surfaces: api, cli, …)
├── architecture/        # Explanation quadrant — system map
├── contexts/            # Explanation quadrant — bounded-context model
├── explanation/         # Explanation quadrant — root + the Diátaxis meta page
├── contributing/        # Contributing (non-Diátaxis): this guide lives here
├── index.md             # Home — the Diátaxis compass
├── tutorials/set-up-local-plexsphere.md               # Tutorials — set up your local plexsphere
└── contributing/dev-stack.md              # Contributing — dev stack runbook

The single source of truth for navigation is docs/.vitepress/config.ts. It declares the site title, the description, the Diátaxis-aligned top-level sidebar groups, and the Vite resolution overrides that let the build find the pinned runtime under tools/docs/node_modules/. The sidebar is hand-curated: there is no front-matter scrape and no auto-grouping in v1. A new file under docs/**/*.md is invisible to the preview until it has a sidebar entry, which is enforced by the drift gates under tests/docs/.

The canonical sidebar groups are the four Diátaxis quadrants in reading order, followed by Contributing — a deliberately non-Diátaxis fifth group for the contributor handbook. In the order they must appear in docs/.vitepress/config.ts, they are:

  1. Tutorials
  2. How-to guides
  3. Reference
  4. Explanation
  5. Contributing

The Explanation quadrant spans two page families — the system architecture under architecture/ and the bounded-context model under contexts/ — nested as sub-groups under the single Explanation group, with explanation/ carrying the quadrant root and the Diátaxis meta page.

Reordering or dropping any of these groups breaks the tests/docs/vitepress_config_test.go gate. Adding a new top-level group requires a coordinated update to that test, the config, and this guide in the same change.

Add a page to the sidebar (worked example)

Suppose you are adding a new how-to page at docs/how-to/operate-credentials.md. The minimum steps are:

  1. Create the markdown file with the canonical front-matter block. At minimum the file declares title, description, status, audience, and quadrant — the five fields the frontmatter gate in tests/docs/frontmatter_contract_test.go enforces. For a how-to that is quadrant: how-to. See Docs authoring conventions for the full field table.

    yaml
    ---
    title: Operating credentials in the field
    description: How to rotate, revoke, and re-mint credentials from the field without breaking the in-flight session.
    status: draft
    audience: operator
    quadrant: how-to
    ---
  2. Open docs/.vitepress/config.ts and add one entry under the How-to group's items: [...]. The entry shape is { text: '<menu label>', link: '<path-without-extension>' }, matching the convention every other sibling already uses:

    ts
    { text: 'Operate Credentials', link: '/how-to/operate-credentials' },
  3. Run make docs-preview and confirm the new entry appears in the sidebar under How-to in the order you added it. Click through to verify the page renders.

  4. Run make docs-build to confirm the build is clean — no broken links, no front-matter parse errors. If the build is green, the docs-check CI job will be green for the same diff.

  5. If the new file links to (or is linked from) sibling docs, run make docs-check to verify markdownlint and lychee accept the prose and the relative link targets.

Cross-references

  • docs/contributing/ci.md — the CI pipeline operator guide. Lists the docs-check job that gates every PR, the lychee / markdownlint commands make docs-check runs, and how to reproduce a red docs-check run locally.
  • docs/contributing/testing.md — the test pyramid and the shared internal/platform/testutil harness, including the Feature-ID rule the docs gates inherit.
  • docs/.vitepress/config.ts — the authoritative navigation + sidebar config the preview and build both consume.
  • tests/docs/vitepress_config_test.go — the drift gate that enforces the four Diátaxis quadrants (plus the Contributing group) in canonical order.
  • tests/docs/vitepress_build_smoke_test.go — the build-tag-gated integration test that runs make docs-build end-to-end and asserts a broken intra-doc link surfaces as a non-zero exit.