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 +135 -119
- package/engine/_freestruct/inject-brand.html +1 -5
- package/engine/audit.js +487 -0
- package/engine/config.js +208 -1
- package/engine/hooks.js +81 -15
- package/engine/indexnow.js +149 -0
- package/engine/inject.js +303 -22
- package/engine/linkcheck.js +83 -11
- package/engine/llms.js +63 -0
- package/engine/notfound.js +21 -5
- package/engine/ogimage.js +142 -0
- package/engine/scan.js +155 -20
- package/engine/search.js +12 -9
- package/engine/seo.js +682 -71
- package/engine/urls.js +48 -1
- package/engine/xmllike.js +7 -4
- package/init/README.md +6 -20
- package/init/VERSION +1 -1
- package/init/docs/_freestruct/inject-brand.html +1 -5
- package/init/docs/freestruct.config.yml +16 -0
- package/package.json +9 -2
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
|
-
|
|
110
|
-
- Attribute-safe everywhere: og/twitter values are HTML-escaped, JSON-LD
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
- Page-level tuning survives the build: a `<!-- freestruct: {...} -->` comment
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
-
|
|
149
|
-
|
|
150
|
-
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
233
|
-
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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;`), 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><link href="/style.css"></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 `&`) 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 (`’`, `—`) stayed literal and the feed double-escaped it (`&#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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
-
|
|
247
|
-
|
|
248
|
-
-
|
|
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
|
-
|
|
262
|
-
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|