A documentation framework built on Angular 21. Write Markdown, HTML and SCSS
in docs/; get a fast, searchable documentation app — sections in the top navbar
with nested dropdown menus, one sidebar per section, live Angular components
inside Markdown, a built-in content manager with two publishing strategies, and
git-based author attribution. Dark mode by default.
Use feastdocs-template — not this repository. It is the same framework with none of this site's content, and GitHub's Use this template button gives you your own repo with no history and no fork link.
This repository is the framework's development home and the live demo at feastdocs.feast-labs.com. Clone it only to work on FeastDocs itself. The template is regenerated from here on every push, so it is never behind.
# Start a site
git clone https://github.com/Mindfeast/feastdocs-template.git my-docs
cd my-docs && npm install && npm start # http://localhost:4200# Or work on FeastDocs itself
npm install
npm start # http://localhost:4200This file is the complete feature reference. The site's own
docs/content covers the same ground with live examples — but you will replace it with your own docs, and this README is what remains. Keep it.
- How it works
- Commands
- Pages, sections and navigation
- Front matter reference
- Markdown features
- Angular components in Markdown
- Styling
- Search
- Theming
- Content manager and editing strategies
- Author attribution
- Repository changelog
- Configuration reference
- Build warnings
- Deploying
- Using this template for your own docs
- Where things live
Two halves that meet at a generated folder:
docs/**/*.md src/app/generated/
docs/**/*.html ──▶ tools/build-content.mjs ──▶ ├── docs/<page>.ts
docs/**/*.scss ├── registry.ts
feastdocs.config.mjs └── site-config.ts
public/search-index.json
public/docs-assets/**
The content pipeline renders everything at build time — Markdown, syntax highlighting (Shiki, both themes in one pass), page-scoped SCSS, the navigation trees, the search index, and git-based author metadata. The Angular app imports the result lazily, one chunk per page, so the initial bundle does not grow with the size of the docs set.
| Command | What it does |
|---|---|
npm start |
Content watcher + Angular dev server + the local editor API |
npm run build |
Render content, then build the static site to dist/feastdocs/browser/ |
npm run docs:build |
Render content once, no server |
npm run docs:new -- <path> ["Title"] [--scss] |
Scaffold a page (front matter filled in; --scss adds a scoped stylesheet) |
npm test |
Unit tests |
npm run format |
Prettier over the project |
During npm start, edits to docs/, feastdocs.config.mjs, and the
pipeline's own code under tools/ all trigger a rebuild (builds run in a
child process, so they always use the code on disk).
The docs/ folder is the navigation — nothing is registered by hand.
docs/
├── index.md → / landing page (no sidebar)
├── index.scss page-scoped styles for the landing page
├── guide/ → section "Guide": a navbar tab + its own sidebar
│ ├── _section.json { "label": "Guide", "description": "…", "position": 1 }
│ ├── index.md → /guide section landing, first in its sidebar
│ ├── installation.md → /guide/installation
│ └── advanced/ a category inside the section
│ ├── _category.json { "label": "Advanced", "position": 20, "collapsed": true }
│ ├── index.md → /guide/advanced makes the category clickable
│ └── recipes/ a sub-category (up to 4 category levels deep)
│ └── tips.md → /guide/advanced/recipes/tips
└── _drafts/ files/folders starting with _ are never published
- Sections (top-level folders) become navbar tabs. Each tab opens a dropdown of the section's tree, with nested categories as flyout submenus.
- Categories (nested folders) become collapsible sidebar groups; collapse state persists per reader, and the active page's branch auto-reveals.
- Depth limit: 8 folders (a section plus seven category levels). The editor
and
docs:newrefuse deeper paths; the build warns if one appears anyway. - Prev/next links follow sidebar order and never cross a section boundary.
- Breadcrumbs and the "On this page" table of contents (
h2/h3, with scroll tracking) are derived automatically. - The hamburger is always visible: on desktop it collapses/expands the docked sidebar (persisted); on mobile it opens a drawer that lists all sections plus the current section's tree.
- Ordering:
sidebar_positionascending, then alphabetical. Leave gaps of 10.
All fields optional, YAML between --- lines at the top of a page:
| Field | Type | Default | Effect |
|---|---|---|---|
title |
string | first # heading, else filename |
Page heading, browser title, search label |
description |
string | empty | Subtitle under the heading; meta description |
sidebar_label |
string | title |
Shorter label for the sidebar |
sidebar_position |
number | 999 |
Sort order among siblings |
slug |
string | derived from path | Overrides the route (collisions are a build warning) |
toc |
boolean | true |
false hides the "On this page" panel |
hidden |
boolean | false |
Keeps the URL and search entry, hides from navigation |
draft |
boolean | false |
Excluded from the build entirely |
tags / keywords |
string[] | [] |
Stored on the page / extra search terms |
sidebarLabel / sidebarPosition work as camelCase aliases.
CommonMark plus:
-
Admonitions — seven types, optional custom title:
:::tip Advice that saves time. ::: :::warning Deprecated in v3 Use `renderPage()` instead. :::
Types:
note,info,tip,success,warning,caution,danger. -
Code blocks — highlighted at build time (no highlighter in the browser), light and dark themes baked in, hover copy button included. Add a title:
```ts title="src/main.ts" bootstrap(); ```
Unknown languages fall back to plain monospace (with a build warning).
-
Tables — wrapped in a horizontal scroll container automatically.
-
Task lists —
- [x] done/- [ ] todo. -
Attributes — attach classes/ids to any element:
Text.{.callout}or## Heading {#custom-id}.{data-toc="false"}keeps a heading out of the TOC. -
Utility classes (global, work everywhere including the editor preview):
{.lead}for a larger intro paragraph,{.callout}for an accent-bar emphasis line. -
Links — relative
.md/.htmllinks become client-side routes and are validated on every build (broken links print a warning naming both files). External links open in a new tab. -
Images/assets — any non-doc file in
docs/is copied to the site and relative references are rewritten. Keep assets next to the page. -
Heading anchors — every heading gets a permanent
#link;##/###feed the table of contents. -
Raw HTML — passes through untouched inside
.md. A standalone.htmlfile indocs/is a full page too (front matter, TOC, link rewriting, search — everything except the Markdown parsing). Inside block-level HTML, wrap Markdown content in blank lines or it stays literal.
A fenced block tagged mermaid renders as a diagram — the same syntax GitHub
and Docusaurus use, so a migrating project keeps its diagrams unchanged:
```mermaid
graph LR
A[Write] --> B[Commit] --> C[Deploy]
```Mermaid is ~500kB, so it loads only on pages that contain a diagram and is not in the initial bundle. Diagrams render in the browser and therefore do not appear in prerendered HTML — the source does, as text, so the content stays indexable. A diagram that will not parse shows its source and Mermaid's error rather than breaking the page.
Doc components are real Angular components registered as custom elements
(src/app/doc-components/registry.ts),
so they run live inside any page — state and all.
Built-ins:
<!-- Tabbed content; panes are divs with a `tab` attribute -->
<fd-tabs>
<div tab="npm">(markdown here, surrounded by blank lines)</div>
<div tab="pnpm">…</div>
</fd-tabs>
<!-- Numbered tutorial steps with a connector line -->
<fd-steps>
<div step="Install">…markdown…</div>
<div step="Configure">…</div>
</fd-steps>
<!-- Live-state demo: attributes map to inputs -->
<fd-counter start="10" step="5"></fd-counter>
<!-- API reference row; the description is the element's content -->
<fd-api-field name="sidebar_position" type="number" default="999" required>
Sort order among siblings.
</fd-api-field>
<!-- Repository history from `git log`, grouped by month -->
<fd-changelog limit="20"></fd-changelog>
<!-- An index of the generated month pages, with counts -->
<fd-changelog-months></fd-changelog-months>
<!-- Cards linking to each repository's changelog -->
<fd-changelog-repos></fd-changelog-repos>
<!-- Cards for everything inside a category -->
<fd-category-index></fd-category-index>Category landing pages. A folder with an index.md uses it as the
category's own link. Without one, the build generates a landing page listing
what is inside as cards — so a category is never a sidebar entry that cannot be
opened. <fd-category-index> renders the same cards on any page you write.
Adding your own: build a standalone component under src/app/doc-components/,
add one line to registry.ts ({ tag: 'fd-chart', component: DocChart }), use
<fd-chart …> in any page. Two rules learned the hard way:
- The template must contain
<ng-content />if the component accepts content —@angular/elementssilently drops light-DOM children without a projection slot. - Use
ViewEncapsulation.Nonewhen the component styles Markdown passed into it.
Four levels, narrowest wins:
- Design tokens — every colour, radius, font and layout width is a CSS
custom property in
src/styles/_tokens.scss, defined for light on:rootand redefined under[data-theme='dark']. Layout knobs:--fd-content-max,--fd-sidebar-width,--fd-toc-width,--fd-navbar-height. - Built-in utilities —
{.lead},{.callout}(see Markdown features). - Site-wide overrides —
src/styles/custom.scssloads last; put your own utility classes and token overrides there (.fd-markdown .my-class { … }). - Page-scoped SCSS — a
.scssfile next to a page compiles at build time and is wrapped in[data-doc-slug="…"], so it cannot leak to other pages.@use/@importlines are hoisted correctly. Scoping is by selector, not shadow DOM — prefer classes over bare element selectors.
Inline in the navbar — no modal. Type and results drop down beneath the input;
Ctrl+K (or /) focuses it from anywhere; arrows + Enter
navigate; results deep-link to the matched heading with Section › Page crumbs
and highlighted snippets. The index is built at build time
(public/search-index.json), split per heading section, fetched once on first
use, ranked client-side (heading > title > body, word-start and exact-phrase
boosts). The sidebar has its own filter box scoped to the current section.
- Dark by default (configurable). A pre-boot script in
index.htmlapplies the theme before Angular loads — no flash of the wrong mode. - The navbar button flips light↔dark; the choice persists per reader.
- Accent colours come from the config (
theme.accent/theme.accentDark) and are written to:rootat runtime — no SCSS edit needed for branding. - Code blocks switch themes with pure CSS (Shiki emits both palettes).
/_editor (the pencil icon in the navbar): file tree, editor with
Ctrl+S, page creation with front-matter scaffolding, live
Markdown preview, and a "View page" link. Two backends, matching two publishing
strategies:
| Backend | When | What Save does |
|---|---|---|
| Local | npm start running (file API on 127.0.0.1:4271, scoped to docs/) |
Writes to disk; you commit and push from your own editor — the normal git flow |
| GitHub | github.repo set in the config — the mode for the deployed site |
Commits to the configured branch via the GitHub API, authored by the connected user |
The GitHub mode asks once for a fine-grained personal access token (contents
read/write on the docs repo); it stays in that browser's localStorage and is
sent only to api.github.com. A full OAuth sign-in requires a small
server-side code-for-token exchange (the client secret cannot ship in a static
site) — the connect screen is the seam where that plugs in.
The preview approximates the real build: admonitions, tables, task lists, attributes and utility classes render exactly; code highlighting, link rewriting and page-scoped SCSS only appear on the real page.
Nothing can write files in production except through GitHub — the local API exists only on localhost during development.
Every page footer shows "Last updated {date} by {author}", read from
git log at build time. Both editing strategies end as commits, so attribution
is always truthful and updates on the next build. Fallback: not a git repo (or
uncommitted file) → file date, no author.
Shallow checkouts (Cloudflare Pages clones with --depth 1, as does any CI
step missing fetch-depth: 0) would blank out most authors and reduce the
changelog to one entry, so the build deepens the clone itself before
reading history — and falls back to the GitHub API when it cannot. Setting
fetch-depth: 0 in your own pipeline is still the cheapest path.
<fd-changelog> turns the git history into a page — no hand-maintained
CHANGELOG file. The build collects the last changelog.limit commits (default 150) into a lazily-imported module: hash, author, date, subject, body, file count
and whether the commit touched docs/.
<fd-changelog></fd-changelog>
<!-- everything -->
<fd-changelog limit="20"></fd-changelog>
<!-- most recent 20 -->
<fd-changelog docs-only></fd-changelog>
<!-- content changes only -->
<fd-changelog repo="acme/api"></fd-changelog>
<!-- another repository -->Several products, several repositories. List them in changelog.repos and
one docs site carries a changelog per product — GitHub and Azure DevOps:
repos: [
'acme/api', // GitHub
{ repo: 'acme/app', branch: 'release' }, // GitHub
{
provider: 'azure',
org: 'contoso',
project: 'Pay', // Azure DevOps
repo: 'pay-api',
id: 'pay',
},
];Commit links follow the source. Private repositories work too — the token goes
in the build environment (GITHUB_TOKEN, AZURE_DEVOPS_PAT) via the host's
secret store, never in the config file. Note that whatever you collect becomes
publicly readable on the deployed page, commit messages and author names
included. Full walk-through: docs/guide/changelog-repos.md.
Commits are grouped by month. A Conventional Commits
prefix (feat:, fix:, docs:) becomes a badge and is stripped from the
headline; each hash links to the commit on GitHub when github.repo is set.
Same source as attribution, so the same shallow-clone handling applies: the
build deepens a --depth 1 checkout, or reads the history from the GitHub API
if it cannot. API entries have no file count (that endpoint carries no file
list).
Pages per repository, year and month. Set changelog.monthlyPages: true and
the build writes the tree itself — a category per repository, a category per
year, a page per month:
Changelog
├── Changelog (your own index.md)
├── Acme Docs
│ └── 2026 › August, July
└── Checkout API
└── 2026 › August
``` They are ordinary Markdown files — sidebar, search,
prerendering and sitemap all work normally — and each holds only a filter, never
the commits, so a new commit changes no file and a new month adds one. Generated
files are overwritten on each build; hand-written pages in those folders are left
alone.
The demo site puts all of it in a **Changelog** section
([`docs/changelog/`](docs/changelog)), but the component works on any page — drop
it wherever it fits your docs.
## Configuration reference
Everything lives in [`feastdocs.config.mjs`](feastdocs.config.mjs):
| Option | Type | Effect |
| --- | --- | --- |
| `title` | string | Navbar brand + browser-title suffix |
| `tagline` | string | Fallback meta description |
| `logo` | string or null | Image in `public/`, shown before the title |
| `docsDir` | string | Content folder (default `docs`) |
| `navbar.links` | array | Extra `{label, to}` or `{label, href}` links right of the section tabs |
| `footer.text` / `footer.links` | string / array | Footer content |
| `theme.defaultMode` | `'dark'`, `'light'`, `'system'` | First-visit theme |
| `theme.accent` / `theme.accentDark` | CSS colour | Accent per mode |
| `socialImage` | string or null | 1200×630 PNG/JPG in `public/` used as `og:image` for link previews |
| `editUrl` | string or null | Base URL for "Edit this page" links |
| `showLastUpdated` | boolean | Show date + author in page footers |
| `github.repo` | string or null | `owner/name` — enables web editing |
| `github.branch` | string | Branch web edits commit to (default `main`) |
| `changelog.limit` | number | Commits read from `git log` for `<fd-changelog>` (default `150`) |
| `changelog.repos` | array | Other repositories to collect history for, as `owner/name` or `{repo, branch}` |
| `changelog.monthlyPages` | boolean | Generate a page per month under a category per year |
| `changelog.branch` | string or null | Branch to read history from; `null` uses the checked-out branch |
| `changelog.groupByRepo` | boolean or `'auto'` | Group generated pages under a category per repository |
| `changelog.selfLabel` | string or null | Category label for this repository |
## Build warnings
The content build **never fails on a content problem** — it warns and carries
on, so a typo cannot block a deploy:
| Warning | Cause | Fix |
| --- | --- | --- |
| `link to "/x" does not match any document` | Broken or renamed link | Update the link |
| `Duplicate route "/x": a.md and b.md` | Two files resolve to one slug (`slug:` collision, or `page.md` + `page/index.md`); first wins | Rename one |
| `nested N folders deep — the maximum is 3` | Page too deep | Flatten or split into a section |
| `relative link "x" has no .md/.html extension` | Treated as an asset | Link the file, not the route |
| `<file>.scss: <error>` | Page stylesheet failed; page renders unstyled | Fix the SCSS |
| `Language "x" was not pre-loaded` | Unknown fence language; renders plain | Fix the language tag |
## SEO
Set `siteUrl` in the config and `npm run build` prerenders **one static
index.html per page** — article HTML baked in, per-page title/description,
canonical and Open Graph tags — plus `sitemap.xml` and `robots.txt`
(`/_editor` excluded). Crawlers and no-JS readers get real pages; Angular
boots on top and takes over seamlessly. Leave `siteUrl` as `null` to skip all
of it (right for internal sites).
## Deploying
`npm run build` → static files in `dist/feastdocs/browser/`. One rule on every
host: requests matching no file must fall back to `index.html`, because routing
happens in the browser. Ready-made configs ship for each target:
| Target | Use |
| --- | --- |
| **Docker** | `docker build -t my-docs . && docker run -p 8080:80 my-docs` — multi-stage `Dockerfile` (Node builds, nginx serves) |
| **nginx** (Linux) | `deploy/nginx.conf` — SPA fallback, immutable caching for hashed assets, gzip |
| **Windows / IIS** | `deploy/web.config` — rewrite rule + MIME types; needs the URL Rewrite module |
| **Azure Pipelines** | `deploy/azure-pipelines.yml` — builds and pushes the image (plus a no-Docker artifact variant) |
| **Cloudflare Pages** | `.github/workflows/ci.yml` — builds, tests and deploys on `main` once the two Cloudflare secrets exist |
Serving from a subpath: `ng build --base-href /docs/`.
**Everywhere: check out with full git history** (`fetch-depth: 0` /
`fetchDepth: 0`, and keep `.git` in the Docker context) — "last updated by" is
read from `git log` at build time and a shallow clone blanks it out.
The app self-heals after redeploys: a browser tab from an older build that hits
a renamed chunk reloads itself once and resyncs.
## Starting your own site
Use the **[starter template](https://github.com/Mindfeast/feastdocs-template)** —
the framework with none of this site's content:
> **Use this template** → your own repository, no history, no fork link.
```bash
npm install && npm start
The template is generated from this repository, never edited by hand: every
feature that lands here reaches it on the next push to main, via
npm run template:sync. What it leaves out is this site's docs/, branding and
configuration — everything else, including CLAUDE.md and the page templates
behind npm run docs:new, is the current version.
To rebuild it locally:
npm run template:sync -- ../feastdocs-template --commit- Clone, then point the remote at your own repository (or delete
.gitandgit initfor a clean history). - Edit
feastdocs.config.mjs— title, tagline, accents,editUrl,github.repo. - Replace the content of
docs/with your own sections. The template's own docs (Guide / Reference / Components) are a living demo of every feature on this page — skim them before deleting. - Add the CI workflow above and deploy.
Requirements: Node 22.12+. Angular is pinned to 21 on purpose (22 needs a newer Node); upgrade Node before upgrading Angular.
| Path | Purpose |
|---|---|
docs/ |
Your content — each top-level folder is a section. The only folder most people touch |
feastdocs.config.mjs |
All site configuration |
src/styles/_tokens.scss |
Design tokens (colours, sizes, fonts) for both themes |
src/styles/custom.scss |
Your site-wide overrides; loaded last |
src/app/doc-components/ |
Angular components usable inside Markdown + their registry |
src/app/ |
The application: layout, search, theming, page rendering, editor |
tools/ |
The content pipeline (collect → render → emit), dev watcher, editor API, git metadata |
src/app/generated/ |
Build output — regenerated, git-ignored, never edited |
public/ |
Static assets; also receives search-index.json and docs-assets/ |
MIT. Editing rights on a deployed site are simply your repository's collaborator permissions: the web editor commits with each visitor's own GitHub identity, and GitHub rejects writes from anyone without push access — a public site or a public repo changes nothing about who can edit.