mnfst-render 0.5.45 → 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.
- package/manifest.render.mjs +101 -20
- package/package.json +1 -1
package/manifest.render.mjs
CHANGED
|
@@ -840,20 +840,43 @@ function discoverDataPaths(manifest, rootDir, wildcardBases = [], locales = [])
|
|
|
840
840
|
return wildcardBases.some((base) => rest.startsWith(base + '/'));
|
|
841
841
|
}
|
|
842
842
|
|
|
843
|
+
// A source mounts under the wildcard route named after it: `docs/*` for key
|
|
844
|
+
// `docs`, or a nested base ending in the key (`docs/framework/*` for key
|
|
845
|
+
// `framework`).
|
|
846
|
+
const mounts = new Map();
|
|
847
|
+
function mountFor(sourceKey) {
|
|
848
|
+
if (!sourceKey) return null;
|
|
849
|
+
if (mounts.has(sourceKey)) return mounts.get(sourceKey);
|
|
850
|
+
let mount = null;
|
|
851
|
+
if (wildcardBases.includes(sourceKey)) mount = sourceKey;
|
|
852
|
+
else {
|
|
853
|
+
const nested = wildcardBases.filter((b) => b.split('/').pop() === sourceKey);
|
|
854
|
+
if (nested.length === 1) {
|
|
855
|
+
mount = nested[0];
|
|
856
|
+
process.stderr.write(`prerender: data source "${sourceKey}" mounts under ${mount}/*\n`);
|
|
857
|
+
} else if (nested.length > 1) {
|
|
858
|
+
process.stderr.write(`prerender: data source "${sourceKey}" matches ${nested.map((b) => b + '/*').join(', ')}; not mounted (ambiguous)\n`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
mounts.set(sourceKey, mount);
|
|
862
|
+
return mount;
|
|
863
|
+
}
|
|
864
|
+
|
|
843
865
|
function expandCandidates(rawPath, sourceKey) {
|
|
844
866
|
const p = String(rawPath || '').replace(/^\/+|\/+$/g, '');
|
|
845
867
|
if (!p) return [];
|
|
846
868
|
const candidates = [p];
|
|
847
869
|
if (wildcardBases.length === 0) return candidates;
|
|
848
|
-
|
|
870
|
+
const mount = mountFor(sourceKey);
|
|
871
|
+
if (!mount) return candidates;
|
|
849
872
|
const parts = p.split('/');
|
|
850
873
|
const hasLocalePrefix = parts.length > 1 && localeSet.has(parts[0].toLowerCase());
|
|
851
874
|
if (hasLocalePrefix) {
|
|
852
875
|
const locale = parts[0];
|
|
853
876
|
const rest = parts.slice(1).join('/');
|
|
854
|
-
if (rest && !rest.startsWith(
|
|
855
|
-
} else if (!p.startsWith(
|
|
856
|
-
candidates.push(`${
|
|
877
|
+
if (rest && !rest.startsWith(mount + '/')) candidates.push(`${locale}/${mount}/${rest}`);
|
|
878
|
+
} else if (!p.startsWith(mount + '/')) {
|
|
879
|
+
candidates.push(`${mount}/${p}`);
|
|
857
880
|
}
|
|
858
881
|
return candidates;
|
|
859
882
|
}
|
|
@@ -3091,6 +3114,75 @@ ${urlEntries.join('\n')}
|
|
|
3091
3114
|
writeLlmsFiles(outputDir, pathList, base, ctx);
|
|
3092
3115
|
}
|
|
3093
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
|
+
|
|
3094
3186
|
/**
|
|
3095
3187
|
* Write llms.txt (curated index) and llms-full.txt (concatenated full content)
|
|
3096
3188
|
* per the llmstxt.org convention. Read each prerendered HTML file in pathList
|
|
@@ -3098,9 +3190,7 @@ ${urlEntries.join('\n')}
|
|
|
3098
3190
|
* injectMetaInDom + smart defaults, so the output reflects the same layered
|
|
3099
3191
|
* precedence (data-head → prerender.meta → smart defaults) without re-deriving.
|
|
3100
3192
|
*
|
|
3101
|
-
*
|
|
3102
|
-
* for /docs/getting-started/*, etc.) so the index is browseable. The root /
|
|
3103
|
-
* page is treated as the site overview.
|
|
3193
|
+
* Sections come from groupLlmsSections.
|
|
3104
3194
|
*/
|
|
3105
3195
|
function writeLlmsFiles(outputDir, pathList, liveBase, ctx = {}) {
|
|
3106
3196
|
const siteName = ctx.siteName || 'Site';
|
|
@@ -3121,18 +3211,7 @@ function writeLlmsFiles(outputDir, pathList, liveBase, ctx = {}) {
|
|
|
3121
3211
|
});
|
|
3122
3212
|
}
|
|
3123
3213
|
|
|
3124
|
-
|
|
3125
|
-
// "Overview". Sections are presented in first-encounter order to preserve
|
|
3126
|
-
// whatever order the project's manifest.json or yaml index dictated.
|
|
3127
|
-
const sections = new Map();
|
|
3128
|
-
const titleCase = (s) => s.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
3129
|
-
for (const entry of entries) {
|
|
3130
|
-
const first = entry.pathSeg.split('/')[0] || '';
|
|
3131
|
-
const sectionKey = first || 'Overview';
|
|
3132
|
-
const sectionLabel = first ? titleCase(first) : 'Overview';
|
|
3133
|
-
if (!sections.has(sectionKey)) sections.set(sectionKey, { label: sectionLabel, entries: [] });
|
|
3134
|
-
sections.get(sectionKey).entries.push(entry);
|
|
3135
|
-
}
|
|
3214
|
+
const sections = groupLlmsSections(entries, siteName);
|
|
3136
3215
|
|
|
3137
3216
|
// --- llms.txt: short curated index ---
|
|
3138
3217
|
let llms = `# ${siteName}\n`;
|
|
@@ -4485,7 +4564,7 @@ async function runPrerender(config) {
|
|
|
4485
4564
|
// observer state at runtime and therefore need the live Alpine scope.
|
|
4486
4565
|
const INTERACTIVE_DIRECTIVES = new Set([
|
|
4487
4566
|
'x-color', 'x-dropdown', 'x-tooltip', 'x-tab', 'x-tabpanel',
|
|
4488
|
-
'x-toast', 'x-carousel', 'x-
|
|
4567
|
+
'x-toast', 'x-carousel', 'x-anchors', 'x-model',
|
|
4489
4568
|
'x-files', 'x-data-files',
|
|
4490
4569
|
]);
|
|
4491
4570
|
// Runtime-only Alpine magics whose values change after the prerender
|
|
@@ -5462,6 +5541,8 @@ if (_isDirectEntry) {
|
|
|
5462
5541
|
// Exports for the CLI bin script and for unit testing.
|
|
5463
5542
|
export {
|
|
5464
5543
|
main,
|
|
5544
|
+
discoverRoutes,
|
|
5545
|
+
groupLlmsSections,
|
|
5465
5546
|
acquireOutputLock,
|
|
5466
5547
|
outputLockPath,
|
|
5467
5548
|
markPrerenderedManifestComponents,
|