freestruct 1.1.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,11 +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, an empty output dir, and a failing `hooks.postBuild` command each
84
- fail loudly with their own exit code (`1` config, `2` template, `3` output
85
- dir, `4` hook failure; `--tolerant` downgrades fatals to exit 0 for CI
86
- 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).
87
74
 
88
75
  ### Useful flags and subcommands
89
76
 
@@ -111,54 +98,26 @@ npx freestruct audit # verify the baked output (heads + sitemap/
111
98
 
112
99
  ### SEO
113
100
 
114
- - Injects missing title, description, canonical, Open Graph, Twitter cards, and
115
- JSON-LD (missing-only: existing tags are left alone)
116
- - Feed autodiscovery: every injected head carries the
117
- `rel="alternate" type="application/rss+xml"` link (a page-owned one wins,
118
- missing-only) so feed readers find feed.xml without knowing the URL
119
- - Attribute-safe everywhere: og/twitter values are HTML-escaped, JSON-LD
120
- literals are JSON-escaped (a raw `<` cannot close the script tag),
121
- `og:image`/`twitter:image` ship only when an image is configured, and an
122
- optional `author` key rides the Organization JSON-LD as a schema.org founder
123
- - `search`, `breadcrumb`, `article`, `verification` and `locales`:
124
- the SearchAction target for the WebSite JSON-LD (no more hardcoded
125
- `?s=` default), auto BreadcrumbList from the URL tree, scoped per-page
126
- `Article`/`TechArticle`/`NewsArticle` blocks, provider-keyed site
127
- verification metas, and hreflang stem-swap alternates for multilingual
128
- trees - all opt-in, missing-only, and loudly diagnostic (see the
129
- Discovery Options in the config reference)
130
- - `llms: true` emits the AI-consumable `llms.txt` summary over the same
131
- indexable page set sitemap and feed honor (noindex stays honored;
132
- an existing file is never overwritten)
133
- - `og.image` takes the back-compat string form or the structured
134
- `{ src, width, height, alt }` object form, which also emits the
135
- `og:image:width`/`height`/`alt` companions; SVG sources warn loudly on
136
- every run (most scrapers cannot render them), `allowSvg: true` accepts
137
- one explicitly, and `--strict` fails the build until it is answered
138
- - Page-level tuning survives the build: a `<!-- freestruct: {...} -->` comment
139
- overrides title/description/ogType/ogImage/twitterCard and adds
140
- `article:published_time`/`author`/`section`; custom `jsonLd` objects append
141
- structured data
142
- - Honest canonical URLs including sub-path (`basePath`) installs - the same
143
- contract holds for the `sitemap.xml` locs, `robots.txt` sitemap pointer, the
144
- 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
145
109
  - Tagged with a `freestruct-build` marker comment
146
110
 
147
111
  ### Search
148
112
 
149
- - **Opt-in** `search.json` (set `searchIndex: true`); default runs generate
150
- nothing - bring your own indexer (Pagefind/Algolia), the search guide covers
151
- 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
152
114
 
153
115
  ### Sitemap, robots, feed & 404
154
116
 
155
- - `sitemap.xml`: encoded URLs, real per-file `lastmod` (page mtime, not build
156
- time), noindex pages excluded
117
+ - `sitemap.xml`: encoded URLs, real per-file `lastmod` (page mtime, not build time), noindex pages excluded
157
118
  - `robots.txt`: `User-agent: *` + `Allow: /` + a basePath-aware sitemap pointer
158
- - `feed.xml` (RSS): escaped titles/descriptions, per-page `pubDate` from each
159
- page's own mtime, basePath-aware channel/self links
160
- - `404.html`: generated when missing, gets the full SEO pipeline + explicit
161
- 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
162
121
 
163
122
  ### Performance
164
123
 
@@ -167,36 +126,13 @@ npx freestruct audit # verify the baked output (heads + sitemap/
167
126
 
168
127
  ### DX
169
128
 
170
- - Config schema with loud `[freestruct] config warning` diagnostics; unknown
171
- and retired keys never die silently
172
- - Link validation (report-only by default, `--strict` to make fatal - our own
173
- docs CI runs `--strict` and it has already caught three production 404s)
174
- - `freestruct audit` verifies the baked output independently - per-page
175
- head hygiene, the 404 noindex contract, sitemap/robots sanity - with an
176
- exit code for CI (`0` clean, `1` findings, `--json` digest for
177
- annotations); it reads only the output dir, so it checks any SSG's build
178
- with freestruct applied
179
- - IndexNow support (issue #19): freestruct is the component that already
180
- owns the sitemap, so the key file (the keyLocation ownership proof) ships
181
- into the output during the run and `freestruct indexnow` POSTs the baked
182
- URL list to api.indexnow.org afterwards - deploy first (keyLocation must
183
- resolve), submission is the post-deploy step, and `--dry-run` prints the
184
- request body for CI
185
- - Slug-glob scoped config (issue #25): `jsonLd` accepts the legacy
186
- object/list forms or a map keyed by page-slug globs (`/` exact, `/blog/*`
187
- one segment, `/guides/**` a sub-tree) so page-specific schema lands only
188
- where it is meant to; `og.scoped` does the same for og image variants
189
- with the most specific match winning. Missing-only holds everywhere,
190
- malformed entries warn loudly and skip only themselves
191
- - Per-page OG image generation (issue #22): freestruct already extracts
192
- each page's title/description, so it can render a branded 1200x630 PNG
193
- per page from a consumer SVG template (`{{ogTitle}}`/`{{ogDescription}}`/
194
- `{{ogUrl}}` placeholders) and wire og/twitter to it - missing-only keeps
195
- pages with their own og set. Rasterization is honest: PNG needs a real
196
- rasterizer, so the optional `@resvg/resvg-js` package is loaded lazily
197
- and the run stays green with a loud skip when it is not installed
198
- - Post-render hooks with purge-command safety (disabled by default; see
199
- [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))
200
136
 
201
137
  ---
202
138
 
@@ -220,6 +156,7 @@ Full key list (unknown or retired keys produce warnings, not silence):
220
156
  | `readingTime` | `true` | Inject `<meta name="reading-time">` |
221
157
  | `lastModified` | `true` | Inject `<meta name="last-modified">` |
222
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) |
223
160
  | `linkCheck` | `true` | Validate internal links (report-only; `--strict` makes them fatal) |
224
161
  | `searchIndex` | `false` | **Opt-in**: generate `search.json` for client-side search |
225
162
  | `minify` | `false` | Experimental HTML minifier (preserves `pre`/`script`/`style`/`textarea`) |
@@ -231,14 +168,7 @@ Full key list (unknown or retired keys produce warnings, not silence):
231
168
  | `cacheBusting.hashInCanonicalUrl` | `false` | Append `?v={buildHash}` to canonical URLs |
232
169
  | `cacheBusting.purge` | *(unset)* | Purge hook list (name + command); disabled without `runHooks: true` |
233
170
 
234
- The docs ([freestruct.creadev.org](https://freestruct.creadev.org)) carry the
235
- key-by-key guides: the [config reference](https://freestruct.creadev.org/guides/freestruct-config),
236
- [cache busting](https://freestruct.creadev.org/guides/cache-busting), [custom
237
- injection](https://freestruct.creadev.org/guides/custom-injection),
238
- [CLI reference](https://freestruct.creadev.org/guides/cli),
239
- [post-render hooks](https://freestruct.creadev.org/guides/post-render-hooks),
240
- [CI/CD](https://freestruct.creadev.org/guides/cicd) and
241
- [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).
242
172
 
243
173
  ---
244
174
 
@@ -255,26 +185,20 @@ cacheBusting:
255
185
  command: "cf-cli purge $SITE_URL"
256
186
  ```
257
187
 
258
- Use only trusted commands. Disabled by default. Available variables:
259
- `$SITE_URL`, `$BUILD_HASH`, `$OUTPUT_DIR` (as env vars on the child:
260
- `FREESTRUCT_SITE_URL`, `FREESTRUCT_BUILD_HASH`, `FREESTRUCT_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`).
261
189
 
262
- The same gateway covers the general postBuild seam - commands that chain
263
- after the entire pass (custom schema injection, audits, IndexNow
264
- submissions). A nonzero hook exit is a build failure (exit code 4):
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):
265
191
 
266
192
  ```yaml
267
193
  # In freestruct.config.yml
268
194
  runHooks: true
269
195
  hooks:
270
196
  postBuild:
271
- - node scripts/faqp-schema.mjs
197
+ - node scripts/faq-schema.mjs
272
198
  - npm run seo:audit
273
199
  ```
274
200
 
275
- Purge errors are reported and the build continues (a failed CDN purge is
276
- not a bad sitemap); postBuild failures fail the build on purpose - chain
277
- only trusted commands there too.
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.
278
202
 
279
203
  ---
280
204
 
@@ -282,107 +206,105 @@ only trusted commands there too.
282
206
 
283
207
  Two shipped surfaces beyond the engine:
284
208
 
285
- - **`init/`** - the consumer scaffold kit: a commented starter config,
286
- template copies, and a README ("copy + go"). Version-stamped on
287
- `package.json`; `npx freestruct` resolves templates user-staged first,
288
- package fallback second.
289
- - **`packs/`** - Freestruct's [crashtest](https://crashtest.creadev.org) pack
290
- surface: `freestruct-docs-shapes` attacks a built docs tree the way a
291
- static host serves it (traversal refusal, 404 noindex, noindex artifact
292
- leaks, canonical query poisoning, XML shape). See
293
- [packs/README.md](packs/README.md). The docs CI runs it against every
294
- 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.
295
211
 
296
212
  ---
297
213
 
298
214
  ## Releases
299
215
 
300
- - Versions live in one place: `package.json`, pinned in CI against the
301
- README/CHANGELOG truth. Single source, no drift.
302
- - `CHANGELOG.md` is the changelog; releases are tagged (`v*`) and
303
- CHANGELOG-truthful or they don't ship.
304
- - Current release: **1.1.0** (the version sweep + first post-1.0.0 surface:
305
- this release carries the issue sweep landed after the 2026-10-22 tag;
306
- see `labs/TASKS.md` for the version policy).
307
- - Deployment truth lives in [DEPLOY.md](DEPLOY.md): the GitHub Pages pipeline
308
- (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
309
268
 
310
269
  ## v1.1.0
311
270
 
312
- The first post-stable release, carried by the all-items issue sweep
313
- (#17-#25 plus the #25 follow-on) and the CI tooling that came with it:
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:
314
272
 
315
- - `freestruct audit` - independent baked-output verification (per-page head
316
- hygiene, the 404 noindex contract, sitemap/robots) with `--json` for CI
317
- - `hooks.postBuild` - the general post-build hook seam (exit code 4,
318
- `FREESTRUCT_*` env on hook children)
319
- - IndexNow - key-file emission + the `freestruct indexnow` post-deploy
320
- submission (`--dry-run` for CI)
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)
321
276
  - Scoped config - slug-glob map forms for `jsonLd` and `og.scoped`
322
- - Structured `og.image` - `{ src, width, height, alt, allowSvg }` with the
323
- companion tags and the SVG warn/strict ladder
324
- - Per-page OG-image generation - `ogImageGeneration` through the optional
325
- `@resvg/resvg-js` rasterizer
326
- - Latent hardening - every config/page-carried substitute now rides
327
- function replacers ($-expansion cannot corrupt output), plus the
328
- docs-layout dead-CSS removal
329
- - CI - packstruct manifest lint joined the push/PR battery alongside the
330
- crashtest pack selftest; the battery grew 49 -> 75 -> 103 pins
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
331
281
 
332
282
  ## v1.0.0
333
283
 
334
- First stable release. It carries the full rehaul-era stack (accumulated
335
- under the 0.6.0 freeze, promoted truthfully as 0.9.0), the prestruct port
336
- wave, and the toolchain maturity landed on 2026-10-22:
337
-
338
- - Config renamed to `freestruct.config.yml` with legacy read; schema-checked
339
- with loud diagnostics; FAST_FAIL exit codes + `--tolerant`
340
- - Module decomposition: the engine is `engine/` (one orchestrator + eight
341
- modules) with `bin/` shim and shipped templates; `init/` is the consumer
342
- scaffold kit; template copies provably in sync via CI
343
- - `searchIndex` opt-in; noindex honored across sitemap/feed/search/404;
344
- basePath contract across every generated URL (canonical, sitemap, feed,
345
- robots pointer, 404 home link)
346
- - Honest XML: escaped values, per-page `lastmod`, per-page RSS dates;
347
- escaped-or-fail on un-escapable content
348
- - Docs: single-source nav, audit gates wired to CI (style, truthfulness,
349
- English-only language gate, engine comment format), the docs CI itself
350
- 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
351
291
  - Release truth: tarball e2e CI gate + hard publish gate; version pinned
352
- - crashtest pack surface (`packs/public/freestruct-docs-shapes`) with CI
353
- teeth on every push and the Pages build
354
- - Port wave from prestruct: attribute-escaped injection, og/twitter image
355
- parity, `author` + JSON-LD founder, per-page override comments, custom
356
- `jsonLd`, deterministic build hash, CLI subcommands (`validate`/`init`/
357
- `help`), `FREESTRUCT_URL` env override
358
- - Toolchain: Node-24-compatible GH action majors, Jekyll 4.4 + Ruby 3.4,
359
- 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)
360
295
 
361
296
  ---
362
297
 
363
298
  ## Project family
364
299
 
365
- Freestruct is one member of the Creadev doc-system family. The family shares
366
- conventions and a docs frame, but each project is separate and
367
- 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.
368
301
 
369
- - [Freestruct docs](https://freestruct.creadev.org) - the docs site built from
370
- 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)
371
303
  - [docs.creadev.org](https://docs.creadev.org) - the Creadev docs system hub
372
- - [prestruct](https://github.com/dhaupin/prestruct) /
373
- [prestruct.creadev.org](https://prestruct.creadev.org) - a separate,
374
- complimentary project: a React SSG for Cloudflare Pages that shares the
375
- docs frame and conventions (and some gates' patterns) with freestruct, but
376
- installs, builds and ships nothing together with it
377
- - [prestruct docs](https://docs.creadev.org/prestruct) - the prestruct docs
378
- layer on the shared system
379
- - [crashtest](https://crashtest.creadev.org) - adversarial QA for agent-built
380
- apps; Freestruct ships and CI-runs its own pack
381
- - [vant](https://github.com/dhaupin/vant) - the prototype Freestruct was
382
- extracted from
383
- - **The workshop** - [Creadev.org](https://creadev.org) (freestruct is made
384
- by Rook @ Creadev.org - the same home as the docs and site surfaces above)
385
-
386
- For agents: [AGENTS.md](AGENTS.md) is the orientation file (scope, house
387
- rules, gates). Humans and agents are welcome equally - see
388
- [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).