The open-source website and documentation for Nightingale,
served at https://n9e.github.io. Built with Docusaurus 3. English is the default locale at /;
Chinese is at /zh/. Chinese copy is the source of truth for wording, and the English copy is
written alongside it rather than machine-translated.
npm install
npm start # dev server for the default locale (English)
npm start -- --locale zh-Hans # dev server for the Chinese site
npm run build # both locales: build/ (English) and build/zh/ (Chinese)
npm run serve # serve the build output locally
npm run typechecknpm run build first runs the data scripts (see Generated data). If a fetch
fails, the build carries on with the data already in the repository. The build also writes
robots.txt (with both locales' sitemaps), llms.txt / llms-full.txt per locale and a .md
copy of every page next to its HTML (/docs/x → /docs/x.md) for AI crawlers. Links and images
in those Markdown files are absolute; scripts/llms-txt-links.mjs fixes the doubled /zh/zh/
prefix the llms-txt plugin otherwise produces for the Chinese locale (its route table is keyed by
prefixed paths, so the prefix cannot simply be stripped before it runs). After the build,
scripts/link-markdown-alternates.mjs adds <link rel="alternate" type="text/markdown"> (the page's
.md twin) and <link rel="describedby"> (the locale's llms.txt) to the head of every page that
has a Markdown copy, following the llms.txt proposal; GitHub Pages cannot send the equivalent
Link: header or negotiate Accept: text/markdown, so the tags are the only in-band discovery path.
Fonts are self-hosted: node scripts/sync-fonts.mjs copies the @fontsource files into
static/fonts/ and writes src/css/fonts.css, which docusaurus.config.ts inlines into <head>
(not customCss, so webpack does not rewrite the URLs) together with preloads for the weights used
above the fold. The CSS also defines metric-matched local fallbacks (Archivo Fallback,
JetBrains Mono Fallback, overrides computed with scripts/font-metrics.py), so the swap from the
fallback to the web font does not reflow the page. Lab CLS is ~0 on every page type; keep it that way
by giving every <img> a width and height or a fixed CSS box.
Every doc carries a last_reviewed: YYYY-MM-DD front matter field. It is rendered under the
title, emitted as article:modified_time and as TechArticle.dateModified, and it is the only
date signal the site gives (git timestamps are deliberately not used, see docusaurus.config.ts).
Bump it only after checking the page against the product. Troubleshooting pages are titled as
questions on purpose: that is how people and AI assistants phrase the query.
Every push to main triggers .github/workflows/deploy.yml: it runs npm ci && npm run build
and publishes build/ to the gh-pages branch, which GitHub Pages serves at
https://n9e.github.io. Pushing to main is a production release. The workflow can also be
run by hand from the Actions tab.
docusaurus.config.ts Site config: navbar, footer, i18n, analytics, the embed-mode script
sidebars.ts Generated by scripts/scaffold-docs.mjs — do not edit by hand
sidebars-integrations.ts Sidebar of the integration pages, grouped by category from integration-pages/index.json
docs/ English docs
integrations/ Generated English integration pages (/integrations/<slug>) — do not edit by hand
i18n/zh-Hans/ Chinese docs and theme strings (docusaurus-plugin-content-docs-integrations/ holds the generated Chinese integration pages)
i18n/en/ English theme strings (navbar, footer, sidebar group labels)
src/
content/ All homepage and landing-page copy (zh.ts and en.ts share types.ts)
components/home/ Homepage sections and the shared home.module.css
components/docs/ Components used inside docs pages (Shot, StackLayers, ...)
pages/ Homepage plus the comparisons, integrations, download and Categraf pages
css/custom.css Design tokens, Docusaurus theme overrides, embed-mode styles
clientModules/ Client-side hooks (Baidu Analytics pageviews on route changes)
data/stats.json GitHub numbers, refreshed by scripts/fetch-stats.mjs
data/releases.json Download page versions, links and checksums, refreshed by scripts/fetch-releases.mjs
data/integrations.json Integration template catalog, refreshed by scripts/fetch-integrations.mjs
data/integration-pages/ Per-integration rules, metrics, dashboards and collector configs for the integration pages
scripts/
docs-tree.mjs The docs information architecture: every page's title and description
scaffold-docs.mjs Regenerates sidebars.ts and the English sidebar labels from docs-tree.mjs
integration-categories.mjs Categories, hidden list, display names and search aliases for templates
integration-pages/ Integration pages: catalog.mjs (which templates get a page), fetch.mjs (upstream data),
intros.mjs (hand-written intro per integration, zh + en), generate.mjs (writes the .md files)
docs-env/ The screenshot environment for docs (see its README)
static/img/ Product screenshots, data source and channel logos, the user logo wall;
img/integrations/ holds template icons synced from upstream
/integrations is the searchable catalog (src/pages/integrations.tsx). The templates listed in
scripts/integration-pages/catalog.mjs additionally get a page each at /integrations/<slug>,
served by a second docs plugin instance (id: integrations, sidebar in sidebars-integrations.ts).
Those pages are generated, never edited by hand:
scripts/integration-pages/fetch.mjspulls each template's alert rules, metric descriptions, dashboard names, collector configs and the upstreami18n/en_US.jsontranslations intosrc/data/integration-pages/<Ident>.json(skipped when the catalog'streeShahas not changed).scripts/integration-pages/intros.mjsholds the hand-written summary, intro and connection notes per integration in both languages. Adding a template tocatalog.mjswithout an intro fails the build.scripts/integration-pages/generate.mjswritesintegrations/<slug>.md(English) andi18n/zh-Hans/docusaurus-plugin-content-docs-integrations/current/<slug>.md(Chinese). The files usemdx: { format: md }because upstream text contains<id>-style placeholders that MDX rejects.
npm run integrations:pages runs fetch + generate; prebuild runs them too, so a deploy picks up
upstream changes automatically. Catalog cards link to the site page when one exists and to the
GitHub README otherwise.
/comparisons is a hub; each competitor has a full page under src/pages/comparisons/<slug>.tsx
rendered by src/components/comparisons/DetailPage.tsx from comparisonsPage.details in
src/content/{en,zh}.ts (the hub's verdicts supply the stance, visual and Q&A; details add
the verdict, two-column table, migration summary and FAQ). The hub keeps the #prometheus,
#zabbix and #grafana anchors so old links still land.
The two long-form guides (zabbix-alternatives, open-source-alerting-platforms) are MDX pages:
English in src/pages/comparisons/, Chinese in
i18n/zh-Hans/docusaurus-plugin-content-pages/comparisons/. Both languages exist for every page
so the hreflang alternates never point at a 404. Claims about other projects are dated in the
page footer; keep "where the other tool fits" honest when editing.
All visible homepage and landing-page text lives in src/content/zh.ts and en.ts. Both follow
the structure in types.ts, so a missing field fails npm run typecheck. Navbar and footer
strings for English are in i18n/en/docusaurus-theme-classic/*.json.
Writing or editing docs follows WRITING-DOCS.md. Adding a docs page means
registering it in scripts/docs-tree.mjs and running npm run scaffold.
scripts/legacy-doc-urls.mjs records how each pre-redesign Hugo documentation URL is handled:
redirect to equivalent content, keep, restore, explain (a noindex notice with a suggested page,
for removed content that old links still point at), or retire (404). Its frozen inventory comes from the
commit before 54cbdc54; the older /docs/install/server entry is recorded separately and
redirects to the current installation overview rather than retaining an obsolete-architecture article.
English and Chinese keep their language. Collector and monitoring-practice articles retain
their original URLs and are registered in the current sidebar.
The old site's non-documentation routes (the blog and the Doks contributor pages) are listed
separately in the same file as legacySiteUrls; they redirect to the locale home, the closest
current page, or the GitHub contributor graph, which the official plugin cannot target, so
postbuild writes those aliases itself.
The official @docusaurus/plugin-client-redirects plugin emits static HTML aliases. postbuild
adds slashless aliases without overwriting current pages, copies the target canonical URL,
uses location.replace while forwarding query and hash, and checks the completed output.
These are zero-second meta-refresh redirects, not HTTP 301 responses. Retired and unknown
URLs remain 404 on GitHub Pages.
The old Hugo site also served RSS at /index.xml, /docs/index.xml, per-section
/docs/<section>/index.xml, /blog/index.xml and /contributors/index.xml (both locales).
scripts/write-legacy-feeds.mjs writes a small valid RSS 2.0 feed at each of those paths, with the
latest Nightingale and Categraf releases and a pointer to the new documentation, so feed readers
and crawlers that still poll them get a 200 and the new site's address rather than a 404.
The same plugin's createRedirects adds a trailing-slash alias for every current page
(/docs/x/ → /docs/x), because trailingSlash: false leaves only docs/x.html and GitHub
Pages answers /docs/x/ with 404 otherwise. scripts/finalize-slash-aliases.mjs gives those
pages the target's absolute canonical and location.replace, matching the legacy aliases. A client module also handles known old URLs reached by SPA
navigation; it does not replace the static aliases.
npm run test:legacy-doc-urls
npm run build # includes alias finalization and validation
npm run check:legacy-doc-urls # validate an already completed buildThe result table is build/legacy-doc-url-report.json. Validation includes every inventoried
URL, both slash forms, final canonical URLs, target content and sitemap exclusions for moved
pages. Changes to a mapping must include its reason and a valid final target. Do not redirect
unrelated deleted articles to the homepage. For a future domain migration, reuse the path
mapping and update the site URL rather than inserting an intermediate redirect destination.
The homepage numbers, the download page and the integrations catalog come from files in
src/data/, refreshed by:
npm run stats # GitHub stars / forks / latest release
npm run releases # release packages and checksums for the download page
npm run integrations # template catalog; icons are re-downloaded only when their blob sha changesSet GITHUB_TOKEN to avoid anonymous rate limits. When upstream adds a template that
scripts/integration-categories.mjs does not categorize yet, the script prints a warning and files
it under "Other"; the build does not fail.
The figures quoted across the homepage, comparisons page, integrations page and introduction
(integration count, data source types, notification channels, MCP tools) all come from
src/data/figures.ts. The integration count is the catalog length, and the "N of them ship alert
rules / dashboards / metric descriptions" counts in the docs are derived from the same catalog
(four upstream templates are deliberately hidden, see HIDDEN in
scripts/integration-categories.mjs, so the product's template center shows a few more); the MCP numbers are
src/data/mcp.json, written by scripts/docs-env/gen-mcp-reference.mjs from the toolset library
pinned in nightingale's go.mod (the standalone n9e-mcp-server is deprecated; the tools ship
inside n9e). Do not type these numbers into copy.
Docs screenshots are lossless WebP under static/img/docs/, produced from the PNGs that
scripts/docs-env/shoot-docs.mjs captures by node scripts/docs-env/webp-docs.mjs, which also
records their dimensions in src/data/doc-shots.json for the <Shot> component.
- Accent colors are only the product's own alert severity colors (S1 red / S2 orange / S3 blue), and they must carry real meaning, never decoration.
- The vertical rail beside the homepage's four stages is the severity bar from each alert row in the
product, enlarged; at the step where data becomes an
eventit switches to the three severity colors, so the color change itself states a fact. - Structural markers use object names that exist in the product (
datasource → rule → event → agent), not 01/02/03. - Motion is limited to one choreographed first screen (cards entering, the severity bar drawing,
delivery channels landing in turn) plus hover states; all of it is off under
prefers-reduced-motion. - Product screenshots are the core visual asset. Light and dark versions switch on
[data-theme], not in JavaScript.
The product's help drawer opens docs pages in an iframe with ?onlyContent&theme=dark|light. In
that mode the navbar, sidebars, table of contents and footer are hidden, the theme follows the
parameter without being saved, and a trailing-slash URL is redirected to the slashless one. The
logic is EMBED_INLINE_SCRIPT in docusaurus.config.ts; the styles are at the end of
src/css/custom.css.