npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kirigami/php-prepros

v3.0.0

Published

PHP preprocessor for the Kirigami static site generator. Compile PHP page templates to clean, deployable HTML — with zero server dependency.

Readme


@kirigami/php-prepros

PHP preprocessor for the Kirigami static site generator.

npm version License: GPL-3.0-or-later Node.js >=24.0.0 Website


Overview

Build full static websites in PHP — with zero server, zero runtime dependency, zero compromise on expressiveness. Write your pages as regular PHP files, annotate them with a PHPDOC header, and let php-prepros compile everything to clean, deployable HTML.

It is the perfect solution for GitHub Pages. Since it runs entirely in Node.js, it is fully compatible with GitHub Actions, allowing you to automate your deployment pipeline effortlessly.

Part of the Kirigami project ecosystem.


Table of contents


What's new in 3.0.0

This release switches YAML:: to the native YAML extension, MD:: to native mdhtml, SCHEMA to native jsonk, and Normalizer to native norm. It also includes page types and request lifecycle hooks.

PHP data files use the native yaml extension backed by LibYAML. Its YAML 1.1 implicit booleans include unquoted y, n, yes, no, on, off, true, and false, including mapping keys. Quote these words when you mean strings (for example, "NO": Norway). YAML::parse() / parseFile() / loadFile() preserve the wrapper’s array/object choice; native yaml_parse() / yaml_parse_file() have their own extension signatures. yaml_load_file() remains a wrapper alias. The project’s kirigami.yaml is parsed separately in Node through struct-walker and js-yaml.

MD:: delegates to the native mdhtml extension (cmark-gfm). Footnotes now use <section class="footnotes" data-footnotes> instead of the old <div class="footnotes">; target .footnotes rather than a specific container tag in custom CSS.

SCHEMA validates through the native jsonk extension (draft 2020-12), keeping its API and the "path: message" error format. It now also supports if/then/else, contains, propertyNames, dependentRequired/dependentSchemas, prefixItems, and $ref to absolute or $id-relative URLs (fetched over the network). Error messages use jsonk's wording, and an additionalProperties: false violation is reported on the parent object instead of the extra property. The previous pure-PHP validator stays available as SCHEMA_LEGACY.

Normalizer now comes from the native norm extension (utf8proc) instead of the bundled pure-PHP polyfill, which stays available as NORMALIZER_LEGACY.

Breaking: the seo.jsonld sub-block is merged into seo:. META and LD now read one set of keys, so the site description, keywords, image, language and person are declared once. JSON-LD is injected whenever the seo: block exists; seo.jsonld is only an on/off switch (default true). To migrate, move the keys of seo.jsonld: {…} up into seo: (jsonld.auto: false becomes jsonld: false) and rename seo.language to seo.lang. kiri rejects the old shapes with a message saying so. Sites with seo: {} and no jsonld now get JSON-LD too; add jsonld: false to keep them without it.

# before                       # after
seo:                           seo:
  language: fr-CA                lang: fr-CA
  jsonld:                        type: ProfessionalService
    type: ProfessionalService    logo: images/logo.png
    logo: images/logo.png

What's new in 2.0.0

  • Breaking: meta: and jsonld: merged into one top-level seo: block. The two used to be independent siblings of kirigami: that happened to share fallback data; they're now one block, one mental model for a project's whole SEO/social surface — META's own keys live directly under seo:, and jsonld is nested inside it as its own sub-block:

    # before (1.x)
    meta:
      favicon: favicon.png
    jsonld: {}
    
    # after (2.0.0)
    seo:
      favicon: favicon.png
      jsonld: {}

    The two stay independently toggled exactly as before — a project can have META's tags without JSON-LD, or vice versa; seo: { jsonld: false } (or { auto: false }) stops just the JSON-LD injection, seo: false (or { auto: false } at the top level) stops just META's. Every fallback chain (a page's PHPDOC → the block → the loose kirigami: keys) is unchanged, including jsonld's own keys still feeding META's defaults (jsonld.image, jsonld.lang, jsonld.person, …) — only where the two blocks live in kirigami.yaml changed, not how resolution works. Migration: rename meta: to seo: and move the jsonld: block's content under it as seo.jsonld:. See seo block / meta config / jsonld config.


What's new in 1.9.3

  • Dependency bump to @kirigami/struct-walker 1.0.5; homepage + README pointed at the site (metadata only).

What's new in 1.9.2

  • Fixed a real Markdown bug: a list item's source line count was 1:1 with <li> count, so an indented continuation line with no marker of its own (a soft-wrapped - **foo** text\n more text) fell outside the block-matching regex entirely — the list closed after the first line, the continuation resurfaced as a stray flat <p>, and a new list reopened for the next marker line. Fixed by widening the block regex to also accept a marker-less indented line and, in the per-line loop, appending it to the previous item instead of dropping it.

What's new in 1.9.1

  • {% img-asset %} no longer crashes the build on an unresolvable path. IMG::asset()'s exception is now caught and turned into an HTML comment, matching codepen/checklist's own missing-argument behavior instead of aborting the whole render. This also makes it safe to document the tag — a literal `{% img-asset path … %}` written as prose inside a code span still runs the plugin (code-span protection only swaps the displayed output back to the literal text; the callback itself always executes), so a placeholder path used to throw "Invalid image file." and fail the build.

What's new in 1.9.0

  • {% youtube %} removed — moved to @kirigami/plugin-embed, which replaces the old plain-iframe output with a real oEmbed-backed card (cover thumbnail, title, play button — no network call until the visitor actually clicks play). If a project used the built-in {% youtube %}, install the plugin; without it, the tag now falls through unresolved ({% youtube ID %} printed as-is) rather than rendering an iframe. codepen/checklist/callout are unaffected.

What's new in 1.8.0

  • {% img-asset %} — a new built-in Markdown plugin. Same pipeline as the <img asset> HTML tag (IMG::asset(): resize, cache, publish under image.dest), usable straight from Markdown text:

    {% img-asset photo.jpg %}
    {% img-asset photo.jpg 800 %}
    {% img-asset photo.jpg 800 600 %}
    {% img-asset photo.jpg 800 600 cover %}

    Positional args: source path (relative to image.source), width, height, and the literal cover keyword. Registered in md.plugins.php alongside codepen/youtube/checklist/callout — available out of the box, drop it with MD::unregisterPlugin('img-asset') if you don't want it.


What's new in 1.7.2

  • Cleaner formatted output around highlighted code. The de-indent script PREPROS::injectHead() adds when prepros.format is on now flattens the <pre><code> indentation HTML::format() writes for every block, including ones a build-time highlighter has wrapped in <span>s. It works on innerHTML line by line and removes only the shared leading run (relative indentation is kept). This lets @kirigami/plugin-highlight 1.7.2+ re-indent its markup to line up with the rest of the document instead of leaving it flush-left — the served HTML stays consistently indented, the rendered code is still de-indented before the first paint.

  • <markdown prose> wraps in .prose. With the prose attribute the built-in tag emits <div class="prose"> … </div> so long-form Markdown picks up @kirigami/canva's styles/prose typography with no extra markup. Opt-in (a bare <markdown> is unchanged); class / id on the tag land on the wrapper.


What's new in 1.7.1

  • No side effects on import. kirigami.yaml is now loaded on first use (render() / sitemap() / runenv() / processImages()), not while the module is being imported. import '@kirigami/php-prepros' from a directory with no project no longer throws — which is what made kiri build --help / kiri export --help / kiri run --help crash instead of printing their help.

What's new in 1.7.0

  • META class — a <head> SEO / social metadata generator, the companion to LD. Builds the standard tags — <title>, description, keywords, robots, language, generator, author, Open Graph, Twitter Card, <link rel="canonical">, favicon / apple-touch-icon / humans — from each page's PHPDOC, the top-level meta: block, and the loose kirigami: / jsonld: keys LD already reads. It emits only what it can resolve, and leaves any tag the layout already hand-writes untouched.

    • Opt-in: a top-level meta: block (empty meta: {} is enough) switches on automatic injection into every page's <head>. meta: false (or { auto: false }) keeps the config but stops the injection.
    • Per-page PHPDOC: @meta false (skip), @meta_title, @meta_description (falls back to @description / @abstract / @excerpt), @meta_keywords, @meta_image, @meta_robots, @meta_type, @canonical.
    • Manual builders — always emitted, still de-duplicated: META::tag(), META::link(), META::raw(), META::tags(). Procedural aliases: meta_tag(), meta_link(), meta_raw(), meta_tags().

    Full key reference: METAmeta config.


What's new in 1.6.0

  • Managed <head> (prepros.head). Every rendered page's <head> is now auto-wired: a tiny theme/FOUC guard as the first child (adds the js class, applies the stored data-theme before first paint), a <link rel="stylesheet"> for every sass task output, and a <script> (no defer, just before </body>) for every esbuild task output — each with a per-page relative path and a ?<timestamp> cache-bust. A file already referenced in the page is left alone, so you can still hand-place one. Turn it off with prepros: { head: false }, or head: false on a single sass/esbuild task. A template's header.php no longer wires assets at all.

  • HTML::format() indents <pre><code>. A fenced code block's lines are shifted to the block's nesting depth so the HTML source stays readable (relative indentation preserved). The exact leading run is stripped again before it's shown, by a small de-indent script prepros.head injects before </body> (only when format is on). Since 1.7.2 the script works on innerHTML line by line, so it also flattens blocks a build-time highlighter has wrapped in <span>s. A bare <pre> and <textarea> are still emitted byte-for-byte.


What's new in 1.4.0

A round of fixes to the rough edges that showed up building a full site from scratch — mostly developer-experience, all backward compatible.

  • HTML::format() keeps <pre> / <textarea> verbatim. Their line breaks, indentation and blank lines are no longer collapsed, so a fenced code block survives the formatter intact — format: true and Markdown code blocks now coexist. (1.6.0 refines this: a <pre><code> block is re-indented to its nesting depth and de-indented again before display.)
  • The default Markdown plugins load out of the box. {% callout %}, {% youtube %}, {% codepen %} and {% checklist %} are registered automatically (md.plugins.php is auto-included from MD), as the docs always said. Drop one with MD::unregisterPlugin('name') or shadow it with your own MD::registerPlugin().
  • Build errors you can actually read. A fatal in a template (a bad call, a null argument, a TypeError…) comes back as a structured failure with the message, the offending page and the file:line — never a bare Error: undefined. The try/catch now covers Throwable, not just Exception. PHP warnings and notices no longer sink an otherwise-clean build: they surface as warnings on the result. kiri prints the message, the page, and the tail of the PHP stderr/debug output on failure.
  • PHPDOC parsing. A tag value may now wrap onto the following indented continuation lines instead of being silently truncated at the first line. And an @word written in the block's prose is ignored rather than overwriting a real tag — only lines that start with @ open a tag.
  • FS::getBreadcrumb() / FS::getChildren() are anchored on the page being rendered (PREPROS::$file), so they return the right trail / child list when called from a layout include, a partial, or a helper function — not only straight from the template. Pass an explicit path to override.
  • {% tag %} inside a code span or code block stays literal (`{% badge %}` renders as text) instead of being expanded — or leaking an unrestored placeholder.
  • IMG never upscales. A requested size larger than the source is clamped down to the source instead of throwing an opaque encoder error (the AVIF encoder in particular).
  • Build-time tokens expand at render time. ###YEAR###, ###TIMESTAMP### and ###TODAY### are substituted when each page is generated, so kiri build / kiri watch previews show real values, not the literal token (previously only kiri export replaced them).
  • page_info hook robustness. The first built-in callback accepts either the [$file, $info] pair the hook fires with or the bare $info object a later callback receives, so a custom page_info hook can't fatal on the argument shape. See PREPROS hooks.

What's new in 1.3.0

  • LD class — a schema.org JSON-LD generator. Collects structured-data nodes during a render and emits them as a single <script type="application/ld+json"> @graph in every page's <head>.
    • Automatic injection is opt-in: add a top-level jsonld: block to kirigami.yaml (even empty, jsonld: {}) and an Organization (+ Person, WebSite, WebPage, BreadcrumbList) graph is derived from that block plus the loose keys projects already carry (person, jobtitle, email, area, knowsabout, keywords, facebook, …). No jsonld: block → nothing is injected.
    • Turn it back off with jsonld: false / jsonld: { auto: false }, or per page with @ld false; per-page @ld_type / @ld_title / @ld_image / … tags feed the page node, and a BreadcrumbList is built from the _index.php ancestor trail with no opt-in.
    • Explicit builders for everything else: LD::add(), LD::article(), LD::faqPage(), LD::breadcrumb(), LD::ref(), and every schema.org type via LD::typeName([...]). Procedural aliases: ld_add(), ld_organization(), ld_script(), …
    • A page that already hand-writes an application/ld+json script is left untouched.

What's new in 1.2.1

  • processImages() JS export — batch resize / palette-extraction through the IMG class (src/imagebatch.php). @kirigami/kirigami's sass task now uses it for img-asset() / colors(), so the whole toolchain is free of a native image dependency (sharp is gone).
  • IMG::save() takes an optional $quality (0-100) for jpg / webp / avif; null keeps the per-format default (82).

What's new in 1.2.0

  • SCHEMA class — a pure-PHP, dependency-free JSON Schema validator (Draft-7 style, Ajv-like API: isValid() / validate() / getErrors()).
  • IMG::asset() / IMG::palette() — static helpers powering kirigami-core's img-asset() and colors() Sass functions: on-demand resize/convert of a source image, and cached representative-colour extraction.
  • <img asset="…"> tag — the HTML-side entry point of the image autogenerator, same parameters as IMG::asset() (see Built-in tags).
  • IMG now handles vector and exotic formats — SVG, EPS, AI, PDF (rasterized via Imagick), plus HEIC / TIFF / BMP, on top of GD's JPEG / PNG / GIF / WebP / AVIF.
  • MD emoji shortcodes:rocket: → 🚀 from a large built-in map, extend­able with MD::registerEmoji().
  • MD footnotes and definition lists[^1] / [^1]: …, and Term / : ….
  • MD inline HTML is now sanitized against a tag/attribute allowlist rather than passed through verbatim.
  • STR::normalize() — Unicode NFD + combining-mark stripping; STR::slug($str, $sep = '') now takes a separator (pass '-' for a hyphenated slug).
  • Bundled Normalizer polyfillext-intl isn't in the WASM build, so a polyfill keeps Normalizer::normalize() (and STR::normalize() / slug()) working.

Earlier, in 1.1.x: PREPROS::mount() + the mountPath() / runenv() JS exports, the SCRAPER and CURL and ARR classes, YAML::loadFile(), the STR::is_url() / html_entities_decode() / shorthash() / slug() helpers, the {% youtube %} / {% codepen %} / {% checklist %} MD plugins, and the @content / @indent PHPDOC annotations.


How it works

@kirigami/php-prepros runs your PHP source files inside a WebAssembly PHP 8.x runtime (@kirigami/php-wasm), entirely in Node.js — no PHP installation required on the host machine.

The lifecycle of a page build looks like this:

_index.php  ──▶  PHP (wasm)  ──▶  processTags()  ──▶  HTML::format()  ──▶  index.html
                    │
                    ├── before.php  (optional layout header)
                    ├── after.php   (optional layout footer)
                    └── PHPDOC annotations resolved (yaml / json / md / url)

Files are mounted into the WebAssembly virtual filesystem on demand. Only .php, .json, .yaml, .yml, .md, .db, .txt and any extra extensions listed in prepros.mountext are mounted automatically, keeping memory usage low. Anything else can be mounted on demand with PREPROS::mount().


Installation

npm install @kirigami/php-prepros

Configuration — kirigami.yaml

Every project must have a kirigami.yaml at its root. The preprocessor reads it at startup and throws if it is absent or invalid.

@kirigami/php-prepros consumes project data, SEO, preprocessing, image settings, and task metadata for managed head injection. The core engine owns plugin loading, task orchestration, export, and full schema validation. Direct use of this package is not a substitute for core configuration validation. All settings share kirigami.yaml; its schema is kirigami.schema.json.

# yaml-language-server: $schema=https://cdn.jsdelivr.net/gh/php-kirigami/kirigami@main/packages/kirigami/kirigami.schema.json
kirigami:
  # ── Required ──────────────────────────────────────────────────────────
  project:     My Website           # Site name. Printed in the CLI banner, exposed as $project.
  baseurl:     https://example.com  # Deployed root URL, no trailing slash. Used for sitemap.xml.
  root:        src                  # Source directory containing your _*.php pages.

  # ── Optional ────────────────────────────────────────────────────────
  banner:      assets/banner.txt    # Text file stamped as a license banner on exported files.

  # ── Arbitrary project data ──────────────────────────────────────────
  # Everything else under `kirigami:` is free-form. The whole block is
  # extracted as PHP variables and made available in every page, in
  # before.php/after.php, and anywhere PREPROS::$config->data is read.
  author:      Jane Doe
  email:       [email protected]
  gtag:        G-XXXXXXXXXX
  description: A short description of the site, useful for <meta name="description">.
  keywords:
    - keyword one
    - keyword two

seo:                            # Presence turns on the META tags and LD JSON-LD (`seo: {}` is enough).
  type: Organization            # JSON-LD main entity; `jsonld: false` turns the JSON-LD off.
  logo: assets/logo.png

prepros:
  before:  _layouts/header.php  # Included before every page body.
  after:   _layouts/footer.php  # Included after every page body.
  format:  true                 # Pretty-print the HTML output (default: false).
  network: true                 # Allow HTTP fetches in PHPDOC @tag annotations / CURL / SCRAPER.
  mountext:                     # Extra file extensions to auto-mount into the wasm fs,
    - .svg                      # in addition to the defaults (.php .json .yaml .yml .md .db .txt).
    - .webp
  includes:                     # PHP files auto-included once, before any page renders.
    - _lib/functions.php
  types:                        # Named page types — opt in per page with @type <name>.
    article:
      before: _layouts/types/article.header.php
      after:  _layouts/types/article.footer.php

image:                          # Image autogenerator — powers IMG::asset() / IMG::palette().
  format: webp                  # webp | avif (default: webp)
  source: assets/images         # Source folder, relative to cwd() (default: assets/images)
  dest:   images                # Output folder, relative to kirigami.root (default: images)

plugins:
  - name: "@kirigami/plugin-highlight"
    active: true
    options:
      theme: auto

esbuild:
  # minify: false

sass:
  style: expanded

export:
  path:   dist
  ignore: ["*.psd", "notes/"]

scripts:
  - name: convert-images
    mount: ["assets/images/**/*.jpg"]
    trigger: before-build       # before-build | before-export | after-export

tasks:
  - name:  js-core
    type:  esbuild
    entry: scripts/kirigami.core.js

  - name:  scss-core
    type:  sass
    entry: styles/kirigami.core.scss

kirigami block

Core project settings. Read by php-prepros. The entire block is extracted into PHP variables and made available in every page template, before.php, after.php, and prepros.includes files — $project, $author, $gtag, etc. are available with no further setup, and also as PREPROS::$config->data.

| Key | Required | Description | |-----|----------|--------------| | project | ✅ | Human-readable site name. Exposed as $project. | | baseurl | ✅ | Root URL of the deployed site, no trailing slash. Used to build absolute <loc> entries in sitemap.xml; exposed as $baseurl. | | root | ✅ | Path (relative to the project root) to the directory containing your _*.php source pages. Build fails immediately if missing or if the path doesn't exist. | | banner | — | Path (relative to the project root) to a text file stamped as a license/copyright banner on exported .js/.css/.html files during kiri export. May contain the ###DATE### token, replaced with today's date. Falls back to an auto-generated banner. | | anything else | — | Free-form key/value pairs (strings, numbers, booleans, lists, nested maps — anything valid YAML). Every key is extracted as a PHP variable ($author, $gtag, …). Use this for contact info, social links, analytics IDs, SEO keywords, or any project data you want available everywhere. When the top-level seo block is present, LD also reads some of these by convention: person, jobtitle, email, area, knowsabout, keywords, and social-network URL keys (facebook, instagram, …). |

seo block

Top-level, optional. The SEO surface: one set of keys feeds both the META tags (the standard SEO / social <meta> and <link> tags) and the LD schema.org JSON-LD graph, and its presence switches both on for every page's <head>. An empty seo: {} is enough; everything is derived from the kirigami block and each page's PHPDOC (@title, @description / @abstract, @keywords, @image, @robots, @og_type, @canonical). A tag or script the layout already hand-writes is left untouched.

  • auto: false stops META's tags, jsonld: false stops the JSON-LD; the config values stay available to META::tags() / LD::script().
  • seo: false keeps nothing on; no block at all means nothing is injected (explicit META::tag() / LD::add() calls still emit).

Full key reference: seo config. Per-page tags: @meta_* and @ld_*.

prepros block

Global before and after files are optional and default to null; an empty prepros: {} renders without a layout. When provided, these paths must refer to existing files. PHP warnings are logged to stderr and returned as diagnostics, without being inserted into generated HTML.

Options for the PHP → HTML compiler. Read by php-prepros. Declaring this block (even empty) also makes kiri prepend a forced prepros task on every build/export/watch.

| Key | Type | Default | Description | |-----|------|---------|--------------| | before | string | — | Path (relative to kirigami.root) to a PHP file included before every page's body. Typically your <head>/layout opening. | | after | string | — | Path (relative to kirigami.root) to a PHP file included after every page's body. Typically your layout closing. | | format | bool | false | Pretty-print the compiled HTML via HTML::format() before writing it to disk. | | head | bool | true | Auto-wire each page's <head>: a theme/FOUC guard as the first child, a <link rel="stylesheet"> per sass task output, and a <script> (no defer, before </body>) per esbuild task output — each with a per-page relative path and a ?<timestamp> cache-bust. A file already referenced in the page is skipped. Set false to disable, or head: false on a single sass/esbuild task to skip just its tag. | | network | bool | false | Enables outbound HTTP(S) inside the WASM PHP runtime. Required for PHPDOC @tag https://… annotations that fetch remote .yaml/.json/.md data (see Auto-loading data files), and for the CURL / SCRAPER classes. | | mountext | string[] | [] | Extra file extensions to mount automatically into the virtual filesystem alongside the built-in .php, .json, .yaml, .yml, .md, .db, .txt. Use this for assets your PHP code reads directly (e.g. .svg, .webp). Files with extensions not in this set are skipped during mounting — mount them on demand with PREPROS::mount() instead. | | includes | string[] | [] | PHP files (relative to kirigami.root) include_once'd once, right after config is loaded — before any page renders. The natural place to PREPROS::registerTag(), PREPROS::registerHook(), or MD::registerPlugin(). | | types | object | {} | Named page types. A page opts in with @type <name> in its PHPDOC header; the matching entry's before/after (each string, relative to kirigami.root, both optional) wrap the page body one level inside the global before/after — render order is global before → type before → body → type after → global after. A page with no @type, or naming a type absent here, renders with just the global wrap. See @type. |

image block

Options for the image autogenerator. Read by php-prepros — these are what IMG::asset() / IMG::palette(), the <img asset> tag, and kirigami-core's img-asset() / colors() Sass functions all resolve against. Optional; the defaults below apply even when the block is absent.

| Key | Type | Default | Description | |-----|------|---------|--------------| | format | string | webp | Output format for generated images: webp or avif. | | source | string | assets/images | Folder holding the source images, relative to cwd(). | | dest | string | images | Destination folder for generated images, relative to kirigami.root. |

plugins block

List of Kirigami plugins. Consumed by the kiri CLI (see @kirigami/sdk), not by php-prepros directly.

| Key | Required | Description | |-----|----------|--------------| | name | ✅ | Plugin package name. Must match @kirigami/plugin-*, <scope>/kirigami-plugin-*, or kirigami-plugin-*. | | active | ✅ | Whether the plugin is loaded. | | options | — | Free-form object passed to the plugin; its shape depends on the plugin. |

esbuild / sass blocks

Free-form objects. Consumed by the kiri CLI. There is no fixed key set: whatever you put here is spread straight into the underlying library call for every matching task, after Kirigami's own defaults — so it can also override them (minify, target, style: "compressed", source maps, …). Refer to esbuild's BuildOptions and Dart Sass's Options for what's accepted. Writing the key with nothing under it parses to null in YAML, equivalent to omitting the block.

sass: additionally recognizes two keys that are not passed to Dart Sass:

| Key | Type | Description | |-----|------|--------------| | before | string / string[] | Extra .scss files compiled before the task entry (paths relative to cwd()). | | after | string / string[] | Extra .scss files compiled after the task entry. |

export block

Options for kiri export. Consumed by the kiri CLI. Optional.

| Key | Type | Default | Description | |-----|------|---------|--------------| | path | string | dist | Output directory for kiri export, relative to the project root. | | ignore | string[] | [] | Extra gitignore-style patterns excluded from the export copy, on top of Kirigami's built-in exclusions. |

scripts block

Named PHP scripts. Consumed by the kiri CLI, which runs each scripts/<name>.php through runenv() — so the full php-prepros class library is available and kirigami.yaml's kirigami block is exposed as PREPROS::$config->data.

| Key | Required | Description | |-----|----------|--------------| | name | ✅ | Must match an existing scripts/<name>.php file. Run with kiri run <name> [args...]; extra CLI arguments are forwarded as $argv entries. | | mount | — | Glob patterns (relative to the project root) of extra local files to mount into the sandbox before the script runs. | | trigger | — | Fire the script automatically: before-build (start of build and export), before-export (very start of export), or after-export (once export has finished). |

tasks block

Ordered list of build tasks, run in array order. Consumed by the kiri CLI, on top of the implicit prepros task (added when the prepros block is present) and the implicit dist task (added during kiri export).

| type | Purpose | Required fields | Optional | |--------|---------|-----------------|----------| | esbuild | Bundle/minify a JS/TS entry. Build + watch. Output: <entry>.min.js. | name, type, entry | force | | sass | Compile a .scss/.sass entry, minified with csso on export. Build + watch. Output: <entry>.min.css. | name, type, entry | force | | prepros | Render pages + sitemap.xml. Watch only (runs on build/export only when forced/implicit). | name, type | target, force | | dist | Copy kirigami.root into an output dir, stamping the banner. Forced/implicit only. | name, type, path | ignore, force |


Writing pages

Source pages live in the directory pointed to by kirigami.root. The naming convention is straightforward: any file whose name starts with _ and ends in .php is treated as a page source. The leading underscore is stripped in the output filename.

src/
├── _layouts/
├── _lib/
├── _index.php          →  src/index.html
├── about/
│   └── _index.php      →  src/about/index.html
└── blog/
    ├── _index.php       →  src/blog/index.html
    └── _articles.yaml   (data file, not compiled)

Directories whose name starts with _ (e.g. _layouts/, _lib/) are skipped entirely during directory-wide builds.

PHPDOC header

Every page starts with a PHP docblock that drives metadata and data loading:

<?php
/**
 * @name     about
 * @title    About us
 * @abstract A short description of this page.
 */
?>
<section>
    <h1><?php echo $title; ?></h1>
    <p><?php echo $abstract; ?></p>
</section>

All annotations are injected as PHP variables ($name, $title, $abstract, …). You can define any custom annotation you need.

Annotations are also available as variables in before and after PHP included files, so you can write proper metas in the HTML header.

Only lines whose first non-whitespace character (past the * gutter) is @ open an annotation — an @word written in the prose of the block is left alone. A value can wrap onto the following indented continuation lines:

/**
 * @title       About us
 * @description A longer blurb that does not fit comfortably
 *              on a single line and continues here.
 */

Auto-loading data files

When an annotation value looks like a filename (with a .yaml, .yml, .json, or .md extension), it is automatically parsed and injected as a structured variable instead of a plain string.

<?php
/**
 * @name     medias
 * @articles _articles.yaml
 */
?>
<?php foreach ($articles as $article): ?>
    <a href="<?php echo $article->lien; ?>">
        <?php echo $article->titre; ?>
    </a>
<?php endforeach; ?>

| Extension | Parsed as | |-----------|-----------| | .yaml / .yml | stdClass object (or array of objects for sequences) | | .json | Result of json_decode() | | .md | HTML string via MD::toHtml() |

When network: true is set in kirigami.yaml, annotation values that start with http:// or https:// are fetched from the network and parsed the same way:

/**
 * @posts https://api.example.com/posts.json
 */

@content, @indent, and @type

Three special annotation names change how a page's body is assembled:

  • @content — if a content variable already resolves to a non-empty value (typically because it's a .md/.yaml/.json annotation that auto-loaded into HTML/data, see above), it is used as-is as the page body, and the PHP file itself is not executed for its output. This is handy for pages that are pure data/markdown wrapped by a shared layout.
  • @indent — when set to a number, every line of the rendered body is prefixed with that many spaces before being wrapped by before.php/after.php. Useful for keeping generated HTML readable when a page is nested inside indented layout markup.
  • @type — names an entry under prepros.types. If it matches, that entry's before/after wrap the (already-indented) body one level inside before.php/after.php: global before → type before → body → type after → global after. No match (missing annotation, or a name absent from prepros.types) leaves the page with just the global wrap — a page type is an extra layer, never a replacement for the site's real header/footer.
<?php
/**
 * @name    changelog
 * @title   Changelog
 * @content _changelog.md
 * @indent  4
 * @type    article
 */

Built-in tags

Two tags are registered out of the box (prepros.plugins.php) and processed after the PHP runs, on the assembled HTML — no include or plugin needed.

<markdown> … </markdown>

Converts its inner content from Markdown to HTML, stripping the common leading indentation first (via STR::trimIndent()) so you can indent it naturally inside your template. All registered MD plugins work inside it. See Extending the <markdown> tag to override it.

<section>
    <markdown>
        ## Who we are

        We are a **student organization** from Québec.
    </markdown>
</section>

Add the prose attribute — <markdown prose> — to wrap the output in <div class="prose">, so it picks up the long-form typography of @kirigami/canva's styles/prose with no extra markup. Any class / id on the tag lands on that wrapper (<markdown prose class="lede" id="intro"><div class="prose lede" id="intro">). A bare <markdown> emits just the converted HTML, as before.

<img asset="…">

The HTML-side entry point of the image autogenerator — the exact same feature as the img-asset() Sass function and IMG::asset(), with the same parameters. The tag calls IMG::asset() under the hood, then swaps the asset attribute for the generated src.

<!-- in:  resolves assets/images/hero.jpg through IMG::asset('hero.jpg', 800, 0, false) -->
<img asset="hero.jpg" width="800" alt="Our office" loading="lazy">
<!-- out: <img src="../images/hero-800w.webp" alt="Our office" loading="lazy"> -->

| Attribute | Maps to IMG::asset() arg | Notes | |-----------|---------------------------|-------| | asset | $path | Required. Path relative to image.source. Missing/empty ⇒ the tag is left untouched. | | width | $width | Optional, integer. Omitted ⇒ 0 (keep). | | height | $height | Optional, integer. Omitted ⇒ 0 (keep). | | cover | $cover | Boolean — presence means true (crop + fill). | | (any other) | — | alt, class, id, loading, … are passed straight through onto the output <img>. |

asset / width / height / cover are consumed and removed; everything else survives. The generated file lands in image.dest and is only (re)generated when missing or older than the source — see IMG for the naming convention.

The Sass img-asset() / colors() functions, the <img asset> tag and IMG::asset() all run on the same engine — the IMG class (GD, with the Imagick fallback) in this package. @kirigami/kirigami's sass task routes its image work here through processImages(), so there is no native image dependency in the toolchain.


JavaScript API

import { render, sitemap, runenv, mountPath, processImages, resetRuntime } from '@kirigami/php-prepros';

PHP operations are queued in call order, including mounts and result extraction. await resetRuntime() waits for preceding PHP work, disposes the owned runtime and its network proxy, and clears cached configuration and mounts. The next operation initializes a fresh runtime from kirigami.yaml. Core Project.reload() calls this and also clears the plugin PHP include list. It does not clear persistent cache/cookie files on disk. This remains a single-project API whose working directory must be set before import.

render(file?, phpIncludes?)

phpIncludes defaults to []. The core collects it through prepros:php; direct callers supply local PHP file paths (absolute or relative to the working directory). Existing paths are mounted under /plugins/ and included before rendering; missing paths are silently skipped. Each render replaces the runtime include list, which remains on its configuration until another render or reset. Direct calls do not load the core plugin registry for you.

Compile a single PHP page or a whole directory.

// Compile one page
const pageResult = await render('about/_index.php');

// Compile everything under src/
const treeResult = await render('.');

// Compile everything (uses kirigami.root from config)
const defaultResult = await render();

Paths used by render() are all relative to the kirigami.root configuration.

Returns Promise<PreprosResult>:

interface PreprosResult {
  success: boolean;
  files?:  string[];   // project-relative paths; may be absent on parsing failure
  error?:  string;
  debug?:  string;     // captured PHP stdout
  stderr?: string;     // diagnostics attached to failures
  warnings?: string;   // nonfatal stderr on success
  page?:   string | null;     // PHP-render failure context, when available
  where?:  string;     // PHP source location, when available
}

Setup and filesystem failures can reject before a result exists; PHP failures usually return success: false. Check both channels. Files may already have been copied to the host before a later error; operations are not transactional. The runtime returns debug/stderr, not the older declared response field.

Directory rendering selects _*.php files only when every directory between kirigami.root and the page has a name without a leading underscore. Sitemap selection uses the same rule. Direct requests for private pages fail; rendering a private directory produces no pages. The configured source root itself may start with _ (for example _src). Previously generated private HTML is not deleted by this selection rule; remove stale outputs when migrating a site.

sitemap()

Generate sitemap.xml and robots.txt at the source root, plus humans.txt when author configuration provides content. It accepts no directory argument.

const result = await sitemap();
// With kirigami.root: src, files includes src/sitemap.xml and src/robots.txt.

runenv(script, paths?, ...args)

Run an arbitrary PHP script — not a page template — inside the very same sandboxed WASM environment used for render(), with the full php-prepros class library autoloaded and kirigami.yaml's kirigami block available as PREPROS::$config->data. Useful for one-off maintenance scripts, data migrations, or CLI-style tooling that needs CACHE, SCRAPER, IMG, etc. without going through the page-rendering pipeline.

// Run a standalone PHP script
const purgeResult = await runenv('scripts/purge-cache.php');

// Also mount explicit extra files into the sandbox before running
const imageResult = await runenv('scripts/build-og-images.php', ['assets/photos/hero.jpg']);

// Extra arguments are appended and available as $argv[2], $argv[3], … in the script
const importResult = await runenv('scripts/import.php', [], '--force');
  • script — path to a PHP file inside the project, executed with require_once.
  • paths — optional array of explicit local file paths, not directories. Missing files are skipped; a directory can cause a filesystem rejection. Use mountPath() first for recursive directory mounting.
  • ...args — extra string arguments appended to the script's $argv.

Script and extra-file paths resolve against the project captured at import. Before initializing PHP or copying files, runenv() rejects paths outside that project, including symbolic links whose real targets are outside it. Directories are rejected; missing optional files are skipped. This check does not make PHP scripts untrusted-code sandboxes or restrict explicit mountPath() calls.

Returns Promise<PreprosResult>, following the same shape as render(). Inside the script, call PREPROS::exportFile() for any file you want listed in result.files.

runPluginScript(script, pluginRoot, paths?, ...args)

Same as runenv(), for a script shipped inside a plugin package. A plugin installed with npm link or from a workspace lives outside the project, so runenv() would reject it. Here the script's authored and real paths must stay inside pluginRoot instead, and it is mounted under /plugin-scripts/<package dir>/. Extra paths are still project files. The caller vouches for pluginRoot: @kirigami/kirigami only passes the resolved package directory of an active plugin.

mountPath(localPath, virtualDir?, php?)

The JavaScript-side counterpart to PREPROS::mount(). Mounts a local file or directory — recursively, preserving structure — into the WASM sandbox's virtual filesystem, ahead of (or between) calls to render(), sitemap(), or runenv(). Useful when a Node-side build step needs to make extra local files visible to PHP before rendering starts.

import { mountPath, render } from '@kirigami/php-prepros';

// Mount a single file at its natural virtual path (/project/<relative path>)
await mountPath('assets/data/team.yaml');

// Mount a whole directory, at a custom virtual path
await mountPath('vendor/fonts', '/project/fonts');

await render();
  • localPath — path to a local file or directory. Relative paths are resolved against the project root.
  • virtualDir — optional destination path inside the WASM filesystem. Defaults to /project/<localPath relative to the project root> when omitted.
  • php — optional WASM PHP instance to mount into. Defaults to PHP-prepros's owned instance (the same one used internally by render()/sitemap()/runenv()), creating it if needed. This is separate from PHP-WASM's shared getter instances and is replaced after resetRuntime().

Mounting a directory only copies files whose extension is one of the defaults (.php, .json, .yaml, .yml, .md, .db, .txt) or listed in prepros.mountext, same as automatic root mounting. Mounting a single file directly copies it regardless of extension — this is the simplest way to make an arbitrary asset (an image, a font, a CSV, …) available to PHP without adding its extension to prepros.mountext project-wide.

Returns Promise<void>.

processImages(jobs)

Run a batch of image jobs — resize/encode, or palette extraction — through the IMG class (GD, with the Imagick fallback). This is the engine @kirigami/kirigami's sass task uses for its img-asset() and colors() functions, so Sass, IMG::asset() and the <img asset> tag all share one implementation, one image: config and one set of output filenames — with no native image dependency.

import { processImages } from '@kirigami/php-prepros';

const { files, colors } = await processImages([
  // resize/encode `hero.jpg` (resolved against image.source) to each dest —
  // absolute virtual paths, already carrying the target extension
  { op: 'resize', src: 'hero.jpg', width: 1200, height: 0, cover: false, quality: 82,
    dests: ['/project/src/images/hero-1200w.webp'] },

  // extract a 5-colour palette (cached in .cache.db); returned, not written
  { op: 'palette', src: 'hero.jpg', count: 5 },
]);

// files includes 'src/images/hero-1200w.webp' and may include '.cache.db'.
// colors → { 'hero.jpg:5': ['#1e3a5f', '#c8a24b', …] }
  • jobs — array of resize / palette jobs (see the shape above). An empty array is a no-op and does not start the WASM runtime.
  • Omitted jobs also returns { success: true, files: [], colors: {} } without starting PHP. Non-array input currently does the same; this is not strict input validation.
  • Staleness is the caller's responsibility: every resize job listed is executed.
  • resize: width/height default to 0 (preserve size when both are zero), cover to false, and lossy encoder quality to 82. dests contains absolute /project/... output paths. palette defaults count to 5.
  • The PHP worker handles palette explicitly and treats any other op as a resize; pass only the two documented operations. Processing stops at the first exception. Always check success before using files or colors.

Returns Promise<PreprosResult & { colors: Record<string, string[]> }>.

resetRuntime()

Returns Promise<void>. Queued after preceding operations, it disposes the owned runtime, mounts, and cached configuration. It does not change the project path captured at import, clear disk caches, or reset the SDK hook registry.

TypeScript declarations

The shipped index.d.ts includes render(file?, phpIncludes?), argument-free sitemap(), explicit-file runenv() mounts, and the current diagnostic fields. PreprosResult.files is optional because response parsing can fail before a file list exists. ImageBatchResult.files is always normalized to an array. The obsolete response field is replaced by debug and stderr.


PHP classes reference

All classes are autoloaded — no manual require needed inside your page files. The autoloader itself, $argv/$config, procedural aliases, and the boot hook are installed via php.ini's auto_prepend_file (pointed at utils.inc.php), set once per WASM runtime instance — every entrypoint (prepros.php, runenv.php, imagebatch.php) gets it automatically, with no include of its own.


PREPROS

The core engine. Manages the rendering pipeline, tag processing, hooks, mounting, and file export.

// Available inside page templates and included files.
PREPROS::$config          // stdClass — full resolved config; ->data is the kirigami: block,
                          // ->image the image: block, plus before/after/format/… from prepros:
PREPROS::registerTag(string $tag, callable $callback)
PREPROS::registerHook(string $hook, callable $callback)
PREPROS::runHook(string $hook, mixed $data = null)  // fire a hook (built-in or your own), returns the piped $data
PREPROS::mount(string|array $patterns)
PREPROS::exportFile(string|array $absolutePath)
PREPROS::getExportedFiles(): string[]
PREPROS::fstat(string $path)          // stat a file in the WASM FS (or false)
PREPROS::backtraceFile()              // path of the page currently rendering

PREPROS::render(string $file)

Internal method called once per source file. Orchestrates the full pipeline:

  1. Resolves PHPDOC metadata and auto-loads data files.
  2. Fires the pre_render hook with the raw source contents.
  3. Includes before.php (wrapped in the pre_before / post_before hooks) and the page body (or @content, see above).
  4. If the page declares @type <name> and prepros.types.<name> exists, wraps the body with that type's before/after (wrapped in pre_type_before / post_type_before and pre_type_after / post_type_after) — nested inside the global wrap.
  5. Includes after.php (wrapped in pre_after / post_after), assembling everything into a single string.
  6. Processes all registered custom HTML tags.
  7. Fires the post_render hook on the assembled HTML.
  8. Optionally pretty-prints via HTML::format() (when format: true).
  9. Writes the output .html file.

PREPROS::sitemap()

Scans the source tree for _index.php files and generates a standards-compliant sitemap.xml (Sitemaps 0.9), using kirigami.baseurl as the root URL.

PREPROS::mount(string|array $patterns)

Mounts additional local project files into the WASM virtual filesystem, on demand, from one or more glob patterns evaluated against the project root (via picomatch). Unlike the automatic mounting done for kirigami.root (limited to .php, .json, .yaml, .yml, .md, .db, .txt, and prepros.mountext), mount() copies any matching file, regardless of extension.

// Mount every .webp under assets/, wherever the page needs them
PREPROS::mount('assets/**/*.webp');

// Multiple patterns at once
PREPROS::mount(['data/**/*.csv', 'vendor/fonts/*.woff2']);

Returns an array of the virtual paths (under /project/...) that were mounted, or false on failure.

PREPROS::exportFile(string $file)

Marks a file as a build output so it gets surfaced in PreprosResult.files. Called automatically by render(), sitemap(), CACHE::set(), and CURL. Call it manually if your custom code writes additional files.


MD

Markdown-to-HTML converter with a plugin system for custom shortcodes, backed by PHP's native mdhtml extension (real cmark-gfm), statically built into @kirigami/php-wasm — no userland parsing.

$html = MD::toHtml(string $markdown): string;

Supports the full GitHub Flavored Markdown subset, plus a few extensions:

  • ATX (#######) and Setext headings, with auto-generated id attributes
  • Ordered and unordered lists, including nested
  • GFM task lists (- [ ] / - [x])
  • GFM tables with column alignment
  • GFM alerts (> [!NOTE], > [!WARNING], etc.)
  • Blockquotes (recursive)
  • Fenced code blocks with language class
  • Inline code
  • Bold, italic, bold+italic, strikethrough
  • Links with automatic target="_blank" rel="noopener noreferrer" for external URLs
  • Images with loading="lazy"
  • Auto-linked bare URLs
  • Horizontal rules
  • Hard line breaks (trailing double space → <br>)
  • Footnotes[^1] references and [^1]: … definitions (multi-paragraph)
  • Definition listsTerm / : Definition
  • Emoji shortcodes:rocket: → 🚀, from a built-in map (see MD::registerEmoji())
  • Sanitized inline HTML — raw tags are filtered against an allowlist of tags and attributes, not passed through verbatim

Plugin API

Extend Markdown with custom shortcode tags:

// Inline tag  {% tagname arg1 "arg with spaces" %}
// Block tag   {% tagname arg1
//             body content
//             %}

MD::registerPlugin(string $name, callable $callback): void
MD::unregisterPlugin(string $name): void
MD::getRegisteredPlugins(): string[]
MD::registerEmoji(string $shortcode, string $char): void   // `:name:` → char

The callback always receives (array $args, string $body):

MD::registerPlugin('video', function (array $args, string $body): string {
    $src = htmlspecialchars($args[0] ?? '', ENT_QUOTES, 'UTF-8');
    return "<video src=\"{$src}\" controls></video>";
});

Then in any Markdown content (including inside <markdown> tags):

{% video /videos/intro.mp4 %}

HTML

Pretty-printer for the final HTML output. Used automatically when format: true is set in the config.

$formatted = HTML::format(string $html): string;

Uses PHP 8.4's Dom\HTMLDocument (Lexbor engine) to parse the input and re-serialize it with consistent 4-space indentation. Inline elements, <script>, and <style> blocks are handled correctly — their content is indented but not reformatted. A <pre><code> block is shifted to its nesting depth too (relative indentation kept), and the leading run is stripped again before display; a bare <pre> and <textarea> stay byte-for-byte. Boolean HTML5 attributes (muted, autoplay, noopener, etc.) are written without a value.


YAML

PHP data files use the native yaml extension backed by LibYAML. Its YAML 1.1 implicit booleans include unquoted y, n, yes, no, on, off, true, and false, including mapping keys. Quote these words when you mean strings (for example, "NO": Norway). YAML::parse() / parseFile() / loadFile() preserve the wrapper’s array/object choice; native yaml_parse() / yaml_parse_file() have their own extension signatures. yaml_load_file() remains a wrapper alias. The project’s kirigami.yaml is parsed separately in Node through struct-walker and js-yaml.

A YAML parser backed by PHP's native yaml extension (libyaml), statically built into @kirigami/php-wasm — full YAML 1.1 support, no userland parsing.

$data = YAML::parse(string $yaml, bool $assoc = false): mixed;
$data = YAML::parseFile(string $path, bool $assoc = false): mixed;
$data = YAML::loadFile(string $path, bool $assoc = false): mixed;

By default, YAML mappings are returned as stdClass objects. Pass true as the second argument to get associative arrays instead.

Following YAML 1.1 means the usual implicit-boolean gotcha applies to both values and mapping keys: y/Y/n/N, yes/no, true/false, on/off (any case) all resolve to a boolean when unquoted — so an unquoted no: key or a NO value becomes false. Quote a scalar ("y": 2) to keep it a string.

YAML::loadFile() behaves like YAML::parseFile(), then walks the result recursively: any string value ending in .yaml, .yml, or .json that resolves to an existing file (relative to its own file's directory) is replaced by that file's parsed content, and so on, recursively. Values that don't match an existing file are left untouched. Circular references (A → B → A) throw a RuntimeException.

# team.yaml
lead: people/jane.yaml     # resolved and inlined automatically
members:
  - people/jane.yaml
  - people/john.yaml
$team = YAML::loadFile('/project/data/team.yaml');
// $team->lead is now the fully parsed content of people/jane.yaml, not a string

SCHEMA

A JSON Schema validator with an Ajv-like API, backed by the native jsonk extension built into @kirigami/php-wasm. Available to your own code and plugins.

$validator = new SCHEMA(array $schema);

$validator->isValid(mixed $data): bool     // true / false
$validator->validate(mixed $data): bool    // alias of isValid()
$validator->getErrors(): string[]          // "path: message" strings from the last run

jsonk implements draft 2020-12 for a self-contained schema: every validation keyword (type, enum, const, required, properties, patternProperties, additionalProperties, propertyNames, dependentRequired, dependentSchemas, items, prefixItems, contains, uniqueItems, the min*/max* and exclusive* bounds, multipleOf, pattern, format, if/then/else, allOf/anyOf/oneOf/not) and $ref to #/$defs/… / #/definitions/…, absolute URLs, or URLs relative to the schema's $id. See php-jsonk for the details and limits.

Schemas are PHP arrays, so SCHEMA adapts them before handing them to jsonk: an empty array in a schema position ('properties' => []) is treated as {}, draft-07 tuple items (a list of schemas) becomes prefixItems (and additionalItems becomes items), and format: url is read as uri. Error paths look like (root), name or tags[1].

The previous pure-PHP (Draft-7 style) validator is still available as SCHEMA_LEGACY, with the same API.

$validator = new SCHEMA([
    'type'     => 'object',
    'required' => ['name', 'age'],
    'properties' => [
        'name' => ['type' => 'string', 'minLength' => 1],
        'age'  => ['type' => 'integer', 'minimum' => 0],
    ],
    'additionalProperties' => false,
]);

if (!$validator->isValid($data)) {
    foreach ($validator->getErrors() as $err) echo $err, PHP_EOL;
}

LD

A schema.org JSON-LD generator. LD accumulates structured-data nodes for the page under render and emits them as one <script type="application/ld+json"> block — with an @graph when there is more than one node — in the <head>.

Automatic mode

On as soon as kirigami.yaml has a seo: block (seo: {} is enough), alongside META's tags and from the same keys. A post_render hook injects a graph built from seo:, the loose keys of the kirigami block, and the current page's PHPDOC:

  • an Organization node (@id #organization) — name/url/description from project/baseurl/description, sameAs gathered from every recognised social-network URL key (facebook, instagram, linkedin, github, youtube, mastodon, …), plus email, telephone, areaServed (← area), knowsAbout (← knowsabout), address, logo, and founder → the Person node when there is one. @type comes from seo.type;
  • a Person node (#person) when person is set — name + jobTitle (← jobtitle) + email + url, linked to the Organization via worksFor;
  • a WebSite node (#website) — publisher → Organization, inLanguage, keywords (← keywords), and a SearchAction when seo.search is set;
  • a WebPage node for the page — see the per-page tags below;
  • a BreadcrumbList for every non-home page, derived from the _index.php ancestor trail (home → each parent section → this page). No @breadcrumb opt-in needed. Disable it for one page with @ld_breadcrumb false.

seo: { jsonld: false } stops the automatic pass; META's tags keep working. A page whose rendered <head> already contains an application/ld+json script is never touched, so hand-rolled markup keeps working.

Per-page PHPDOC tags — these feed the page node (and override the generic @title / @description / @datePublished fallbacks):

| Tag | Effect | |-----|--------| | @ld false | Skip JSON-LD for this page entirely (@ld_ignore true also works). | | @ld_type <Type> | @type of th