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/ and lib/ stay byte-identical across every ReeWeb project. All variation lives in config/* (data and flags) and src/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

HookWhen it runsDefault
helper_functionsTemplate renderNo extra helpers
page_data_extrasEvery page renderNo extra fields
is_localized_pathSSG (SEO)true (path is localized)
resolve_md_layoutMarkdown renderBuilt-in resolution
shape_md_pageMarkdown renderBody unchanged, no extra fields
content_visibilityVisibility checksUpstream 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>.reelayout).

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;
    },
};