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/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
- const basePath = config.basePath ? _norm(config.basePath) : '';
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
- const titleMatch = html.match(/<title>([^<]+)<\/title>/i);
89
- const descMatch = html.match(/<meta[^>]*name="description"[^>]*content="([^"]+)"/i);
90
- const title = titleMatch ? titleMatch[1].split(' | ')[0] : path.basename(page.file, '.html');
91
- const desc = descMatch ? descMatch[1] : '';
89
+ // Entity-DECODED extraction: the SSG's '&amp;' must reach escXml as a raw
90
+ // '&' so it escapes ONCE. Reading the raw match and re-escaping double-
91
+ // encoded the text ('&amp;amp;', which renders literally as '&amp;').
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
- adjust `site.description` to fit.
33
- 2. Let your SSG build. `npx freestruct` discovers the output dir
34
- (default `docs/_site`; override via the `outputDir` config key or a
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.0.0
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.0.0",
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
- "packs:selftest": "node scripts/packs-selftest.cjs"
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
  }