@pagenary/publisher
v2026.9.1
Published
Multi-tenant documentation static site generator with Markdown, search, SEO snapshots, and docs-as-code publishing.
Downloads
7,513
Maintainers
Readme
Pagenary Publisher
Where documentation takes shape.
@pagenary/publisher is the static site generator behind Pagenary — it turns one shared template catalog into many branded, tenant-specific documentation sites. Self-hosted static output by default, hash-based routing, full-text search, and a Git-aware build pipeline. Install it as a dev dependency and drive it with the pagenary CLI.
Built with AIWG, the multi-agent AI framework used to plan, audit, and ship this project.
npm install --save-dev @pagenary/publisher # add Pagenary to your project
npx pagenary build my-docs # build your docs tenant
npx pagenary serve # serve on http://localhost:5173Docs Site · Quick Start · Features · Tenant Workflow · Documentation
What It Is
The publisher takes a catalog of shared section templates plus per-tenant content and configuration and produces a self-contained documentation bundle for each tenant. Each bundle is a static single-page app — hash-based routing (#page-id), no application server, with self-hosted browser assets by default — that you build once and host anywhere that serves files. A per-tenant <base> resolves asset and module URLs to the tenant root, so the same bundle serves correctly at a domain root or under a subpath mount. Tenants share the template catalog but keep isolated content, branding, navigation, and domains — each with ranked client-side search and SEO-ready output — so one repository can publish a dozen distinct sites.
Quick Start
Install the package and drive it with the pagenary CLI — no clone required.
New here? Follow the Getting Started guide.
npm install --save-dev @pagenary/publisher
npx pagenary build my-docs # build your tenant (see Tenant Registry below)
npx pagenary serve # preview on http://localhost:5173Commands: build, serve, tenants list, check, new
(run npx pagenary --help for the full list). The package also
ships a compiled reference site under site/ — the Pagenary docs, built by
Pagenary itself.
Building from source (contributors / modifying Pagenary):
npm install
npm run dev # build + serve with watch mode
npm run build # build default bundle to dist/Pagenary development uses AIWG. On this host, maintainers can
inspect, build, or run the AIWG project from ~/dev/aiwg.
Optional writable runtimes
The optional @pagenary/react adapter exposes record and PGlite runtimes, reviewed published-source refresh, local dataset imports, and scoped lexical search. These capabilities require explicit tenant/application integration; the default publisher search remains static and read-only. See the runtime API and boundaries and dependency tiers.
For repository tests, use Node 24.12.0 and run npm run test:setup from the repository root before npm test. Browser layout tests are part of the publisher suite. See Testing Pagenary for the complete prerequisites and qualification scope.
Features
Content Authoring
- Markdown — write in
.mdfiles with full CommonMark support - HTML — direct markup control with
.htmlfiles - JavaScript Modules — dynamic content with
.jsfiles returning{ html, afterRender? } - Nested Directories — organize content in subdirectories (
content/guides/setup.md) - React Brochureware — load a validated canonical content module while the SPA owns presentation; emit semantic route HTML, search, sitemap, JSON/text, and LLM-readable artifacts (guide)
Rich Content
- Inline LaTeX — full KaTeX-supported math syntax in inline/display contexts, rendered to local HTML + MathML with bounded macros and expansion
- LaTeX Pages — convert complete
.texpages to semantic HTML + MathML through an explicitly selected local converter profile - Mermaid Diagrams — local pinned Mermaid 11.16.0 runtime by default, with optional sanitized build-time SVG
- PlantUML Diagrams — local-only build rendering with the PlantUML
SANDBOXsecurity profile and contained includes - Rich Content Guide — syntax, configuration, prerequisites, security boundaries, and failure behavior (guide)
- Interactive Images — opt-in bounded zoom, pan, and reset for SVG, PNG, and JPEG/JPG with accessible text and keyboard/touch controls
- Markdown Delivery — optionally emit per-route
.mdrepresentations and serve them from the same canonical URL whenAccept: text/markdownis preferred - Syntax Highlighting — Prism.js with 10+ language support
- Markdown Tables — full table syntax with alignment support
- HTML Components — spec tables, layer stacks, box diagrams, cards
- Internal Links — auto-resolved
#section-idlinks in Markdown
External Links
- Navigation Links — add external URLs directly in the manifest with a
urlproperty - Smart Link Handling — external links open in a new tab with security headers (
rel="noopener noreferrer") - Visual Indicators — a subtle ↗ icon marks external destinations
- CTA Styling — button-like
external-ctaclass for prominent external links
Navigation & Search
- Command Palette —
Ctrl/Cmd+Kor/opens a global finder - Fortemi-backed full-text search — ranked results with snippets over a static
chunked index emitted at build time; lazy chunk fetch (precache) and offset
paging for infinite scroll, with a clean in-browser fallback. No server, no WASM.
The engine is vendored (
@fortemi/core) — seedocs/VENDORING.mdfor the process,docs/ARCHITECTURE.md, and.aiwg/architecture/adr/ADR-015-fortemi-core-search-adapter.md. - Docs Map — opt-in static graph view over the same Fortemi corpus, with weighted relationships, zoom/pan controls, neighbor highlighting, and node metadata popups.
- Manifest-Driven Nav — declarative navigation structure
- Keyboard Navigation — arrow keys, Enter to select
Theming & Branding
- Custom Colors —
accentColorandsurfaceColorper tenant - Brand Identity — logo text, tagline, copyright
- Typography — IBM Plex Sans/Mono defaults, customizable
SEO (built in)
- Metadata-driven titles — the shell
<title>derives from the default page's metadata title ("<page title> · <brand>"), and each section sets its own title/description at runtime; the generic brand is only a fallback - Absolute URLs — declare a
domain(orseo.siteUrl) and the sitemap, canonical,og:url, androbotsURLs become fully-qualified - Static snapshots — crawler-friendly
/pages/<id>.htmlfor every section, self-canonical (the SPA hash route isn't crawlable) - Root HTML fallback —
seo.rootHtmlFallbackdefaults on, embedding the default page inindex.htmlso no-JS readers do not see an empty shell sitemap.xml,robots.txt,llms.txt— generated automatically- JSON-LD + Open Graph —
TechArticle/BreadcrumbListper page, optional Organization data, andog:image/twitter:imageviaseo.ogImage - Subpath-safe assets — a per-tenant
<base>keeps stylesheet/script/snapshot URLs valid whether served at a domain root or a subpath
Export & Sharing
- Export Options — Current Page or Entire Site
- Branded Exports — tenant logo, brand name, and tagline in the export header
- Document Export — one-click HTML export with a table of contents, print-optimized for PDF
Tenant Content Workflow
Directory Structure
my-tenant/
├── config.json # Branding, theme, and SEO settings
├── manifest.json # Navigation structure (optional)
├── content/ # Content files
│ ├── welcome.md # Root-level content
│ ├── guides/ # Nested directory
│ │ ├── _manifest.json # Section manifest
│ │ ├── getting-started.md
│ │ └── advanced.md
│ └── api/
│ ├── _manifest.json
│ └── reference.md
└── overrides/ # Post-build file replacements (optional)Content Types
Markdown (.md)
# Getting Started
Welcome to the docs. Here's a code example:
\`\`\`javascript
console.log('Hello, Pagenary!');
\`\`\`
And a Mermaid diagram:
\`\`\`mermaid
graph LR
A[Start] --> B[Build]
B --> C[Deploy]
\`\`\`Markdown also supports build-time inline LaTeX with \(...\), \[...\], and
$$...$$; single-dollar math is opt-in. See the
rich technical content guide for syntax and bounds.
LaTeX (.tex)
\documentclass{article}
\usepackage{amsmath}
\begin{document}
\section{A semantic LaTeX page}
\[ E = mc^2 \]
\end{document}Complete .tex pages require the local converter selected by latexPage.profile.
Pagenary does not install converters or TeX packages during the build.
HTML (.html)
<section class="section doc">
<h1>Custom HTML Section</h1>
<table class="spec-table">
<tr><th>Feature</th><th>Status</th></tr>
<tr><td>Search</td><td>Ready</td></tr>
</table>
</section>JavaScript (.js)
export async function load() {
const data = await fetch('/api/metrics.json').then(r => r.json());
return {
html: `<section><h1>Metrics: ${data.count}</h1></section>`,
afterRender(container) {
// DOM manipulation after render
}
};
}Manifest Configuration
Root manifest.json (optional — auto-generated from content/ if omitted):
[
{ "id": "welcome", "title": "Welcome", "file": "welcome.md" },
{
"id": "guides",
"title": "Guides",
"subsections": [
{ "id": "guides/getting-started", "title": "Getting Started", "file": "guides/getting-started.md" },
{ "id": "guides/advanced", "title": "Advanced Usage", "file": "guides/advanced.md" }
]
}
]Section _manifest.json (in content subdirectories):
{
"title": "API Reference",
"sections": [
{ "id": "overview", "title": "Overview", "file": "overview.md" },
{ "id": "endpoints", "title": "Endpoints", "file": "endpoints.md" }
]
}External links in the manifest (use url instead of file):
[
{ "id": "welcome", "title": "Welcome", "file": "welcome.md" },
{ "title": "Support Portal", "url": "https://support.example.com" }
]Branding & SEO Configuration
config.json:
{
"title": "My Documentation",
"description": "Comprehensive guide to our platform",
"brandMark": "ACME",
"brandSub": "Docs",
"tagline": "Build better, faster",
"copyright": "ACME Corp",
"accentColor": "#6366F1",
"surfaceColor": "#F7FAFC",
"domain": "docs.acme.com",
"seo": {
"siteUrl": "https://docs.acme.com",
"ogImage": "/assets/og-card.png",
"structuredData": { "organizationName": "ACME Corporation" }
}
}| Property | Description | Default |
|----------|-------------|---------|
| title | Browser tab title | "Documentation" |
| description | Meta description for SEO | - |
| brandMark | Primary brand text (bold) | "DOCS" |
| brandSub | Secondary brand text (light) | "TOOLKIT" |
| tagline | Subtitle under brand | - |
| copyright | Footer copyright text | - |
| accentColor | Links, buttons, highlights | #111111 |
| surfaceColor | Background color (hex) | #ffffff |
| domain | Canonical domain; also the SEO base URL when seo.siteUrl is unset | - |
| seo | SEO block — see Tenant Configuration | - |
Build Commands
With the package installed (the default):
npx pagenary build # build the default bundle to dist/
npx pagenary build --all # build all enabled tenants
npx pagenary build my-tenant # build a specific tenant
npx pagenary tenants list # list configured tenants
npx pagenary serve # serve dist/ on localhost:5173From source (clone — adds dev/utility scripts):
npm run build:incremental my-tenant # git-aware incremental rebuild
npm run dev # build + serve with watch
npm run lint:content # check trailing whitespace/tabs
npm run check:seo # verify SEO metadata
npm run check # run all checks
npm test # run test suite
npm run test:browser # optional real-browser smoke (see below)Browser smoke test (optional)
npm run test:browser runs scripts/smoke-browser.mjs, a real-browser check for
things the jest/node suite can't cover: <base href> resolution under a subpath
mount, asset + section loading, the runtime <title> brand, and Fortemi
command-palette search. It builds + serves the pagenary tenant and drives a
headless Chromium, capturing a screenshot for review.
Playwright is not a dependency (keeps the install lean); the script skips with instructions when it's absent. Enable it once:
npm i -D playwright && npx playwright install chromium
SMOKE_REQUIRE=1 npm run test:browser # fail (not skip) if Playwright is missing — use in CITenant Registry
Register tenants in a tenants.json at your project root (validated by the
bundled tenants.schema.json):
{
"tenants": [
{
"id": "my-docs",
"source": { "type": "local", "path": "./docs" },
"strictLinks": true
},
{
"id": "client-portal",
"source": { "type": "git", "url": "https://github.com/org/client-docs.git", "ref": "main" },
"domains": ["docs.client.com"]
}
]
}Source types:
- Local:
{ "type": "local", "path": "./relative/or/abs/path" } - Git:
{ "type": "git", "url": "https://…", "ref": "main", "path": "subdir" }
Per-tenant options include enabled (default true), strictLinks (default
true — fail the build on broken internal links), and domains (for the
multi-tenant Caddy router). See Tenant Configuration.
Docker Caddy Workflow
For multi-tenant domain testing:
# Add to /etc/hosts:
# 127.0.0.1 my-docs.local client-portal.local
npm run build:tenants # build tenants
npm run caddy:up # start Caddy
# Visit http://my-docs.local or http://client-portal.local
npm run caddy:logs # tail logs
npm run caddy:reload # reload config without restart
npm run caddy:restart # full restart
npm run caddy:down # stop containerUse a non-privileged port: DOCS_TOOLKIT_PORT=5173 npm run caddy:up
Repository Layout
apps/publisher/
├── src/
│ ├── index.html # SPA shell
│ ├── app.js # Router and core logic
│ ├── styles.css # All styling
│ ├── manifest.js # Default navigation
│ ├── seo.js # Runtime meta tag management
│ ├── mermaid-init.js # Diagram rendering
│ ├── syntax-highlight.js # Code highlighting
│ └── lib/ # search, router, export
├── scripts/
│ ├── build.js # Core build script
│ ├── build-tenants.js # Multi-tenant builder
│ ├── serve.js # Dev server
│ └── lib/seo-generator.js # Sitemap, robots, snapshots, JSON-LD
├── tenants/ # Built-in example tenants
├── docs/ # Documentation
└── Caddyfile, docker-compose.yml # Multi-tenant routingDocumentation
The full documentation site is published at docs.pagenary.com — built by this publisher from the source below. Read it online, or browse the source:
- Getting Started — start here: zero to a published site with the npm package
- Quick Start Guide — step-by-step tenant creation
- Publish with GitHub/Gitea Actions — make any docs repo Pagenary-ready: copy-paste CI workflows + auto-discovery
- Consuming and Embedding Blog Updates — use
@pagenary/blog-client,<pagenary-blog>, and CORS/CDN settings for cross-site updates pages - Tenant Configuration — all config options (branding, theme, SEO)
- Theming Recipes — copy-paste recipes for colors, fonts, and nav positions, with screenshots
- Styles: choose and install — select, verify, install, test, and roll back a versioned catalog style
- Styles: configure and use — load order, tenant branding, upgrades, light/dark behavior, and troubleshooting
- Styles: author — manifests, compatibility, provenance, hashes, fixtures, and previews
- Styles: contribute — canonical Gitea workflow and review gates
- Architecture — system design
- API Reference — module documentation
- Deployment — hosting patterns
- Extending — customization guide
- Dependency Posture & Operational Tiers — static (zero-dependency) vs hybrid React vs full Fortémi runtime, and how to choose
- Minimizing Supply-Chain Exposure — hardening playbook for low-trust / air-gapped deployments
- React/SPA Publishing — the opt-in React adapter and hybrid routes
- Vendoring — how the static tier stays dependency-free
License
GNU Affero General Public License v3.0 — strong copyleft. You may use, modify, and distribute Pagenary, but if you run a modified version to provide a network service, you must make the modified source available to its users. See LICENSE.
Made with care by Joseph Magly
