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/engine/urls.js
CHANGED
|
@@ -16,7 +16,14 @@ const _norm = s => '/' + String(s).replace(/^\/+/, '');
|
|
|
16
16
|
|
|
17
17
|
export function pageUrlFor(config, slug) {
|
|
18
18
|
const base = canonicalBase(config);
|
|
19
|
-
|
|
19
|
+
// Trailing slashes on a CONSUMER-PROVIDED basePath normalize away
|
|
20
|
+
// (Pass 26.13): '/myproject/' joined onto a lead-slash slug doubled the
|
|
21
|
+
// junction ('/myproject//guide/') through every injected URL class
|
|
22
|
+
// (canonical, sitemap, feed, hreflang). Directory SLUGS keep theirs -
|
|
23
|
+
// a directory page's trailing slash is canonical - only basePath strips.
|
|
24
|
+
const basePath = config.basePath
|
|
25
|
+
? _norm(config.basePath).replace(/\/+$/, '')
|
|
26
|
+
: '';
|
|
20
27
|
const slugPath = _norm(slug);
|
|
21
28
|
|
|
22
29
|
// rel path already carries the deploy directory (e.g. tree nests docs/) -
|
|
@@ -26,3 +33,43 @@ export function pageUrlFor(config, slug) {
|
|
|
26
33
|
}
|
|
27
34
|
return base + basePath + slugPath;
|
|
28
35
|
}
|
|
36
|
+
|
|
37
|
+
// ---------- slug/glob matching (issues #25 + follow-on) ----------
|
|
38
|
+
// The scoping contracts (jsonLd map keys, og.scoped keys) key on page slugs,
|
|
39
|
+
// NOT raw urls - the engine keys pages by slug and never knows a deploy host.
|
|
40
|
+
// '/' exact root
|
|
41
|
+
// '/faq' exact page (trailing slashes normalized away)
|
|
42
|
+
// '/blog/*' exactly one segment under /blog
|
|
43
|
+
// '/blog/**' /blog itself AND anything below it ('**' at the end only)
|
|
44
|
+
// Specificity ranks exact > literal-heavy glob > '**'; callers use it to
|
|
45
|
+
// pick the winning pattern when one value is decided (images).
|
|
46
|
+
|
|
47
|
+
function _forMatch(s) {
|
|
48
|
+
return _norm(s).replace(/\/+$/, '') || '/';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function slugMatchesGlob(pattern, slug) {
|
|
52
|
+
const p = _forMatch(pattern);
|
|
53
|
+
const s = _forMatch(slug);
|
|
54
|
+
// '**' (with or without the root slash) means everything
|
|
55
|
+
if (p === '**' || p === '/**') return true;
|
|
56
|
+
if (!p.includes('*')) return p === s;
|
|
57
|
+
// trailing '**' covers the stem directory itself AND everything below
|
|
58
|
+
if (p.endsWith('/**')) {
|
|
59
|
+
const stem = _forMatch(p.replace(/\/\*\*$/, ''));
|
|
60
|
+
return s === stem || s.startsWith(stem + '/');
|
|
61
|
+
}
|
|
62
|
+
// single '*' (and mid-pattern '**' degrading the same way): one segment
|
|
63
|
+
const re = new RegExp('^' + p
|
|
64
|
+
.split('*').map((seg) => seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
65
|
+
.join('[^/]+') + '$');
|
|
66
|
+
return re.test(s);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Higher number wins: exact match outranks every glob; between globs the
|
|
70
|
+
// one carrying more literal path characters is the more specific contract.
|
|
71
|
+
export function globSpecificity(pattern) {
|
|
72
|
+
const p = _forMatch(pattern);
|
|
73
|
+
if (!p.includes('*')) return 100000 + p.length;
|
|
74
|
+
return p.replace(/\*+/g, '').length;
|
|
75
|
+
}
|
package/engine/xmllike.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import fs from 'fs';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import { pageUrlFor } from './urls.js';
|
|
11
|
+
import { extractTitle, extractDescription } from './scan.js';
|
|
11
12
|
|
|
12
13
|
/** Escape a value for XML text/attribute contexts. */
|
|
13
14
|
export function escXml(value) {
|
|
@@ -85,10 +86,12 @@ export function generateFeed(pages, config, outputDir) {
|
|
|
85
86
|
let items = '';
|
|
86
87
|
for (const page of pages) {
|
|
87
88
|
const html = fs.readFileSync(page.file, 'utf8');
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
89
|
+
// Entity-DECODED extraction: the SSG's '&' must reach escXml as a raw
|
|
90
|
+
// '&' so it escapes ONCE. Reading the raw match and re-escaping double-
|
|
91
|
+
// encoded the text ('&', which renders literally as '&').
|
|
92
|
+
const extracted = extractTitle(html);
|
|
93
|
+
const title = extracted || path.basename(page.file).replace(/\.html?$/i, '');
|
|
94
|
+
const desc = extractDescription(html) || '';
|
|
92
95
|
const pageUrl = pageUrlFor(config, page.slug);
|
|
93
96
|
const pubDate = page.mtime.toUTCString();
|
|
94
97
|
items += '\n<item>\n'
|
package/init/README.md
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
# Freestruct init kit
|
|
2
2
|
|
|
3
|
-
This is the consumer scaffold kit (prestruct's `init/` pattern, sized to
|
|
4
|
-
Freestruct): the minimum a brand-new project copies in to be Freestruct-ready
|
|
5
|
-
in minutes. Freestruct runs AFTER your static site generator builds, so the
|
|
6
|
-
kit is deliberately SSG-agnostic - your Jekyll/Hugo/Docusaurus/MkDocs setup
|
|
7
|
-
stays untouched.
|
|
3
|
+
This is the consumer scaffold kit (prestruct's `init/` pattern, sized to Freestruct): the minimum a brand-new project copies in to be Freestruct-ready in minutes. Freestruct runs AFTER your static site generator builds, so the kit is deliberately SSG-agnostic - your Jekyll/Hugo/Docusaurus/MkDocs setup stays untouched.
|
|
8
4
|
|
|
9
5
|
## What's in the kit
|
|
10
6
|
|
|
@@ -28,21 +24,11 @@ cp -r node_modules/freestruct/init/docs .
|
|
|
28
24
|
|
|
29
25
|
Then:
|
|
30
26
|
|
|
31
|
-
1. Edit `docs/freestruct.config.yml`: `site.name` and `site.url` are required;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
positional CLI argument).
|
|
36
|
-
3. Verify: `docs/_site` gains `sitemap.xml`, `robots.txt`, `feed.xml` and
|
|
37
|
-
(when missing) a `404.html`; injected pages carry canonical/OG tags and a
|
|
38
|
-
`freestruct-build` marker.
|
|
39
|
-
4. Customize templates only if you need to: the packaged engine templates are
|
|
40
|
-
used automatically when no staged copy exists. To customize, edit
|
|
41
|
-
`docs/_freestruct/` copies (which you just copied in) - a staged template
|
|
42
|
-
always wins over the package-shipped one.
|
|
27
|
+
1. Edit `docs/freestruct.config.yml`: `site.name` and `site.url` are required; adjust `site.description` to fit. The starter also seeds the family identity - an `author` block (the Creadev.org founder) and a `jsonLd` maintainer Person - so scaffolded ports ship ownership from birth; set the maintainer handle to your own.
|
|
28
|
+
2. Let your SSG build. `npx freestruct` discovers the output dir (default `docs/_site`; override via the `outputDir` config key or a positional CLI argument).
|
|
29
|
+
3. Verify: `docs/_site` gains `sitemap.xml`, `robots.txt`, `feed.xml` and (when missing) a `404.html`; injected pages carry canonical/OG tags and a `freestruct-build` marker.
|
|
30
|
+
4. Customize templates only if you need to: the packaged engine templates are used automatically when no staged copy exists. To customize, edit `docs/_freestruct/` copies (which you just copied in) - a staged template always wins over the package-shipped one.
|
|
43
31
|
|
|
44
32
|
## Why a kit (not a lucky guess)
|
|
45
33
|
|
|
46
|
-
Copying this kit means your first run exercises the exact shape the test
|
|
47
|
-
battery and the tarball e2e verify: staged config + staged templates next to
|
|
48
|
-
the docs folder, discovered by walking up from your project root.
|
|
34
|
+
Copying this kit means your first run exercises the exact shape the test battery and the tarball e2e verify: staged config + staged templates next to the docs folder, discovered by walking up from your project root.
|
package/init/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.2.0
|
|
@@ -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
|
|
|
@@ -7,6 +7,22 @@ site:
|
|
|
7
7
|
url: https://example.com/docs
|
|
8
8
|
description: My documentation
|
|
9
9
|
|
|
10
|
+
# Family JSON-LD identity (owner standard): the founder ships as a schema.org
|
|
11
|
+
# Person under "founder" on the injected Organization block, and a maintainer
|
|
12
|
+
# Person is appended after the built-in Organization + WebSite blocks
|
|
13
|
+
# (missing-only, so pages that arrive with their own ld+json stay untouched).
|
|
14
|
+
# Fill in the maintainer handle for your repo; the author block is the family
|
|
15
|
+
# default and needs no edit.
|
|
16
|
+
author:
|
|
17
|
+
name: Creadev.org
|
|
18
|
+
url: https://creadev.org
|
|
19
|
+
|
|
20
|
+
jsonLd:
|
|
21
|
+
- "@type": Person
|
|
22
|
+
name: your-github-handle
|
|
23
|
+
url: https://github.com/your-github-handle
|
|
24
|
+
jobTitle: My Docs maintainer
|
|
25
|
+
|
|
10
26
|
# The rest is optional; see the README Configuration table for the list.
|
|
11
27
|
# basePath: /project-docs # when the output tree is served under a prefix
|
|
12
28
|
# searchIndex: true # opt-in: generate search.json
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "freestruct",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Post-build SEO layer for static doc sites - works with any SSG",
|
|
5
5
|
"homepage": "https://github.com/dhaupin/freestruct#readme",
|
|
6
6
|
"bugs": "https://github.com/dhaupin/freestruct/issues",
|
|
@@ -31,7 +31,11 @@
|
|
|
31
31
|
"test": "node test/run.js",
|
|
32
32
|
"e2e": "node scripts/e2e-install.cjs",
|
|
33
33
|
"lint:docs": "node scripts/run-docs-audit.cjs",
|
|
34
|
-
"
|
|
34
|
+
"lint:claims": "node scripts/check-fiction-signatures.cjs",
|
|
35
|
+
"packs:selftest": "node scripts/packs-selftest.cjs",
|
|
36
|
+
"check": "npm test && npm run lint:docs && npm run lint:claims && npm run packs:selftest",
|
|
37
|
+
"gate": "npm run sync:check && npm run check",
|
|
38
|
+
"prepublishOnly": "npm run gate"
|
|
35
39
|
},
|
|
36
40
|
"repository": {
|
|
37
41
|
"type": "git",
|
|
@@ -54,5 +58,8 @@
|
|
|
54
58
|
},
|
|
55
59
|
"dependencies": {
|
|
56
60
|
"js-yaml": "^5.5.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@resvg/resvg-js": "^2.6.2"
|
|
57
64
|
}
|
|
58
65
|
}
|