Content Visibility
Introduction
Content visibility is ReeWeb's single home for the per-page visibility policy: given a page's frontmatter and the SSG script clock, it decides where a page is visible. It lives in lib/content_visibility.ts and consolidates logic that was previously scattered across several files behind ad-hoc frontmatter keys (rss, sitemap, noindex, plus "always rendered").
The core idea is that a draft is a per-post soft launch. A page that is not published - a draft, or a post whose published_at is still in the future - is still rendered and reachable by URL, but it is absent from every aggregation (listings, RSS/JSON feeds, sitemap) and carries robots: noindex. To the outside world (random visitors, crawlers, feed readers) it does not exist, yet the author can still hand the URL to a reviewer. This mirrors how soft-launch languages behave, keyed per-post instead of per-language.
The module is pure - no I/O, and no clock of its own (callers pass now) - so the policy is deterministic and unit-testable. Projects do not edit this file. They adjust the result per-page through the content_visibility project hook.
The five channels
Visibility is not a single boolean - it is five independent channels:
type Visibility = {
render: boolean; // emit the HTML page at its URL at all
list: boolean; // appear in on-page listings / pagination
feed: boolean; // appear in the RSS + JSON feeds
sitemap: boolean; // appear in sitemap.xml
index: boolean; // omit robots:noindex (true = indexable)
};
| Channel | Controls |
|---|---|
render | Whether the page's HTML is emitted at its URL at all. |
list | Whether the page appears in on-page listings and pagination. |
feed | Whether the page appears in the RSS and JSON feeds. |
sitemap | Whether the page appears in sitemap.xml. |
index | Whether the page is indexable (false ⇒ robots: noindex). |
What counts as published
is_published(frontmatter, now) is the AND of two conjuncts:
not_draft-draft: true(or its inverse aliaspublished: false) marks a page unpublished.date_has_passed- apublished_at(or its aliasdate) in the future marks the page unpublished. This is a correctness guard so a future-dated post never leaks before its own date - it is not scheduling: nothing auto-regenerates; the page simply goes public the first time the SSG script runs after its date. Absent or unparseable dates never suppress.
Frontmatter keys
| Key | Type | Default | Effect |
|---|---|---|---|
draft | boolean | false | true → unpublished (generated but hidden everywhere, noindex). |
published | boolean | true | false is an inverse alias for draft: true. |
published_at | date string | - | A future date → unpublished until that date. |
date | date string | - | Alias for published_at. |
rss | boolean | true | false drops a published page from listings and feeds. |
sitemap | boolean | true | false drops a published page from the sitemap. |
noindex | boolean | false | true drops the page from listings, feeds, sitemap and marks it noindex. |
The default policy
default_visibility(frontmatter, now) resolves the channels:
Not published (draft or future-dated) - generated but hidden:
| render | list | feed | sitemap | index |
|---|---|---|---|---|
| ✅ | ❌ | ❌ | ❌ | ❌ |
Published - the historical per-key opt-outs apply exactly (so output is byte-identical to the pre-consolidation behaviour):
| Frontmatter | render | list | feed | sitemap | index |
|---|---|---|---|---|---|
| (none) | ✅ | ✅ | ✅ | ✅ | ✅ |
rss: false | ✅ | ❌ | ❌ | ✅ | ✅ |
sitemap: false | ✅ | ✅ | ✅ | ❌ | ✅ |
noindex: true | ✅ | ❌ | ❌ | ❌ | ❌ |
Soft-launch-language
noindexis applied separately (in page data) and is intentionally not modeled here - this function is per-page, not per-language.
Where it is applied
The same policy is consulted everywhere a page might be aggregated:
- Markdown render (SSG phase 9) - evaluates visibility per page; unpublished pages are rendered but excluded from listings/feeds/sitemap and marked
noindex. - Record collection (
lib/collect_records.ts) - a record is dropped when bothlistandfeedare false, so drafts and future-dated posts never reach a paginated listing or a feed. - Sitemap and RSS generators - honour the
sitemapandfeedchannels.
See Pagination and SSG Pipeline for where these calls sit.
Project override
resolve_visibility() returns the default policy, optionally adjusted by a project override. The override is injected by the caller (the scripts pass project_hooks.content_visibility), so this framework module never imports project code. An absent override returns the default unchanged.
The override receives the page plus the default decision, and returns the final one:
// src/lib/project_hooks.ts
export const project_hooks: ProjectHooks = {
content_visibility({ frontmatter, canonical_path, lang, default: base }) {
// start from `base`, then express project-specific rules:
if (frontmatter.status === "review") {
return { ...base, list: false, feed: false, sitemap: false, index: false };
}
return base;
},
};
Use it to express project statuses (draft / review / published), path-based rules, or to decouple channels the default couples (for example, listing a page but keeping it out of feeds). See Project Hooks for the full contract.
Related
- Project Hooks - the
content_visibilityseam. - Pagination - visibility filtering of collected records.
- Frontmatter Reference - all frontmatter keys.