title: Freshness
shell: standard
description: Build-time staleness checking for docs and knowledge bases — review cadence, owners, sources of truth, zero runtime JS.
freshness:
updated: 2026-04-21
review_every: 90d
owner: owner@example.com
sources_of_truth:
- label: Implementation
href: https://github.com/tdiderich/kazam/blob/main/src/freshness.rs
- label: "File a freshness bug"
href: https://github.com/tdiderich/kazam/issues/new
components:
- type: header
title: Freshness
subtitle: Every page can declare who owns it, when it was last updated, how often it should be reviewed, and where the sources of truth live. kazam turns that into a banner — yellow if a review is due within a week, red if it's already overdue — and a build-time report of everything that needs attention.
- type: section
eyebrow: The metadata
heading: One block of YAML per page
components:
- type: markdown
body: |
Add a `freshness:` block to any page's YAML. Every field is optional — a page with only
`updated:` and `review_every:` will still compute status correctly; fields you don't set
just aren't shown in the banner.
- type: code
language: yaml
code: |
title: Onboarding guide
shell: standard
freshness:
updated: 2026-01-15 # ISO date of the last content change
review_every: 90d # Nd | Nw | Nm | Ny | weekly | monthly | quarterly | yearly
owner: owner@example.com # free-form — email, Slack handle, team name
sources_of_truth: # bare URL or { label, href }
- https://notion.so/abc123
- label: "#ts-hub"
href: https://company.slack.com/archives/C012345
- label: "Linear: TSH project"
href: https://linear.app/co/project/tsh
components:
- type: header
title: Onboarding guide
...
- type: section
eyebrow: Banner variants
heading: Yellow, then red
components:
- type: markdown
body: |
kazam injects a banner at the top of the rendered page based on how close it is to the
review deadline. **No runtime JavaScript** — the check runs at `kazam build` time and
the banner is baked into the HTML. To keep the banner accurate between rebuilds, run
a scheduled daily build (a GitHub Action on `schedule:` works).
- type: callout
variant: warn
title: Review due soon
body: "Review is due in **5 days**. Last updated **Jan 15, 2026** (85 days ago). Review cadence: **every 90d**. Site last built: Apr 21, 2026. Owner: **owner@example.com**."
- type: callout
variant: danger
title: Review overdue
body: "Review is **20 days overdue**. Last updated **Jan 15, 2026** (110 days ago). Review cadence: **every 90d**. Site last built: Apr 21, 2026. Owner: **owner@example.com**."
- type: markdown
body: |
When `sources_of_truth:` is set, the real banner also renders a list of links
underneath — the reviewer clicks straight through to the doc / Slack channel / Linear
project to do the refresh. *(The two callouts above show the banner body; the real
banner adds a sources-of-truth row beneath.)*
- type: section
eyebrow: The build report
heading: Every build tells you what's stale
components:
- type: markdown
body: |
`kazam build` always prints a grouped summary of every page past (or nearly past) its
review window. Overdue items sort first, most-overdue at the top. Silent when nothing
is stale — no noise on healthy builds.
- type: code
language: text
code: |
$ kazam build .
_site/guide.html
_site/onboarding.html
_site/api/reference.html
✓ 3 page(s) → _site
⚠ 2 overdue page(s):
onboarding.html 20 day(s) overdue (cadence: every 90d) — owner owner@example.com
api/reference.html 5 day(s) overdue (cadence: every 180d) — owner eng@example.com
⏳ 1 page(s) due for review soon:
guide.html due in 3 day(s) (cadence: every 30d)
- type: callout
variant: info
title: Wire it into CI
body: "Run `kazam build .` on every PR and capture the log; the report becomes a free review surface. Or on a nightly cron: a weekly `gh action` that emails the owner field for anything overdue costs nothing and keeps the KB honest."
- type: section
eyebrow: Sources of truth
heading: kazam doesn't fetch — your agent does
components:
- type: markdown
body: |
The `sources_of_truth:` list is deliberately just labeled URLs. kazam never reaches
out to Notion, Linear, Slack, or anything else at build time — that preserves the
zero-supply-chain, no-network-at-build-time guarantee.
Where this gets interesting: an agent refreshing the page can see the sources, fetch
what it's able to (URLs via WebFetch, Linear via its MCP, Slack via its MCP), and ask
the user to paste anything it can't reach. User owns the fetcher; agent owns the
integration; kazam stays narrow.
- type: columns
equal_heights: true
columns:
- - type: markdown
body: |
**Shorthand** — just a URL. The label becomes the URL itself.
- type: code
language: yaml
code: |
sources_of_truth:
- https://notion.so/abc
- - type: markdown
body: |
**Labeled** — any link (Slack channel, Linear project, HubSpot dashboard) gets a readable label.
- type: code
language: yaml
code: |
sources_of_truth:
- label: "#ts-hub"
href: https://company.slack.com/archives/C01
- label: "Linear: TSH"
href: https://linear.app/co/project/tsh
- type: section
eyebrow: How status is computed
heading: "`updated + review_every` vs today"
components:
- type: markdown
body: |
The check is a pure date comparison — `updated + review_every` vs today (from
`KAZAM_TODAY` when set, else the system clock). Three states:
- type: stat_grid
columns: 3
stats:
- label: Fresh
value: No banner
detail: More than 7 days until the review deadline.
color: green
- label: Due soon
value: Yellow
detail: Within 7 days of the deadline. Reviewer sees the nudge.
color: yellow
- label: Overdue
value: Red
detail: Past the deadline. Report surfaces on every build.
color: red
- type: markdown
body: |
Pages missing either `updated:` or `review_every:` are always Fresh — there's nothing
to compare against. The yellow-window size (currently 7 days) will become configurable
per site in a future release if demand surfaces.
- type: section
eyebrow: Agent workflow
heading: "Point your agent at `_site/stale.md`"
components:
- type: markdown
body: |
Every build that finds any stale pages also writes a markdown report to
`_site/stale.md`. Overdue pages first, due-soon below, each with path, cadence,
and owner. Perfect for handing an agent one path and saying "fix these."
- type: code
language: bash
code: |
# Build, then hand the stale report straight to Claude Code.
# Claude reads _site/stale.md, then opens each page's .yaml and
# its sources_of_truth to propose updates.
kazam build .
claude -p "Read _site/stale.md. For each stale page, open its source YAML in the repo + the sources_of_truth links, then propose concrete updates."
- type: markdown
body: |
The file is rewritten on every build and deleted when no pages are stale,
so "does `_site/stale.md` exist?" is a cheap CI check: any PR that introduces
stale pages will produce the file; a clean build leaves it absent.
- type: section
eyebrow: Sibling check
heading: The link graph
components:
- type: markdown
body: |
Same shape, different signal. Every `kazam build` also walks the page graph
from `index.html` + the site `nav:` and reports two classes of problem:
- **Orphan pages** — built pages not reachable from `index.html` via nav
or any in-page `href:`. Either link them, delete them, or set
`unlisted: true` on the page (same flag that excludes drafts from
`llms.txt`).
- **Broken internal links** — `.html` hrefs that don't match any built
page. Conservative: assets, externals, anchors, `mailto:`, and `tel:`
are skipped.
Silent on clean builds. When anything surfaces, the build prints a grouped
summary and writes `_site/links.md` — same agent-consumable pattern as
`stale.md`.
- type: code
language: text
code: |
$ kazam build .
_site/index.html
_site/guide.html
_site/examples/landing.html
✓ 3 page(s) → _site
⚠ 1 orphan page(s) (not linked from nav or any page):
examples/landing.html
⚠ 1 broken internal link(s):
guide.html → /missing.html
- type: markdown
body: |
`kazam dev` and `kazam build --allow-orphans` silence orphan detection —
useful for work-in-progress pages you haven't wired into nav yet. Broken
links always surface; there's no legitimate reason to tolerate a dead
internal href.