freestruct 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,15 +2,9 @@
2
2
 
3
3
  **The post-build SEO layer for static doc sites.**
4
4
 
5
- Build your docs with any SSG - Jekyll, Hugo, Docusaurus, MkDocs, VitePress, Astro, Eleventy.
6
- Then run Freestruct over the output: complete SEO metadata (title, description, canonical,
7
- Open Graph, Twitter, JSON-LD), `sitemap.xml` with real per-page `lastmod`, `robots.txt`
8
- with the right sitemap pointer, an RSS `feed.xml` with honest per-page dates, a `404.html`
9
- that carries noindex, cache-busted assets, and internal-link validation. One dependency
10
- (`js-yaml`), no SSG plugin, no template edits.
5
+ Build your docs with any SSG - Jekyll, Hugo, Docusaurus, MkDocs, VitePress, Astro, Eleventy. Then run Freestruct over the output: complete SEO metadata (title, description, canonical, Open Graph, Twitter, JSON-LD), `sitemap.xml` with real per-page `lastmod`, `robots.txt` with the right sitemap pointer, an RSS `feed.xml` with honest per-page dates, a `404.html` that carries noindex, cache-busted assets, and internal-link validation. One dependency (`js-yaml`), no SSG plugin, no template edits.
11
6
 
12
- Part of the [Creadev](https://creadev.org) doc-system family - the same docs frame and
13
- engineering conventions as its siblings (see Project family below).
7
+ Part of the [Creadev](https://creadev.org) doc-system family - the same docs frame and engineering conventions as its siblings (see Project family below).
14
8
 
15
9
  ---
16
10
 
@@ -26,8 +20,7 @@ npm install --save-dev freestruct
26
20
 
27
21
  ### 2. Create the config
28
22
 
29
- Create `docs/freestruct.config.yml` next to your docs folder (the legacy name
30
- `docs/ssr-config.yml` is still read; discovery walks upward from your cwd):
23
+ Create `docs/freestruct.config.yml` next to your docs folder (the legacy name `docs/ssr-config.yml` is still read; discovery walks upward from your cwd):
31
24
 
32
25
  ```yaml
33
26
  # docs/freestruct.config.yml
@@ -41,9 +34,7 @@ og:
41
34
  image: /og-default.png
42
35
  ```
43
36
 
44
- `site.name` and `site.url` are required; everything else has a working default
45
- (the full reference is the [Configuration](#configuration) table below, and the
46
- [config guide](https://freestruct.creadev.org/guides/freestruct-config) goes key by key).
37
+ `site.name` and `site.url` are required; everything else has a working default (the full reference is the [Configuration](#configuration) table below, and the [config guide](https://freestruct.creadev.org/guides/freestruct-config) goes key by key).
47
38
 
48
39
  ### 3. Build, then inject
49
40
 
@@ -79,10 +70,7 @@ grep -o 'freestruct-build' docs/_site/index.html
79
70
  ls docs/_site/{sitemap.xml,robots.txt,feed.xml,404.html}
80
71
  ```
81
72
 
82
- If any of those fail, Freestruct tells you why: config errors, a missing
83
- template, and an empty output dir each fail loudly with their own exit code
84
- (`1` config, `2` template, `3` output dir; `--tolerant` downgrades fatals to
85
- exit 0 for CI tolerance).
73
+ If any of those fail, Freestruct tells you why: config errors, a missing template, an empty output dir, and a failing `hooks.postBuild` command each fail loudly with their own exit code (`1` config, `2` template, `3` output dir, `4` hook failure; `--tolerant` downgrades fatals to exit 0 for CI tolerance).
86
74
 
87
75
  ### Useful flags and subcommands
88
76
 
@@ -97,6 +85,11 @@ npx freestruct validate # config-only preflight; exit 1 on fatal errors
97
85
  npx freestruct init # scaffold the starter kit into ./docs
98
86
  # (npx freestruct init mydir -> mydir/docs;
99
87
  # existing files are never clobbered)
88
+ npx freestruct audit # verify the baked output (heads + sitemap/
89
+ # robots/404-noindex); --json for CI digest;
90
+ # --external also probes up to 50 unique
91
+ # external hrefs (warn-grade, needs network);
92
+ # exit 1 on FAIL findings (issue #18)
100
93
  ```
101
94
 
102
95
  ---
@@ -105,36 +98,26 @@ npx freestruct init # scaffold the starter kit into ./docs
105
98
 
106
99
  ### SEO
107
100
 
108
- - Injects missing title, description, canonical, Open Graph, Twitter cards, and
109
- JSON-LD (missing-only: existing tags are left alone)
110
- - Attribute-safe everywhere: og/twitter values are HTML-escaped, JSON-LD
111
- literals are JSON-escaped (a raw `<` cannot close the script tag),
112
- `og:image`/`twitter:image` ship only when an image is configured, and an
113
- optional `author` key rides the Organization JSON-LD as a schema.org founder
114
- - Page-level tuning survives the build: a `<!-- freestruct: {...} -->` comment
115
- overrides title/description/ogType/ogImage/twitterCard and adds
116
- `article:published_time`/`author`/`section`; custom `jsonLd` objects append
117
- structured data
118
- - Honest canonical URLs including sub-path (`basePath`) installs - the same
119
- contract holds for the `sitemap.xml` locs, `robots.txt` sitemap pointer, the
120
- RSS channel/self links, and the generated 404's home link
101
+ - Injects missing title, description, canonical, Open Graph, Twitter cards, and JSON-LD (missing-only: existing tags are left alone)
102
+ - Feed autodiscovery: every injected head carries the `rel="alternate" type="application/rss+xml"` link (a page-owned one wins, missing-only) so feed readers find feed.xml without knowing the URL
103
+ - Attribute-safe everywhere: og/twitter values are HTML-escaped, JSON-LD literals are JSON-escaped (a raw `<` cannot close the script tag), `og:image`/`twitter:image` ship only when an image is configured, and an optional `author` key rides the Organization JSON-LD as a schema.org founder
104
+ - `search`, `breadcrumb`, `article`, `verification` and `locales`: the SearchAction target for the WebSite JSON-LD (no more hardcoded `?s=` default), auto BreadcrumbList from the URL tree, scoped per-page `Article`/`TechArticle`/`NewsArticle` blocks, provider-keyed site verification metas, and hreflang stem-swap alternates for multilingual trees - all opt-in, missing-only, and loudly diagnostic (see the Discovery Options in the config reference)
105
+ - `llms: true` emits the AI-consumable `llms.txt` summary over the same indexable page set sitemap and feed honor (noindex stays honored; an existing file is never overwritten)
106
+ - `og.image` takes the back-compat string form or the structured `{ src, width, height, alt }` object form, which also emits the `og:image:width`/`height`/`alt` companions; SVG sources warn loudly on every run (most scrapers cannot render them), `allowSvg: true` accepts one explicitly, and `--strict` fails the build until it is answered
107
+ - Page-level tuning survives the build: a `<!-- freestruct: {...} -->` comment overrides title/description/ogType/ogImage/twitterCard and adds `article:published_time`/`author`/`section`; custom `jsonLd` objects append structured data
108
+ - Honest canonical URLs including sub-path (`basePath`) installs - the same contract holds for the `sitemap.xml` locs, `robots.txt` sitemap pointer, the RSS channel/self links, and the generated 404's home link
121
109
  - Tagged with a `freestruct-build` marker comment
122
110
 
123
111
  ### Search
124
112
 
125
- - **Opt-in** `search.json` (set `searchIndex: true`); default runs generate
126
- nothing - bring your own indexer (Pagefind/Algolia), the search guide covers
127
- the wiring
113
+ - **Opt-in** `search.json` (set `searchIndex: true`); default runs generate nothing - bring your own indexer (Pagefind/Algolia), the search guide covers the wiring
128
114
 
129
115
  ### Sitemap, robots, feed & 404
130
116
 
131
- - `sitemap.xml`: encoded URLs, real per-file `lastmod` (page mtime, not build
132
- time), noindex pages excluded
117
+ - `sitemap.xml`: encoded URLs, real per-file `lastmod` (page mtime, not build time), noindex pages excluded
133
118
  - `robots.txt`: `User-agent: *` + `Allow: /` + a basePath-aware sitemap pointer
134
- - `feed.xml` (RSS): escaped titles/descriptions, per-page `pubDate` from each
135
- page's own mtime, basePath-aware channel/self links
136
- - `404.html`: generated when missing, gets the full SEO pipeline + explicit
137
- noindex; a user-supplied 404 gets the same treatment
119
+ - `feed.xml` (RSS): escaped titles/descriptions, per-page `pubDate` from each page's own mtime, basePath-aware channel/self links
120
+ - `404.html`: generated when missing, gets the full SEO pipeline + explicit noindex; a user-supplied 404 gets the same treatment
138
121
 
139
122
  ### Performance
140
123
 
@@ -143,12 +126,13 @@ npx freestruct init # scaffold the starter kit into ./docs
143
126
 
144
127
  ### DX
145
128
 
146
- - Config schema with loud `[freestruct] config warning` diagnostics; unknown
147
- and retired keys never die silently
148
- - Link validation (report-only by default, `--strict` to make fatal - our own
149
- docs CI runs `--strict` and it has already caught three production 404s)
150
- - Post-render hooks with purge-command safety (disabled by default; see
151
- [Security](#security))
129
+ - Config schema with loud `[freestruct] config warning` diagnostics; unknown and retired keys never die silently
130
+ - Link validation (report-only by default, `--strict` to make fatal - our own docs CI runs `--strict` so a broken internal link fails the Pages build before it ships)
131
+ - `freestruct audit` verifies the baked output independently - per-page head hygiene, the 404 noindex contract, sitemap/robots sanity - with an exit code for CI (`0` clean, `1` findings, `--json` digest for annotations); it reads only the output dir, so it checks any SSG's build with freestruct applied
132
+ - IndexNow support (issue #19): freestruct is the component that already owns the sitemap, so the key file (the keyLocation ownership proof) ships into the output during the run and `freestruct indexnow` POSTs the baked URL list to api.indexnow.org afterwards - deploy first (keyLocation must resolve), submission is the post-deploy step, and `--dry-run` prints the request body for CI
133
+ - Slug-glob scoped config (issue #25): `jsonLd` accepts the legacy object/list forms or a map keyed by page-slug globs (`/` exact, `/blog/*` one segment, `/guides/**` a sub-tree) so page-specific schema lands only where it is meant to; `og.scoped` does the same for og image variants with the most specific match winning. Missing-only holds everywhere, malformed entries warn loudly and skip only themselves
134
+ - Per-page OG image generation (issue #22): freestruct already extracts each page's title/description, so it can render a branded 1200x630 PNG per page from a consumer SVG template (`{{ogTitle}}`/`{{ogDescription}}`/ `{{ogUrl}}` placeholders) and wire og/twitter to it - missing-only keeps pages with their own og set. Rasterization is honest: PNG needs a real rasterizer, so the optional `@resvg/resvg-js` package is loaded lazily and the run stays green with a loud skip when it is not installed
135
+ - Post-render hooks with purge-command safety (disabled by default; see [Security](#security))
152
136
 
153
137
  ---
154
138
 
@@ -172,22 +156,19 @@ Full key list (unknown or retired keys produce warnings, not silence):
172
156
  | `readingTime` | `true` | Inject `<meta name="reading-time">` |
173
157
  | `lastModified` | `true` | Inject `<meta name="last-modified">` |
174
158
  | `lazyLoad` | `true` | Add `loading="lazy"` to images |
159
+ | `tableWrap` | `true` | Wrap wide `<table>`s in a scroll container so they never pin the page width (mobile-safe by construction) |
175
160
  | `linkCheck` | `true` | Validate internal links (report-only; `--strict` makes them fatal) |
176
161
  | `searchIndex` | `false` | **Opt-in**: generate `search.json` for client-side search |
177
162
  | `minify` | `false` | Experimental HTML minifier (preserves `pre`/`script`/`style`/`textarea`) |
178
- | `runHooks` | `false` | Gateway: only `true` lets `cacheBusting.purge` commands execute |
163
+ | `runHooks` | `false` | Gateway: only `true` lets `cacheBusting.purge` and `hooks.postBuild` commands execute |
164
+ | `hooks.postBuild` | *(unset)* | Post-build command list chaining after the pass; disabled without `runHooks: true`; nonzero exit fails the build (exit 4) |
165
+ | `indexNow` | *(unset)* | IndexNow notify support: `true` (generate-once-and-persist key) or `{ key: "<32-hex>" }`; ships `<key>.txt` into the output and enables the `freestruct indexnow` post-deploy submission |
166
+ | `ogImageGeneration` | *(unset)* | Per-page OG image generation (`{ template, font?, width?, height? }`): renders a branded 1200x630 PNG per page from a consumer SVG template; needs the optional `@resvg/resvg-js` dev package (loud skip without it) |
179
167
  | `cacheBusting.assetQueryParam` | `true` | Append `?v={buildHash}` to linked assets |
180
168
  | `cacheBusting.hashInCanonicalUrl` | `false` | Append `?v={buildHash}` to canonical URLs |
181
169
  | `cacheBusting.purge` | *(unset)* | Purge hook list (name + command); disabled without `runHooks: true` |
182
170
 
183
- The docs ([freestruct.creadev.org](https://freestruct.creadev.org)) carry the
184
- key-by-key guides: the [config reference](https://freestruct.creadev.org/guides/freestruct-config),
185
- [cache busting](https://freestruct.creadev.org/guides/cache-busting), [custom
186
- injection](https://freestruct.creadev.org/guides/custom-injection),
187
- [CLI reference](https://freestruct.creadev.org/guides/cli),
188
- [post-render hooks](https://freestruct.creadev.org/guides/post-render-hooks),
189
- [CI/CD](https://freestruct.creadev.org/guides/cicd) and
190
- [troubleshooting](https://freestruct.creadev.org/guides/troubleshooting).
171
+ The docs ([freestruct.creadev.org](https://freestruct.creadev.org)) carry the key-by-key guides: the [config reference](https://freestruct.creadev.org/guides/freestruct-config), [cache busting](https://freestruct.creadev.org/guides/cache-busting), [custom injection](https://freestruct.creadev.org/guides/custom-injection), [CLI reference](https://freestruct.creadev.org/guides/cli), [post-render hooks](https://freestruct.creadev.org/guides/post-render-hooks), [CI/CD](https://freestruct.creadev.org/guides/cicd) and [troubleshooting](https://freestruct.creadev.org/guides/troubleshooting).
191
172
 
192
173
  ---
193
174
 
@@ -204,8 +185,20 @@ cacheBusting:
204
185
  command: "cf-cli purge $SITE_URL"
205
186
  ```
206
187
 
207
- Use only trusted commands. Disabled by default. Available variables:
208
- `$SITE_URL`, `$BUILD_HASH`, `$OUTPUT_DIR`.
188
+ Use only trusted commands. Disabled by default. Available variables: `$SITE_URL`, `$BUILD_HASH`, `$OUTPUT_DIR` (as env vars on the child: `FREESTRUCT_SITE_URL`, `FREESTRUCT_BUILD_HASH`, `FREESTRUCT_OUTPUT_DIR`).
189
+
190
+ The same gateway covers the general postBuild seam - commands that chain after the entire pass (custom schema injection, audits, IndexNow submissions). A nonzero hook exit is a build failure (exit code 4):
191
+
192
+ ```yaml
193
+ # In freestruct.config.yml
194
+ runHooks: true
195
+ hooks:
196
+ postBuild:
197
+ - node scripts/faq-schema.mjs
198
+ - npm run seo:audit
199
+ ```
200
+
201
+ Purge errors are reported and the build continues (a failed CDN purge is not a bad sitemap); postBuild failures fail the build on purpose - chain only trusted commands there too.
209
202
 
210
203
  ---
211
204
 
@@ -213,82 +206,105 @@ Use only trusted commands. Disabled by default. Available variables:
213
206
 
214
207
  Two shipped surfaces beyond the engine:
215
208
 
216
- - **`init/`** - the consumer scaffold kit: a commented starter config,
217
- template copies, and a README ("copy + go"). Version-stamped on
218
- `package.json`; `npx freestruct` resolves templates user-staged first,
219
- package fallback second.
220
- - **`packs/`** - Freestruct's [crashtest](https://crashtest.creadev.org) pack
221
- surface: `freestruct-docs-shapes` attacks a built docs tree the way a
222
- static host serves it (traversal refusal, 404 noindex, noindex artifact
223
- leaks, canonical query poisoning, XML shape). See
224
- [packs/README.md](packs/README.md). The docs CI runs it against every
225
- Pages build.
209
+ - **`init/`** - the consumer scaffold kit: a commented starter config, template copies, and a README ("copy + go"). Version-stamped on `package.json`; `npx freestruct` resolves templates user-staged first, package fallback second.
210
+ - **`packs/`** - Freestruct's [crashtest](https://crashtest.creadev.org) pack surface: `freestruct-docs-shapes` attacks a built docs tree the way a static host serves it (traversal refusal, 404 noindex, noindex artifact leaks, canonical query poisoning, XML shape). See [packs/README.md](packs/README.md). The docs CI runs it against every Pages build.
226
211
 
227
212
  ---
228
213
 
229
214
  ## Releases
230
215
 
231
- - Versions live in one place: `package.json`, pinned in CI against the
232
- README/CHANGELOG truth. Single source, no drift.
233
- - `CHANGELOG.md` is the changelog; releases are tagged (`v*`) and
234
- CHANGELOG-truthful or they don't ship.
235
- - Current release: **1.0.0** (the owner's version call, 2026-10-22 - the
236
- first stable release; see `labs/TASKS.md` for the version policy).
237
- - Deployment truth lives in [DEPLOY.md](DEPLOY.md): the GitHub Pages pipeline
238
- (this repo's own docs site deploys through it) and the npm release runbook.
216
+ - Versions live in one place: `package.json`, pinned in CI against the README/CHANGELOG truth. Single source, no drift.
217
+ - `CHANGELOG.md` is the changelog; releases are tagged (`v*`) and CHANGELOG-truthful or they don't ship.
218
+ - Current release: **1.2.0** (everything since the v1.1.0 tag: the family sweep, the docs/tooling wave, the post-queue engine sweep, and the issue #41 version-truth census leg; see `labs/TASKS.md` for the version policy).
219
+ - Deployment truth lives in [DEPLOY.md](DEPLOY.md): the GitHub Pages pipeline (this repo's own docs site deploys through it) and the npm release runbook.
220
+
221
+ ## v1.2.0
222
+
223
+ Everything since the v1.1.0 tag: the family sweep and the docs/tooling wave, then the post-queue engine sweep - with the GitHub issue queue fully closed (#1-#40), the engine corners the queue-era waves never probed got nine real fixes, each with a battery pin (121 -> 130), plus the issue #41 version-truth census leg (130 -> 132), the issue #43 count census (132 -> 133), the post-merge staging pass (133 -> 134), the repo-wide gap walk (134 -> 160), the prestruct port pass (160 -> 162), and the docs-audit runner convergence (162 -> 163), the independence guard (163 -> 164), the cache-bust same-origin + srcset pass (164 -> 166), and the release-walk pre-tag hardening sweep (166 -> 172):
224
+
225
+ - Rerun hygiene - body-start/footer snippets are marker-wrapped and a rerun drops the previous copy before re-adding it, so a second pass no longer stacks snippets or churns the content-derived build hash
226
+ - Verbatim-block-aware `minifyHtml` - `<pre class="...">`/`<script>`/`<style>`/`<textarea>` content (attribute forms included) survives byte-for-byte, so code fences and inline JS with `//` comments are never collapsed
227
+ - Cache-bust fix - asset extensions match as a case-insensitive SET, so `.cs`/`.tt`/`.j` are no longer busted and uppercase `.PNG` now is
228
+ - Link checking - single-quoted `href` links are checked too, so `--strict` catches a single-quoted broken link
229
+ - Purge hooks - `FREESTRUCT_SITE_URL` / `FREESTRUCT_BUILD_HASH` / `FREESTRUCT_OUTPUT_DIR` now reach the child, matching `hooks.postBuild` and the docs
230
+ - Lazy-load fix - self-closing `<img ... />` stays well-formed instead of getting a dangling `/ loading="lazy"`
231
+ - Audit fixes - `grabMeta` tolerates a leading attribute (no more false `missing meta description`), and `urlToDisk` normalizes a slash-less `basePath`
232
+ - Audit quote fix - `grabMeta`/`grabCanonical` read a quoted attribute up to its matching quote, so a description holding an apostrophe ("this repo's docs") is measured at full length instead of truncated at the apostrophe
233
+ - Docs h1 gate - the docs audit now fails any living page without exactly one top-level `# ` heading; seven pages that opened on `##` or prose (a rig-adoption regression) gained their missing h1
234
+ - CLI - `-h` now aliases `--help` instead of riding as the positional output dir
235
+ - Version truth - a census leg derives the version from `package.json` and refuses every living surface that hand-copied the current one (README line, AGENTS/CONTRIBUTING/DEPLOY pins, the pass-log reminder, the `release.yml` tag example, `init/VERSION`)
236
+ - Count census - the same trick for the hand-copied counts: the homepage battery size and the engine module counts are derived from source and refused when they drift (issue #43)
237
+ - Doc truthfulness - the cache-busting FAQ/config reference drop the retired `cacheBusting.hash` and the canonical-by-default claim, and new environment + packs reference pages fill the gaps
238
+ - basePath links - root-absolute hrefs carrying the configured `basePath` resolve against the tree, so a baseurl'd site passes `--strict`
239
+ - Release gate - `npm run gate` composes template sync lockstep + the battery + docs audit + claims gate + pack selftest, and `prepublishOnly` runs it, so a drifted or dirty tree cannot publish
240
+ - Audit signal - config-gated absences (`og:image` / `twitter:image` not configured) print once with an affected-page count instead of once per page
241
+ - Table guard - a default-on `tableWrap` pass wraps wide tables in a scroll container so every downstream site is mobile-safe by construction
242
+ - Init kit - the starter config seeds the family JSON-LD identity, so scaffolded ports ship founder + maintainer from birth
243
+ - Docs - the playbooks leg (one recipe per runbook, indexed and in the nav), the rootless local docs-preview recipe, the full rouge token vocabulary, and the retired 72-column hard-wrap (living prose flows, `scripts/unwrap-prose.cjs` keeps it that way)
244
+ - CI - the Pages runbook promoted to a reusable `workflow_call` lane (`docs-lane.yml`) with `docs.yml` a thin caller, every `actions/cache` step pinned to one major, and the phone-publish lane (`scripts/ship-from-download.cjs`)
245
+ - Entity truth - `extractTitle`/`extractDescription` are attribute-order and quote tolerant and decode HTML entities, so a reordered/single-quoted meta is read (no silent site-default fallback) and the feed no longer double-encodes (`&amp;amp;`), while search.json/llms.txt carry plain text instead of raw entities
246
+ - Minify text fix - a line break inside text collapses to a space instead of a joiner, so `Hello\nworld` no longer fuses into `Helloworld`
247
+ - Cache-bust parity - single-quoted `href`/`src` assets get `?v=` too, matching the link checker's both-quote handling
248
+ - Audit containment - a site-local `og:image`/JSON-LD `logo`/`image` that escapes the output tree is named as escaping instead of being probed off-tree and read back as present
249
+ - Audit noindex exemption - a `robots noindex` page (a redirect stub, a thank-you page) is exempt from the content-quality classes (description length, h1 shape, JSON-LD absence) that grade indexable content, while the structural classes still apply
250
+ - Link check query fix - a query string on an internal link (`/guide?utm_source=x`) is stripped before resolving, so a valid link is no longer reported broken (which failed `--strict` on any site whose nav carries query params)
251
+ - Cache-bust query/fragment fix - an asset emitted with an existing query (`style.css?foo=bar`) or a fragment (`legacy.css#iefix`) is now busted too (the version param joins the query, a previous `v=` is dropped), where before it was silently left stale
252
+ - Cache-bust code protection - asset links SHOWN in a code example (`<pre><code>&lt;link href="/style.css"&gt;</code></pre>`) or inside a script body are left untouched; only live `href`/`src` attributes are busted
253
+ - Purge hook reporting - every configured purge hook (and every `hooks.postBuild` entry) is reported when hooks are disabled, not just the first
254
+ - Table-wrap hash stability - the default-on `tableWrap` pass runs after the build hash is computed, so a rerun saw the wrapper the first run added and churned the content-derived hash (flipping every asset `?v=` and busting CDN caches on an unchanged site); `computeBuildHash` now strips the wrapper before hashing
255
+ - Search index text hygiene - `search.json` `content`/`heading` now carry plain page text: script and style bodies (raw JS/CSS) are dropped and a heading with inline markup (`<h1>Title <em>x</em></h1>`) no longer ships its tags into the index
256
+ - 404 name escaping - the generated `404.html` interpolated the raw `site.name` into `<title>`/text and the raw home URL into `href`/`action`, so a quote- or `<`-bearing name broke the title and injected live markup into the error page; both ride the shared attribute escaper now
257
+ - Reading-time prose count - the `reading-time` meta counted script, style and comment text as page prose (a 600-word inline bundle inflated a five-word page to "4 min read"); both it and the search index now share one `htmlToText` view that drops script/style/comment bodies before tags
258
+ - IndexNow URL decoding - the `indexnow` subcommand read `<loc>` values straight out of the XML-escaped sitemap and POSTed them verbatim, so a URL with `&` (escaped as `&amp;`) was submitted to the engines in a form that does not resolve; the values decode back to the real URL now
259
+ - llms.txt link labels - a page title carrying markdown brackets (`Foo] bar`, a `[preview]` tag) was emitted raw into `- [title](url)`, so the `]` closed the label early and the URL spilled into the prose as broken text; brackets and backslashes escape in the label now
260
+ - Entity decoding - the shared decoder mapped five named entities only, so a smart-quoted title (`&#8217;`, `&mdash;`) stayed literal and the feed double-escaped it (`&amp;#8217;`); decimal, hex and the common named set decode now, and the audit grades title/description length on the decoded text rather than the raw markup
261
+ - IndexNow keyLocation - the submission named `keyLocation` as `site.url + '/' + key.txt`, ignoring `basePath`, so on a sub-path install the engines probed the domain root (a 404) and refused the submission; it now follows the same `pageUrlFor` contract as the sitemap and robots pointer
262
+ - og:image basePath - a site-local `og:image` (and `twitter:image`) joined onto `site.url` alone, so on a sub-path install the share card pointed at the domain root and the preview blanked; the join now rides `pageUrlFor`, matching the audit's own on-disk check (which already stripped the prefix), and a src written without a leading slash no longer produces a slashless URL
263
+ - Case-insensitive attributes - the description branch of the missing-only gate and the internal-link checker's `href` matcher were the only two HTML-attribute regexes in the engine without the `/i` flag, so an upper-case `<META NAME="description">` drew a second description (which the audit then failed as a duplicate the engine caused) and an upper-case `<A HREF="/broken/">` shipped green under `--strict`; both are case-insensitive now
264
+ - Lazy-load guard - the "already has loading" check was `includes('loading=')` (lowercase, no whitespace tolerance), so `<img ... LOADING="eager">` or `loading = "eager"` collected a SECOND `loading` attribute and the author's eager hint was lost; the guard is case- and whitespace-insensitive now
265
+ - 404 noindex on rerun - the 404 pipeline inserted its `noindex` robots meta before `injectPage`, so on the second run the tag sat inside the region the rerun-hygiene strip deletes and was dropped: the build passed on run 1 and its own audit then failed the 404 on run 2. The noindex is now asserted after injection, past the strip, so it survives every rerun
266
+ - Feed/sitemap dates on rerun - feed `pubDate` and sitemap `lastmod` come from each page's pre-injection mtime, but freestruct rewrites every page it decorates, so a second run read its own write back as the page date and the whole feed re-stamped on every redeploy with no content change. The pre-injection mtime is restored after the writes now, so the dates hold across reruns and still advance when the SSG rebuilds the page
267
+ - Staging flow (CI) - `test.yml` and `docs.yml` now verify `staging` as well as `main` (push and pull request), so work is checked where it lands; the reusable `docs-lane.yml`'s deploy job is gated to `main` by ref, so widening the docs trigger cannot publish staging content to production and every caller of the lane inherits the guard; and every promotion fast-forwards `staging` to `main` (`git push origin origin/main:staging`), because a merge-commit promotion drifts the two by one each time
268
+
269
+ ## v1.1.0
270
+
271
+ The first post-stable release, carried by the all-items issue sweep (#17-#25 plus the #25 follow-on) and the CI tooling that came with it:
272
+
273
+ - `freestruct audit` - independent baked-output verification (per-page head hygiene, the 404 noindex contract, sitemap/robots) with `--json` for CI
274
+ - `hooks.postBuild` - the general post-build hook seam (exit code 4, `FREESTRUCT_*` env on hook children)
275
+ - IndexNow - key-file emission + the `freestruct indexnow` post-deploy submission (`--dry-run` for CI)
276
+ - Scoped config - slug-glob map forms for `jsonLd` and `og.scoped`
277
+ - Structured `og.image` - `{ src, width, height, alt, allowSvg }` with the companion tags and the SVG warn/strict ladder
278
+ - Per-page OG-image generation - `ogImageGeneration` through the optional `@resvg/resvg-js` rasterizer
279
+ - Latent hardening - every config/page-carried substitute now rides function replacers ($-expansion cannot corrupt output), plus the docs-layout dead-CSS removal
280
+ - CI - packstruct manifest lint joined the push/PR battery alongside the crashtest pack selftest; the battery grew 49 -> 75 -> 103 pins
239
281
 
240
282
  ## v1.0.0
241
283
 
242
- First stable release. It carries the full rehaul-era stack (accumulated
243
- under the 0.6.0 freeze, promoted truthfully as 0.9.0), the prestruct port
244
- wave, and the toolchain maturity landed on 2026-10-22:
245
-
246
- - Config renamed to `freestruct.config.yml` with legacy read; schema-checked
247
- with loud diagnostics; FAST_FAIL exit codes + `--tolerant`
248
- - Module decomposition: the engine is `engine/` (one orchestrator + eight
249
- modules) with `bin/` shim and shipped templates; `init/` is the consumer
250
- scaffold kit; template copies provably in sync via CI
251
- - `searchIndex` opt-in; noindex honored across sitemap/feed/search/404;
252
- basePath contract across every generated URL (canonical, sitemap, feed,
253
- robots pointer, 404 home link)
254
- - Honest XML: escaped values, per-page `lastmod`, per-page RSS dates;
255
- escaped-or-fail on un-escapable content
256
- - Docs: single-source nav, audit gates wired to CI (style, truthfulness,
257
- English-only language gate, engine comment format), the docs CI itself
258
- runs `--strict` plus the crashtest pack dogfood
284
+ First stable release. It carries the full rehaul-era stack (accumulated under the 0.6.0 freeze, promoted truthfully as 0.9.0), the prestruct port wave, and the toolchain maturity landed on 2026-10-22:
285
+
286
+ - Config renamed to `freestruct.config.yml` with legacy read; schema-checked with loud diagnostics; FAST_FAIL exit codes + `--tolerant`
287
+ - Module decomposition: the engine is `engine/` (one orchestrator + nine modules) with `bin/` shim and shipped templates; `init/` is the consumer scaffold kit; template copies provably in sync via CI
288
+ - `searchIndex` opt-in; noindex honored across sitemap/feed/search/404; basePath contract across every generated URL (canonical, sitemap, feed, robots pointer, 404 home link)
289
+ - Honest XML: escaped values, per-page `lastmod`, per-page RSS dates; escaped-or-fail on un-escapable content
290
+ - Docs: single-source nav, audit gates wired to CI (style, truthfulness, English-only language gate, engine comment format), the docs CI itself runs `--strict` plus the crashtest pack dogfood
259
291
  - Release truth: tarball e2e CI gate + hard publish gate; version pinned
260
- - crashtest pack surface (`packs/public/freestruct-docs-shapes`) with CI
261
- teeth on every push and the Pages build
262
- - Port wave from prestruct: attribute-escaped injection, og/twitter image
263
- parity, `author` + JSON-LD founder, per-page override comments, custom
264
- `jsonLd`, deterministic build hash, CLI subcommands (`validate`/`init`/
265
- `help`), `FREESTRUCT_URL` env override
266
- - Toolchain: Node-24-compatible GH action majors, Jekyll 4.4 + Ruby 3.4,
267
- js-yaml 5.5, packaging validated dry (npm tarball + GitHub source tar)
292
+ - crashtest pack surface (`packs/public/freestruct-docs-shapes`) with CI teeth on every push and the Pages build
293
+ - Port wave from prestruct: attribute-escaped injection, og/twitter image parity, `author` + JSON-LD founder, per-page override comments, custom `jsonLd`, deterministic build hash, CLI subcommands (`validate`/`init`/ `help`), `FREESTRUCT_URL` env override
294
+ - Toolchain: Node-24-compatible GH action majors, Jekyll 4.4 + Ruby 3.4, js-yaml 5.5, packaging validated dry (npm tarball + GitHub source tar)
268
295
 
269
296
  ---
270
297
 
271
298
  ## Project family
272
299
 
273
- Freestruct is one member of the Creadev doc-system family. The family shares
274
- conventions and a docs frame, but each project is separate and
275
- complimentary - freestruct imports nothing from its siblings.
300
+ Freestruct is one member of the Creadev doc-system family. The family shares conventions and a docs frame, but each project is separate and complimentary - freestruct imports nothing from its siblings.
276
301
 
277
- - [Freestruct docs](https://freestruct.creadev.org) - the docs site built from
278
- this repo (`docs/` is itself the worked example: Jekyll + Freestruct + Pagefind)
302
+ - [Freestruct docs](https://freestruct.creadev.org) - the docs site built from this repo (`docs/` is itself the worked example: Jekyll + Freestruct + Pagefind)
279
303
  - [docs.creadev.org](https://docs.creadev.org) - the Creadev docs system hub
280
- - [prestruct](https://github.com/dhaupin/prestruct) /
281
- [prestruct.creadev.org](https://prestruct.creadev.org) - a separate,
282
- complimentary project: a React SSG for Cloudflare Pages that shares the
283
- docs frame and conventions (and some gates' patterns) with freestruct, but
284
- installs, builds and ships nothing together with it
285
- - [prestruct docs](https://docs.creadev.org/prestruct) - the prestruct docs
286
- layer on the shared system
287
- - [crashtest](https://crashtest.creadev.org) - adversarial QA for agent-built
288
- apps; Freestruct ships and CI-runs its own pack
289
- - [vant](https://github.com/dhaupin/vant) - the prototype Freestruct was
290
- extracted from
291
-
292
- For agents: [AGENTS.md](AGENTS.md) is the orientation file (scope, house
293
- rules, gates). Humans and agents are welcome equally - see
294
- [CONTRIBUTING.md](CONTRIBUTING.md).
304
+ - [prestruct](https://github.com/dhaupin/prestruct) / [prestruct.creadev.org](https://prestruct.creadev.org) - a separate, complimentary project: a React SSG for Cloudflare Pages that shares the docs frame and conventions (and some gates' patterns) with freestruct, but installs, builds and ships nothing together with it
305
+ - [prestruct docs](https://docs.creadev.org/prestruct) - the prestruct docs layer on the shared system
306
+ - [crashtest](https://crashtest.creadev.org) - adversarial QA for agent-built apps; Freestruct ships and CI-runs its own pack
307
+ - [vant](https://github.com/dhaupin/vant) - the prototype Freestruct was extracted from
308
+ - **The workshop** - [Creadev.org](https://creadev.org) (freestruct is made by Rook @ Creadev.org - the same home as the docs and site surfaces above)
309
+
310
+ For agents: [AGENTS.md](AGENTS.md) is the orientation file (scope, house rules, gates). Humans and agents are welcome equally - see [CONTRIBUTING.md](CONTRIBUTING.md).
@@ -38,11 +38,7 @@
38
38
  "@context": "https://schema.org",
39
39
  "@type": "WebSite",
40
40
  "name": "{{siteNameJson}}",
41
- "url": "{{siteUrlJson}}",
42
- "potentialAction": {
43
- "@type": "SearchAction",
44
- "target": "{{siteUrlJson}}/?s={search_term_string}"
45
- }
41
+ "url": "{{siteUrlJson}}"{{searchActionJson}}
46
42
  }
47
43
  </script>
48
44