Project Hooks
Introduction
Project hooks are the seam between ReeWeb's project-agnostic ssg/dev scripts and per-project behaviour. The contract is defined in lib/hooks.ts; each project supplies an implementation in src/lib/project_hooks.ts. The scripts call these hooks at fixed points, and a project tweaks behaviour by implementing only the hooks it needs.
The guiding rule:
scripts/andlib/stay byte-identical across every ReeWeb project. All variation lives inconfig/*(data and flags) andsrc/lib/project_hooks.ts(behaviour).
Every hook is optional. An omitted hook means "use the framework default", so base ReeWeb - which ships an empty hooks object - produces byte-identical output to having no hooks at all.
// src/lib/project_hooks.ts - the base/default
import type { ProjectHooks } from "$lib/hooks";
export const project_hooks: ProjectHooks = {};
The seams
| Hook | When it runs | Default |
|---|---|---|
helper_functions | Template render | No extra helpers |
page_data_extras | Every page render | No extra fields |
is_localized_path | SSG (SEO) | true (path is localized) |
resolve_md_layout | Markdown render | Built-in resolution |
shape_md_page | Markdown render | Body unchanged, no extra fields |
content_visibility | Visibility checks | Upstream decision unchanged |
helper_functions
helper_functions?: Record<string, (...args: any[]) => unknown>;
Extra functions exposed to every template via data.helpers, merged into the built-in helpers. Use it to add project-specific template utilities.
helper_functions: { shout: (s: string) => String(s).toUpperCase() },
page_data_extras
page_data_extras(ctx: PageDataCtx): Record<string, unknown>;
Extra fields merged into every page's render data - for example a version pill read once from package.json. Called per render, so projects that read from disk should compute once at module load and return the cached object.
PageDataCtx carries { is_dev, languages, default_language }.
import pkg from "../../package.json";
export const project_hooks: ProjectHooks = {
page_data_extras: () => ({ app_version: pkg.version }),
};
is_localized_path
is_localized_path(canonical_path: string, lang: string): boolean;
SSG-only SEO policy: is this canonical path a genuinely localized page? Default true. Return false for English-only subtrees (a blog, product docs) so they are dropped from the hreflang cluster and canonicalize to the default-language URL.
is_localized_path: (path) => !path.startsWith("/blog"),
resolve_md_layout
resolve_md_layout(rel_path, frontmatter, public_dir): string | undefined;
Override markdown layout resolution. Return undefined to fall back to the built-in chain (<name>.layout.ree → <name>.ree → layout).
shape_md_page
shape_md_page(input: ShapeMdInput): ShapeMdResult;
Transform a rendered markdown page's body HTML and contribute extra render fields (a table of contents, docs sidebar groups, a coming-soon body, page_title, …). Default: body unchanged, no extra fields.
ShapeMdInput carries the page's canonical_path, lang, frontmatter, the post-processed html_body, the extracted headings, public_dir, the full md_files list (e.g. to build a sidebar), languages, and default_language.
ShapeMdResult returns { body, fields? } - the final body markup injected as body, plus any extra fields merged into the page render data.
shape_md_page({ html_body, headings }) {
return { body: html_body, fields: { toc: headings } };
},
content_visibility
content_visibility(input: VisibilityInput): Visibility;
Per-page visibility. Receives the framework's default decision (from default_visibility: render / list / feed / sitemap / index) plus the page's frontmatter, canonical_path, and lang; returns the final decision. Use it to express project statuses (draft / review / published), path-based rules, or to decouple channels the default couples.
content_visibility({ frontmatter, default: base }) {
if (frontmatter.status === "review") {
return { ...base, list: false, feed: false, sitemap: false, index: false };
}
return base;
},
See Content Visibility for the full model and the meaning of each channel.
Putting it together
A project that needs several seams implements them in one object:
// src/lib/project_hooks.ts
import pkg from "../../package.json";
import type { ProjectHooks } from "$lib/hooks";
export const project_hooks: ProjectHooks = {
helper_functions: { shout: (s: string) => String(s).toUpperCase() },
page_data_extras: () => ({ app_version: pkg.version }),
is_localized_path: (path) => !path.startsWith("/blog"),
content_visibility({ frontmatter, default: base }) {
return frontmatter.status === "review"
? { ...base, list: false, feed: false, sitemap: false, index: false }
: base;
},
};
Related
- Content Visibility - the
content_visibilityseam in depth. - SSG Pipeline - where the scripts call each hook.