mnfst-render 0.5.46 → 0.5.47

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.
Files changed (2) hide show
  1. package/manifest.render.mjs +73 -16
  2. package/package.json +1 -1
@@ -3114,6 +3114,75 @@ ${urlEntries.join('\n')}
3114
3114
  writeLlmsFiles(outputDir, pathList, base, ctx);
3115
3115
  }
3116
3116
 
3117
+ /**
3118
+ * Group llms entries into sections by first URL segment ("Blog" for /blog/*),
3119
+ * root page as "Overview", in first-encounter order. Wherever a path holds two
3120
+ * or more sub-areas that each have their own page and children, each sub-area
3121
+ * becomes its own section labelled by its page title, recursively: /docs/ai/*
3122
+ * and /docs/framework/* split into "AI Plugins" and "Framework", and groups
3123
+ * inside them into "Framework / Core Plugins" and so on. A lone sub-area
3124
+ * (/fr/docs under /fr) is walked through when a split lies below it, and the
3125
+ * first segment then leads the label ("Fr / AI Plugins").
3126
+ */
3127
+ function groupLlmsSections(entries, siteName = '') {
3128
+ const titleCase = (s) => s.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
3129
+ const paths = new Set(entries.map((e) => e.pathSeg));
3130
+ const parents = new Set();
3131
+ for (const p of paths) {
3132
+ const segs = p.split('/');
3133
+ for (let i = 1; i < segs.length; i++) parents.add(segs.slice(0, i).join('/'));
3134
+ }
3135
+ const childAreas = new Map();
3136
+ for (const p of paths) {
3137
+ if (!parents.has(p) || !p.includes('/')) continue;
3138
+ const parent = p.slice(0, p.lastIndexOf('/'));
3139
+ if (!childAreas.has(parent)) childAreas.set(parent, new Set());
3140
+ childAreas.get(parent).add(p);
3141
+ }
3142
+ const splits = (prefix) => (childAreas.get(prefix)?.size || 0) >= 2;
3143
+ const below = new Map();
3144
+ const splitsBelow = (prefix) => {
3145
+ if (!below.has(prefix)) {
3146
+ const kids = childAreas.get(prefix);
3147
+ below.set(prefix, splits(prefix) || (kids?.size === 1 && splitsBelow(kids.values().next().value)));
3148
+ }
3149
+ return below.get(prefix);
3150
+ };
3151
+ const suffix = siteName ? new RegExp(`\\s+[—–|-]\\s+${siteName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`) : null;
3152
+ const titles = new Map(entries.map((e) => [e.pathSeg, e.title || '']));
3153
+ const labelCache = new Map();
3154
+ const titleOf = (area) => {
3155
+ if (labelCache.has(area)) return labelCache.get(area);
3156
+ let t = titles.get(area) || '';
3157
+ if (suffix) t = t.replace(suffix, '');
3158
+ t = t.trim();
3159
+ if (!t || t === siteName || t === area) t = titleCase(area.split('/').pop());
3160
+ labelCache.set(area, t);
3161
+ return t;
3162
+ };
3163
+ const sections = new Map();
3164
+ for (const entry of entries) {
3165
+ const segs = entry.pathSeg.split('/');
3166
+ const first = segs[0] || '';
3167
+ let key = first || 'Overview';
3168
+ const parts = first && !splits(first) && splitsBelow(first) ? [titleCase(first)] : [];
3169
+ let prefix = first;
3170
+ for (let i = 1; first && i < segs.length && splitsBelow(prefix); i++) {
3171
+ const child = segs.slice(0, i + 1).join('/');
3172
+ if (!childAreas.get(prefix).has(child)) break;
3173
+ if (splits(prefix)) {
3174
+ key = child;
3175
+ parts.push(titleOf(child));
3176
+ }
3177
+ prefix = child;
3178
+ }
3179
+ const label = parts.length > 1 || (parts.length && splits(first)) ? parts.join(' / ') : (first ? titleCase(first) : 'Overview');
3180
+ if (!sections.has(key)) sections.set(key, { label, entries: [] });
3181
+ sections.get(key).entries.push(entry);
3182
+ }
3183
+ return sections;
3184
+ }
3185
+
3117
3186
  /**
3118
3187
  * Write llms.txt (curated index) and llms-full.txt (concatenated full content)
3119
3188
  * per the llmstxt.org convention. Read each prerendered HTML file in pathList
@@ -3121,9 +3190,7 @@ ${urlEntries.join('\n')}
3121
3190
  * injectMetaInDom + smart defaults, so the output reflects the same layered
3122
3191
  * precedence (data-head → prerender.meta → smart defaults) without re-deriving.
3123
3192
  *
3124
- * Pages are grouped into sections by their first URL segment ("Getting Started"
3125
- * for /docs/getting-started/*, etc.) so the index is browseable. The root /
3126
- * page is treated as the site overview.
3193
+ * Sections come from groupLlmsSections.
3127
3194
  */
3128
3195
  function writeLlmsFiles(outputDir, pathList, liveBase, ctx = {}) {
3129
3196
  const siteName = ctx.siteName || 'Site';
@@ -3144,18 +3211,7 @@ function writeLlmsFiles(outputDir, pathList, liveBase, ctx = {}) {
3144
3211
  });
3145
3212
  }
3146
3213
 
3147
- // Group entries by section. For /a/b/c, the section is "a"; for the root,
3148
- // "Overview". Sections are presented in first-encounter order to preserve
3149
- // whatever order the project's manifest.json or yaml index dictated.
3150
- const sections = new Map();
3151
- const titleCase = (s) => s.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
3152
- for (const entry of entries) {
3153
- const first = entry.pathSeg.split('/')[0] || '';
3154
- const sectionKey = first || 'Overview';
3155
- const sectionLabel = first ? titleCase(first) : 'Overview';
3156
- if (!sections.has(sectionKey)) sections.set(sectionKey, { label: sectionLabel, entries: [] });
3157
- sections.get(sectionKey).entries.push(entry);
3158
- }
3214
+ const sections = groupLlmsSections(entries, siteName);
3159
3215
 
3160
3216
  // --- llms.txt: short curated index ---
3161
3217
  let llms = `# ${siteName}\n`;
@@ -4508,7 +4564,7 @@ async function runPrerender(config) {
4508
4564
  // observer state at runtime and therefore need the live Alpine scope.
4509
4565
  const INTERACTIVE_DIRECTIVES = new Set([
4510
4566
  'x-color', 'x-dropdown', 'x-tooltip', 'x-tab', 'x-tabpanel',
4511
- 'x-toast', 'x-carousel', 'x-resize', 'x-anchors', 'x-model',
4567
+ 'x-toast', 'x-carousel', 'x-anchors', 'x-model',
4512
4568
  'x-files', 'x-data-files',
4513
4569
  ]);
4514
4570
  // Runtime-only Alpine magics whose values change after the prerender
@@ -5486,6 +5542,7 @@ if (_isDirectEntry) {
5486
5542
  export {
5487
5543
  main,
5488
5544
  discoverRoutes,
5545
+ groupLlmsSections,
5489
5546
  acquireOutputLock,
5490
5547
  outputLockPath,
5491
5548
  markPrerenderedManifestComponents,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-render",
3
- "version": "0.5.46",
3
+ "version": "0.5.47",
4
4
  "description": "Render Manifest sites to static HTML for SEO",
5
5
  "type": "module",
6
6
  "bin": {