nodality 1.0.166 → 1.0.168
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/bin/nodality.js +312 -0
- package/layout/animator.js +1 -1
- package/layout/audio.js +1 -1
- package/layout/audionew.js +1 -1
- package/layout/base-2.js +1 -1
- package/layout/base.js +1 -1
- package/layout/beta-desktop-bar.js +1 -1
- package/layout/beta-mobile-bar.js +1 -1
- package/layout/box.js +1 -1
- package/layout/button.js +1 -1
- package/layout/cards.js +1 -1
- package/layout/center.js +1 -1
- package/layout/checkbox.js +1 -1
- package/layout/circle.js +1 -1
- package/layout/clean-row.js +1 -1
- package/layout/code.js +1 -1
- package/layout/container.js +1 -1
- package/layout/custom.js +1 -1
- package/layout/div-image.js +1 -1
- package/layout/dropdown-2025.js +1 -1
- package/layout/dropdown.js +1 -1
- package/layout/empty-element.js +1 -1
- package/layout/external-stylesheet.js +1 -1
- package/layout/flex-card.js +1 -1
- package/layout/flex-grid.js +1 -1
- package/layout/flex-row.js +1 -1
- package/layout/footer.js +1 -1
- package/layout/form-components/custom.js +1 -1
- package/layout/form-components/data-list.js +1 -1
- package/layout/form-components/floating-input.js +1 -1
- package/layout/form-components/form-all.js +1 -1
- package/layout/form-components/form.js +1 -1
- package/layout/form-components/image-picker.js +1 -1
- package/layout/form-components/picker.js +1 -1
- package/layout/form-components/radio.js +1 -1
- package/layout/form-components/radiogroup.js +1 -1
- package/layout/form-components/range.js +1 -1
- package/layout/free.js +1 -1
- package/layout/grid-new.js +1 -1
- package/layout/grid-switcher.js +1 -1
- package/layout/grid.js +1 -1
- package/layout/group.js +1 -1
- package/layout/header.js +1 -1
- package/layout/horizontal-scroller.js +1 -1
- package/layout/image-old.js +1 -1
- package/layout/image.js +1 -1
- package/layout/index.js +1 -1
- package/layout/label.js +1 -1
- package/layout/link.js +1 -1
- package/layout/list-OLD.js +1 -1
- package/layout/list.js +1 -1
- package/layout/meta-adder.js +1 -1
- package/layout/modal-2025.js +1 -1
- package/layout/modernwrap.js +1 -1
- package/layout/multiswitcher.js +1 -1
- package/layout/multiswitcherBeta.js +1 -1
- package/layout/nav-bar.js +1 -1
- package/layout/nav-factor/custom-div.js +1 -1
- package/layout/navBar-OLD.js +1 -1
- package/layout/new-flat-adder.js +1 -1
- package/layout/new-nav-bar.js +1 -1
- package/layout/offset-container.js +1 -1
- package/layout/polygon.js +1 -1
- package/layout/prerender-site.js +16 -2
- package/layout/prerender.js +15 -5
- package/layout/progress.js +1 -1
- package/layout/row.js +1 -1
- package/layout/saved-new-nav-bar.js +1 -1
- package/layout/scroll-video.js +1 -1
- package/layout/side-bar.js +1 -1
- package/layout/side-nav-bar.js +1 -1
- package/layout/simple-bar.js +1 -1
- package/layout/slider-2025.js +1 -1
- package/layout/spacer.js +1 -1
- package/layout/stack.js +1 -1
- package/layout/styler.js +1 -1
- package/layout/svg.js +1 -1
- package/layout/switcher.js +1 -1
- package/layout/table.js +1 -1
- package/layout/text-field.js +1 -1
- package/layout/text.js +1 -1
- package/layout/ulist.js +1 -1
- package/layout/video.js +1 -1
- package/layout/without-new.js +1 -1
- package/layout/wrap.js +1 -1
- package/layout/zoom-card.js +1 -1
- package/lib/card-getter.js +1 -1
- package/lib/data.js +48 -0
- package/lib/designer.js +1 -1
- package/lib/element-mapper.js +1 -1
- package/lib/keyframe-animation.js +1 -1
- package/lib/link-getter.js +1 -1
- package/lib/scroll-video.js +1 -1
- package/lib/seo.js +198 -0
- package/lib/stacker.js +1 -1
- package/lib/theme.js +1 -1
- package/lib/transform-anim.js +1 -1
- package/package.json +4 -2
package/bin/nodality.js
CHANGED
|
@@ -40,6 +40,9 @@ function showUsage() {
|
|
|
40
40
|
console.log(`Usage:
|
|
41
41
|
nodality prerender [flags] # SSG: render upload/*.html in place
|
|
42
42
|
nodality compile [src/<file>.js] [flags] # Emit Designer output as a companion file
|
|
43
|
+
nodality fanout # Generate per-item pages from JSON (reads
|
|
44
|
+
# the \`fanout\` block in nodality.config.json;
|
|
45
|
+
# runs automatically before \`prerender\`)
|
|
43
46
|
nodality help
|
|
44
47
|
|
|
45
48
|
Compile flags:
|
|
@@ -162,6 +165,280 @@ function loadConfigFile(cwd) {
|
|
|
162
165
|
}
|
|
163
166
|
}
|
|
164
167
|
|
|
168
|
+
// ─── Fanout (data-driven page expansion) ────────────────────────
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Generate one HTML + entry-script pair per item in a JSON dataset.
|
|
172
|
+
* The classic use case is a "detail" template that takes a query
|
|
173
|
+
* parameter at runtime (`/detail.html?id=helix`) and the SEO problem
|
|
174
|
+
* that crawlers see an empty mount for every product. Fanout reads
|
|
175
|
+
* the data once at build time, walks an array within it, and writes
|
|
176
|
+
* a per-item static page that the prerender step then turns into
|
|
177
|
+
* fully populated HTML.
|
|
178
|
+
*
|
|
179
|
+
* Configured in `nodality.config.json`:
|
|
180
|
+
*
|
|
181
|
+
* "fanout": [
|
|
182
|
+
* {
|
|
183
|
+
* "template": "detail.html",
|
|
184
|
+
* "data": "products.json",
|
|
185
|
+
* "items": "categories[].products", // dot path with [] for flatMap
|
|
186
|
+
* "id": "id", // field on each item
|
|
187
|
+
* "title": "SLS3 — {name}", // {name} = item.name, {id} etc.
|
|
188
|
+
* "entry": "pages/detail.js", // exported renderDetailPage(id)
|
|
189
|
+
* "bodyAttr": "data-product-id" // attr on <body> for the id
|
|
190
|
+
* }
|
|
191
|
+
* ]
|
|
192
|
+
*
|
|
193
|
+
* Output:
|
|
194
|
+
* • `upload/<basename>-<id>.html` — clone of the template, with
|
|
195
|
+
* `<title>` substituted and `<body data-...="<id>">`.
|
|
196
|
+
* • `upload/pages/<basename>-<id>.js` — thin wrapper that imports
|
|
197
|
+
* the entry's exported renderer and invokes it with this id.
|
|
198
|
+
*
|
|
199
|
+
* Stale outputs from a previous run (orphaned products etc.) are
|
|
200
|
+
* cleaned up before regenerating. The expansion runs once at the
|
|
201
|
+
* start of `nodality prerender` whenever the config has a `fanout`
|
|
202
|
+
* block; you can also invoke it standalone via `nodality fanout`.
|
|
203
|
+
*/
|
|
204
|
+
function runFanout(cwd, uploadDir, fanoutConfig) {
|
|
205
|
+
if (!Array.isArray(fanoutConfig) || fanoutConfig.length === 0) return 0;
|
|
206
|
+
|
|
207
|
+
let totalWrote = 0;
|
|
208
|
+
for (const spec of fanoutConfig) {
|
|
209
|
+
const {
|
|
210
|
+
template, data, items, id = "id", title, entry,
|
|
211
|
+
bodyAttr = "data-product-id",
|
|
212
|
+
// ─── Auto-injected JSON-LD per item (1.0.168+) ─────────
|
|
213
|
+
// When `jsonLdType` is set, fanout emits a
|
|
214
|
+
// `<script type="application/ld+json" data-seo="1">` block per
|
|
215
|
+
// generated HTML with the right schema.org type and fields
|
|
216
|
+
// mapped from each item. Saves projects from importing
|
|
217
|
+
// nodality/seo and calling productJsonLd() by hand inside the
|
|
218
|
+
// page entry — useful because the page entry runs LATE (after
|
|
219
|
+
// the static HTML is already served) so crawlers that don't
|
|
220
|
+
// execute JS miss any client-injected structured data.
|
|
221
|
+
jsonLdType, // e.g. "Product" | "Article"
|
|
222
|
+
jsonLdFields, // map of schema-key → dot-path on item (e.g. { name: "name", image: "images.0", sku: "id" })
|
|
223
|
+
jsonLdExtra, // static fields to add verbatim (e.g. { brand: { "@type": "Brand", name: "SLS3" }, priceCurrency: "CZK" })
|
|
224
|
+
} = spec;
|
|
225
|
+
if (!template || !data) {
|
|
226
|
+
console.warn(`[nodality] fanout: skipping spec without template/data`);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const tplPath = path.join(uploadDir, template);
|
|
231
|
+
const dataPath = path.join(uploadDir, data);
|
|
232
|
+
if (!fs.existsSync(tplPath)) {
|
|
233
|
+
console.error(`[nodality] fanout: template missing: ${template}`);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
if (!fs.existsSync(dataPath)) {
|
|
237
|
+
console.error(`[nodality] fanout: data missing: ${data}`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const json = JSON.parse(fs.readFileSync(dataPath, "utf8"));
|
|
242
|
+
const list = resolveItemsPath(json, items ?? "");
|
|
243
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
244
|
+
console.warn(`[nodality] fanout: ${data} produced no items at path "${items}"`);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const templateHtml = fs.readFileSync(tplPath, "utf8");
|
|
249
|
+
const base = path.basename(template, ".html");
|
|
250
|
+
const pagesDir = path.join(uploadDir, "pages");
|
|
251
|
+
fs.mkdirSync(pagesDir, { recursive: true });
|
|
252
|
+
|
|
253
|
+
// Clean stale outputs from a previous run so removing an item
|
|
254
|
+
// also removes its page. Match by prefix; original template
|
|
255
|
+
// (e.g. `detail.html` itself) is never touched.
|
|
256
|
+
for (const f of fs.readdirSync(uploadDir)) {
|
|
257
|
+
if (new RegExp(`^${escapeRegex(base)}-.+\\.html$`).test(f)) {
|
|
258
|
+
fs.unlinkSync(path.join(uploadDir, f));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
for (const f of fs.readdirSync(pagesDir)) {
|
|
262
|
+
if (new RegExp(`^${escapeRegex(base)}-.+\\.js$`).test(f)) {
|
|
263
|
+
fs.unlinkSync(path.join(pagesDir, f));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Discover the entry file to mirror per-item if not given.
|
|
268
|
+
const entryRel = entry ?? `pages/${base}.js`;
|
|
269
|
+
const entryFull = path.join(uploadDir, entryRel);
|
|
270
|
+
if (!fs.existsSync(entryFull)) {
|
|
271
|
+
console.error(`[nodality] fanout: entry script missing: ${entryRel}`);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
const entryBase = path.basename(entryRel, ".js");
|
|
275
|
+
const entryDir = path.dirname(entryRel);
|
|
276
|
+
|
|
277
|
+
let wrote = 0;
|
|
278
|
+
for (const item of list) {
|
|
279
|
+
const itemId = item?.[id];
|
|
280
|
+
if (!itemId || !/^[A-Za-z0-9][A-Za-z0-9-_]*$/.test(String(itemId))) {
|
|
281
|
+
console.warn(`[nodality] fanout: skipping item with bad ${id}: ${JSON.stringify(itemId)}`);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// HTML: rewrite <title>, inject body data-attr, point script src
|
|
286
|
+
// at the per-item entry.
|
|
287
|
+
const resolvedTitle = title
|
|
288
|
+
? interpolate(title, item)
|
|
289
|
+
: String(item?.name ?? itemId);
|
|
290
|
+
const escTitle = escapeHtml(resolvedTitle);
|
|
291
|
+
|
|
292
|
+
let html = templateHtml
|
|
293
|
+
.replace(/<title>[^<]*<\/title>/i, `<title>${escTitle}</title>`)
|
|
294
|
+
.replace(/<body(\s[^>]*)?>/i, (_, attrs = "") => {
|
|
295
|
+
const cleaned = (attrs ?? "").replace(
|
|
296
|
+
new RegExp(`\\s+${escapeRegex(bodyAttr)}="[^"]*"`),
|
|
297
|
+
"",
|
|
298
|
+
);
|
|
299
|
+
return `<body${cleaned} ${bodyAttr}="${itemId}">`;
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Rewrite the original `<script src=".../pages/<entryBase>.js">`
|
|
303
|
+
// to point at the per-item wrapper.
|
|
304
|
+
const srcRx = new RegExp(
|
|
305
|
+
`(<script[^>]*src=")([^"]*\\/?)${escapeRegex(entryBase)}\\.js("[^>]*>)`,
|
|
306
|
+
"i",
|
|
307
|
+
);
|
|
308
|
+
html = html.replace(srcRx, `$1$2${entryBase}-${itemId}.js$3`);
|
|
309
|
+
|
|
310
|
+
// Auto-injected JSON-LD per item, if the spec asked for it.
|
|
311
|
+
// Inserted inside <head> (before </head>) so crawlers see it
|
|
312
|
+
// without executing the page entry — critical for Bing /
|
|
313
|
+
// social-card scrapers / non-Google crawlers that don't run JS.
|
|
314
|
+
if (jsonLdType && jsonLdFields) {
|
|
315
|
+
const ld = buildItemJsonLd(jsonLdType, jsonLdFields, jsonLdExtra, item);
|
|
316
|
+
const tag = `<script type="application/ld+json" data-seo="1">${
|
|
317
|
+
JSON.stringify(ld)
|
|
318
|
+
}</script>`;
|
|
319
|
+
html = html.replace(/<\/head>/i, `${tag}\n</head>`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
fs.writeFileSync(path.join(uploadDir, `${base}-${itemId}.html`), html);
|
|
323
|
+
|
|
324
|
+
// JS wrapper. The user's entry must export an async function
|
|
325
|
+
// named `renderDetailPage` (or, generically, the camelCase
|
|
326
|
+
// form of the basename + "Page"). We call it with the id.
|
|
327
|
+
const fnName = entryBase
|
|
328
|
+
.split(/[-_]/)
|
|
329
|
+
.map((p, i) => (i === 0 ? p : p[0].toUpperCase() + p.slice(1)))
|
|
330
|
+
.join("") + "Page"; // detail → renderDetailPage? No — detailPage
|
|
331
|
+
// To minimise convention surprises, default to the underscored
|
|
332
|
+
// `render<Pascal>Page` form too. Try both: the wrapper imports
|
|
333
|
+
// whichever the entry exports.
|
|
334
|
+
const pascal = entryBase
|
|
335
|
+
.split(/[-_]/)
|
|
336
|
+
.map((p) => p[0].toUpperCase() + p.slice(1))
|
|
337
|
+
.join("");
|
|
338
|
+
const wrapperBody = `// Auto-generated by \`nodality fanout\`.
|
|
339
|
+
// Per-item entry — invokes the renderer in ${entryRel}
|
|
340
|
+
// with this item's id baked in.
|
|
341
|
+
import * as mod from "./${path.basename(entryRel)}";
|
|
342
|
+
|
|
343
|
+
const fn = mod.render${pascal}Page ?? mod.${fnName} ?? mod.default;
|
|
344
|
+
if (typeof fn !== "function") {
|
|
345
|
+
throw new Error(
|
|
346
|
+
"[fanout] ${entryRel} must export render${pascal}Page(id) " +
|
|
347
|
+
"(or a default async function). nodality fanout could not find one.",
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
await fn(${JSON.stringify(String(itemId))});
|
|
351
|
+
`;
|
|
352
|
+
fs.writeFileSync(
|
|
353
|
+
path.join(pagesDir, `${entryBase}-${itemId}.js`),
|
|
354
|
+
wrapperBody,
|
|
355
|
+
);
|
|
356
|
+
wrote++;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
console.log(`[nodality] fanout: ${template} × ${data} → ${wrote} page(s)`);
|
|
360
|
+
totalWrote += wrote;
|
|
361
|
+
}
|
|
362
|
+
return totalWrote;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Look up a dot/bracket path on a single item, e.g. "images.0" or
|
|
367
|
+
* "sizing.sizes.0.range". Returns undefined for missing branches.
|
|
368
|
+
* Used by fanout's per-item JSON-LD field mapper.
|
|
369
|
+
*/
|
|
370
|
+
function getByPath(obj, path) {
|
|
371
|
+
if (obj == null || !path) return undefined;
|
|
372
|
+
const parts = String(path).split(".");
|
|
373
|
+
let acc = obj;
|
|
374
|
+
for (const p of parts) {
|
|
375
|
+
if (acc == null) return undefined;
|
|
376
|
+
acc = acc[p];
|
|
377
|
+
}
|
|
378
|
+
return acc;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Build a schema.org JSON-LD object for one fanout item. `fields`
|
|
383
|
+
* maps schema-key → dot-path on the item; `extra` is merged in
|
|
384
|
+
* verbatim (static fields like brand, priceCurrency, "@context").
|
|
385
|
+
*/
|
|
386
|
+
function buildItemJsonLd(type, fields, extra, item) {
|
|
387
|
+
const out = { "@context": "https://schema.org", "@type": type };
|
|
388
|
+
for (const [key, fieldPath] of Object.entries(fields || {})) {
|
|
389
|
+
const v = getByPath(item, fieldPath);
|
|
390
|
+
if (v !== undefined && v !== null && v !== "") out[key] = v;
|
|
391
|
+
}
|
|
392
|
+
if (extra && typeof extra === "object") {
|
|
393
|
+
for (const [k, v] of Object.entries(extra)) {
|
|
394
|
+
if (v !== undefined) out[k] = v;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return out;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Resolve a dot-path with `[]` segments to an array of items. */
|
|
401
|
+
function resolveItemsPath(data, path) {
|
|
402
|
+
if (!path) return Array.isArray(data) ? data : [];
|
|
403
|
+
const tokens = path.split(/\.|(\[\])/).filter(Boolean);
|
|
404
|
+
let acc = data;
|
|
405
|
+
for (const tok of tokens) {
|
|
406
|
+
if (tok === "[]") {
|
|
407
|
+
if (!Array.isArray(acc)) return [];
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (Array.isArray(acc)) {
|
|
411
|
+
acc = acc.flatMap((x) => (x != null ? [x[tok]] : [])).filter((x) => x != null);
|
|
412
|
+
// After accessing a field through an array, the result is an
|
|
413
|
+
// array of values (possibly nested). Don't auto-flatten — the
|
|
414
|
+
// user can put another `[]` after if they want.
|
|
415
|
+
} else if (acc != null) {
|
|
416
|
+
acc = acc[tok];
|
|
417
|
+
} else {
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return Array.isArray(acc) ? acc.flat(Infinity).filter((x) => x != null) : [];
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Replace `{field}` in a template with item[field]. */
|
|
425
|
+
function interpolate(template, item) {
|
|
426
|
+
return template.replace(/\{([^}]+)\}/g, (_, key) => {
|
|
427
|
+
const v = key.split(".").reduce((acc, k) => (acc == null ? acc : acc[k]), item);
|
|
428
|
+
return v == null ? "" : String(v);
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function escapeRegex(s) {
|
|
433
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function escapeHtml(s) {
|
|
437
|
+
return String(s).replace(/[<>&"']/g, (c) =>
|
|
438
|
+
c === "<" ? "<" : c === ">" ? ">" : c === "&" ? "&" : c === '"' ? """ : "'",
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
165
442
|
// ─── Page auto-discovery ────────────────────────────────────────
|
|
166
443
|
|
|
167
444
|
/**
|
|
@@ -258,6 +535,16 @@ async function runPrerender(rawArgs) {
|
|
|
258
535
|
);
|
|
259
536
|
}
|
|
260
537
|
|
|
538
|
+
// Data-driven page fanout. When the config declares a `fanout`
|
|
539
|
+
// block (per-product detail pages, per-article blog entries, etc.),
|
|
540
|
+
// expand it BEFORE auto-discovery so the generated HTMLs are picked
|
|
541
|
+
// up alongside the hand-written ones. Subprocess child renders skip
|
|
542
|
+
// fanout — the parent already wrote the files and re-running inside
|
|
543
|
+
// each locale would just thrash them.
|
|
544
|
+
if (!process.env.NODALITY_SSG_LOCALE && Array.isArray(fileConfig.fanout)) {
|
|
545
|
+
runFanout(cwd, uploadDir, fileConfig.fanout);
|
|
546
|
+
}
|
|
547
|
+
|
|
261
548
|
// Explicit `pages` from config wins over auto-discovery. The
|
|
262
549
|
// discovery rules (pages/<base>.js, <base>.js) can't infer
|
|
263
550
|
// irregular pairs like h7-nodality's `index.html → app.js`; for
|
|
@@ -441,6 +728,29 @@ ${body}
|
|
|
441
728
|
}
|
|
442
729
|
}
|
|
443
730
|
|
|
731
|
+
// ─── fanout subcommand (standalone) ────────────────────────────
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Run the fanout expansion standalone (without prerender) — useful
|
|
735
|
+
* for debugging the generated files before kicking off a full SSG.
|
|
736
|
+
* Reads the `fanout` block from nodality.config.json.
|
|
737
|
+
*/
|
|
738
|
+
async function runFanoutStandalone(rawArgs) {
|
|
739
|
+
const cwd = process.cwd();
|
|
740
|
+
const flags = parseFlags(rawArgs);
|
|
741
|
+
const fileConfig = loadConfigFile(cwd);
|
|
742
|
+
const uploadDir = path.resolve(cwd, flags.upload || fileConfig.uploadDir || "upload");
|
|
743
|
+
|
|
744
|
+
if (!Array.isArray(fileConfig.fanout) || fileConfig.fanout.length === 0) {
|
|
745
|
+
console.error(`[nodality] fanout: no \`fanout\` block in nodality.config.json`);
|
|
746
|
+
process.exit(1);
|
|
747
|
+
}
|
|
748
|
+
const wrote = runFanout(cwd, uploadDir, fileConfig.fanout);
|
|
749
|
+
if (wrote === 0) {
|
|
750
|
+
console.warn(`[nodality] fanout: 0 page(s) generated — check config & data`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
444
754
|
// ─── Dispatch ──────────────────────────────────────────────────
|
|
445
755
|
|
|
446
756
|
async function main() {
|
|
@@ -462,6 +772,8 @@ async function main() {
|
|
|
462
772
|
await runPrerender(rest);
|
|
463
773
|
} else if (command === "compile") {
|
|
464
774
|
await runCompile(rest);
|
|
775
|
+
} else if (command === "fanout") {
|
|
776
|
+
await runFanoutStandalone(rest);
|
|
465
777
|
} else {
|
|
466
778
|
console.error(`[nodality] Unknown command: ${command}`);
|
|
467
779
|
showUsage();
|
package/layout/animator.js
CHANGED
package/layout/audio.js
CHANGED
package/layout/audionew.js
CHANGED
package/layout/base-2.js
CHANGED
package/layout/base.js
CHANGED
package/layout/box.js
CHANGED
package/layout/button.js
CHANGED
package/layout/cards.js
CHANGED
package/layout/center.js
CHANGED
package/layout/checkbox.js
CHANGED
package/layout/circle.js
CHANGED
package/layout/clean-row.js
CHANGED
package/layout/code.js
CHANGED
package/layout/container.js
CHANGED
package/layout/custom.js
CHANGED
package/layout/div-image.js
CHANGED
package/layout/dropdown-2025.js
CHANGED
package/layout/dropdown.js
CHANGED
package/layout/empty-element.js
CHANGED
package/layout/flex-card.js
CHANGED
package/layout/flex-grid.js
CHANGED
package/layout/flex-row.js
CHANGED
package/layout/footer.js
CHANGED
package/layout/free.js
CHANGED
package/layout/grid-new.js
CHANGED
package/layout/grid-switcher.js
CHANGED
package/layout/grid.js
CHANGED