@stati/core 1.28.0 → 1.29.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 +6 -1
- package/dist/core/build.d.ts.map +1 -1
- package/dist/core/build.js +122 -34
- package/dist/core/collections.d.ts +139 -0
- package/dist/core/collections.d.ts.map +1 -0
- package/dist/core/collections.js +342 -0
- package/dist/core/content.d.ts.map +1 -1
- package/dist/core/content.js +2 -1
- package/dist/core/dev.d.ts.map +1 -1
- package/dist/core/dev.js +11 -62
- package/dist/core/expand.d.ts +86 -0
- package/dist/core/expand.d.ts.map +1 -0
- package/dist/core/expand.js +484 -0
- package/dist/core/isg/builder.d.ts +15 -2
- package/dist/core/isg/builder.d.ts.map +1 -1
- package/dist/core/isg/builder.js +44 -6
- package/dist/core/isg/hash.d.ts +4 -2
- package/dist/core/isg/hash.d.ts.map +1 -1
- package/dist/core/isg/hash.js +16 -2
- package/dist/core/isg/index.d.ts +2 -1
- package/dist/core/isg/index.d.ts.map +1 -1
- package/dist/core/isg/index.js +3 -1
- package/dist/core/isg/manifest.js +6 -0
- package/dist/core/isg/prune.d.ts +32 -0
- package/dist/core/isg/prune.d.ts.map +1 -0
- package/dist/core/isg/prune.js +113 -0
- package/dist/core/navigation.d.ts +4 -2
- package/dist/core/navigation.d.ts.map +1 -1
- package/dist/core/navigation.js +17 -2
- package/dist/core/templates.d.ts +3 -0
- package/dist/core/templates.d.ts.map +1 -1
- package/dist/core/templates.js +67 -127
- package/dist/core/utils/fs.utils.d.ts +8 -0
- package/dist/core/utils/fs.utils.d.ts.map +1 -1
- package/dist/core/utils/fs.utils.js +13 -1
- package/dist/core/utils/index.d.ts +3 -3
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +3 -3
- package/dist/core/utils/paths.utils.d.ts +15 -0
- package/dist/core/utils/paths.utils.d.ts.map +1 -1
- package/dist/core/utils/paths.utils.js +20 -0
- package/dist/core/utils/template-discovery.utils.d.ts +25 -1
- package/dist/core/utils/template-discovery.utils.d.ts.map +1 -1
- package/dist/core/utils/template-discovery.utils.js +61 -11
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/rss/generator.d.ts.map +1 -1
- package/dist/rss/generator.js +5 -2
- package/dist/search/generator.d.ts +9 -0
- package/dist/search/generator.d.ts.map +1 -1
- package/dist/search/generator.js +20 -0
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +1 -1
- package/dist/types/config.d.ts +55 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/content.d.ts +129 -1
- package/dist/types/content.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/isg.d.ts +12 -0
- package/dist/types/isg.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -166,7 +166,8 @@ Customize layouts with [Eta templates](https://eta.js.org):
|
|
|
166
166
|
|
|
167
167
|
Only rebuild what changed:
|
|
168
168
|
|
|
169
|
-
- Smart caching based on hashes of each page's content and templates
|
|
169
|
+
- Smart caching based on hashes of each page's content and templates, and of the collection and navigation data it reads
|
|
170
|
+
- Removal of pages that no longer exist, such as deleted posts and unused tag pages
|
|
170
171
|
- TTL-based refresh for dynamic content
|
|
171
172
|
- Tag-based invalidation for related content
|
|
172
173
|
|
|
@@ -183,6 +184,10 @@ await invalidate('path:/posts');
|
|
|
183
184
|
await invalidate('age:3months');
|
|
184
185
|
```
|
|
185
186
|
|
|
187
|
+
#### Collections, Pagination and Generated Pages
|
|
188
|
+
|
|
189
|
+
A folder with an `index.md` is a collection: templates read its pages as `stati.collection`, sorted by date and grouped by tag. `paginate: 10` in front matter splits a listing into `/blog`, `/blog/page/2` and so on (`stati.pagination`), and a `[tag].md` template becomes one page per tag (`stati.generated`). See [Collections and generated pages](https://stati.build/core-concepts/collections).
|
|
190
|
+
|
|
186
191
|
#### Built-In SEO
|
|
187
192
|
|
|
188
193
|
SEO optimization works automatically with zero configuration:
|
package/dist/core/build.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/core/build.ts"],"names":[],"mappings":"AAuEA,OAAO,KAAK,EAEV,UAAU,EACV,MAAM,EASP,MAAM,mBAAmB,CAAC;AAK3B,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGxE;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,gCAAgC;IAChC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,YAAY;IAC3B,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,uCAAuC;IACvC,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACpC,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,8BAA8B;IAC9B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,+BAA+B;IAC/B,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,iCAAiC;IACjC,OAAO,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;IACrC,uEAAuE;IACvE,eAAe,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACtC,sGAAsG;IACtG,gBAAgB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,mFAAmF;IACnF,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,WAAY,SAAQ,UAAU;IAC7C,wDAAwD;IACxD,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAkGD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAsB5E"}
|
package/dist/core/build.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import { ensureDir, writeFile, remove, pathExists, stat, readdir, copyFile, resolveOutDir, resolveStaticDir, resolveCacheDir, enableInventoryTracking, disableInventoryTracking, clearInventory, writeTailwindClassInventory, getInventorySize, isTailwindUsed, loadPreviousInventory, compileTypeScript, detectExistingBundles, autoInjectBundles, getBundlePathsForPage, formatBytes, } from './utils/index.js';
|
|
1
|
+
import { ensureDir, writeFile, remove, pathExists, stat, readdir, copyFile, resolveOutDir, resolveStaticDir, resolveCacheDir, enableInventoryTracking, disableInventoryTracking, clearInventory, writeTailwindClassInventory, getInventorySize, isTailwindUsed, loadPreviousInventory, compileTypeScript, detectExistingBundles, autoInjectBundles, getBundlePathsForPage, formatBytes, isVirtualPage, getPageOutputFile, } from './utils/index.js';
|
|
2
2
|
import { join, dirname, relative, posix } from 'node:path';
|
|
3
3
|
import { performance } from 'node:perf_hooks';
|
|
4
4
|
import { loadConfig } from '../config/loader.js';
|
|
5
5
|
import { loadContent } from './content.js';
|
|
6
|
+
import { expandPages } from './expand.js';
|
|
7
|
+
import { createSummaryHasher, resolveCollectionPath, resolveSummaryFields, } from './collections.js';
|
|
6
8
|
import { createMarkdownProcessor, renderMarkdown, extractToc } from './markdown.js';
|
|
7
9
|
import { createTemplateEngine, renderPage } from './templates.js';
|
|
8
10
|
import { buildNavigation } from './navigation.js';
|
|
9
|
-
import { loadCacheManifest, saveCacheManifest, shouldRebuildPage, createCacheEntry, updateCacheEntry, withBuildLock, computeNavigationHash, computeContentHash, clearFileHashCache, clearTemplatePathCache, } from './isg/index.js';
|
|
11
|
+
import { loadCacheManifest, saveCacheManifest, shouldRebuildPage, createCacheEntry, updateCacheEntry, hasRecordedReads, pruneOrphanedPages, withBuildLock, computeNavigationHash, computeContentHash, clearFileHashCache, clearTemplatePathCache, } from './isg/index.js';
|
|
10
12
|
import { generateSitemap, generateRobotsTxtFromConfig, autoInjectSEO, } from '../seo/index.js';
|
|
11
13
|
import { generateRSSFeeds, validateRSSConfig } from '../rss/index.js';
|
|
12
|
-
import { computeSearchIndexFilename, generateSearchIndex, writeSearchIndex, autoInjectSearchMeta, } from '../search/index.js';
|
|
14
|
+
import { computeSearchIndexFilename, generateSearchIndex, writeSearchIndex, autoInjectSearchMeta, isGeneratedPageExcludedFromSearch, } from '../search/index.js';
|
|
13
15
|
import { getEnv, isDevelopment } from '../env.js';
|
|
14
16
|
import { DEFAULT_OUT_DIR } from '../constants.js';
|
|
15
17
|
import { ImageRuntime } from '../images/runtime.js';
|
|
@@ -180,12 +182,18 @@ async function setupCacheAndManifest(cacheDir) {
|
|
|
180
182
|
return { manifest };
|
|
181
183
|
}
|
|
182
184
|
/**
|
|
183
|
-
* Loads content and builds navigation.
|
|
185
|
+
* Loads content, expands generated and paginated pages, and builds navigation.
|
|
184
186
|
*/
|
|
185
187
|
async function loadContentAndBuildNavigation(config, options, logger) {
|
|
186
|
-
// Load all content
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
// Load all content, then replace [param].md templates with their pages and add the
|
|
189
|
+
// pages of paginated listings. Navigation, SEO, sitemap and search see the result.
|
|
190
|
+
const loaded = await loadContent(config, options.includeDrafts);
|
|
191
|
+
const pages = await expandPages(loaded, config, logger);
|
|
192
|
+
const virtualCount = pages.filter((page) => isVirtualPage(page)).length;
|
|
193
|
+
logger.status(`Found ${pages.length - virtualCount} pages`);
|
|
194
|
+
if (virtualCount > 0) {
|
|
195
|
+
logger.status(`Generated ${virtualCount} more from templates and pagination`);
|
|
196
|
+
}
|
|
189
197
|
// Build navigation from pages
|
|
190
198
|
if (logger.step) {
|
|
191
199
|
console.log(); // Add spacing before navigation step
|
|
@@ -202,31 +210,35 @@ async function loadContentAndBuildNavigation(config, options, logger) {
|
|
|
202
210
|
// Create processors
|
|
203
211
|
const md = await createMarkdownProcessor(config);
|
|
204
212
|
const eta = createTemplateEngine(config);
|
|
205
|
-
return { pages, navigation, md, eta, navigationHash };
|
|
213
|
+
return { pages, markdownFiles: loaded.length, navigation, md, eta, navigationHash };
|
|
206
214
|
}
|
|
207
215
|
/**
|
|
208
216
|
* Output file and ISG cache key of a page.
|
|
209
217
|
*/
|
|
210
218
|
function getPageOutput(page, outDir) {
|
|
211
|
-
|
|
212
|
-
if (page.url === '/') {
|
|
213
|
-
outputPath = join(outDir, 'index.html');
|
|
214
|
-
}
|
|
215
|
-
else if (page.url.endsWith('/')) {
|
|
216
|
-
outputPath = join(outDir, page.url, 'index.html');
|
|
217
|
-
}
|
|
218
|
-
else {
|
|
219
|
-
outputPath = join(outDir, `${page.url}.html`);
|
|
220
|
-
}
|
|
219
|
+
const outputPath = join(outDir, getPageOutputFile(page.url));
|
|
221
220
|
// Cache key is the output path relative to outDir
|
|
222
221
|
const relativePath = relative(outDir, outputPath).replace(/\\/g, '/');
|
|
223
222
|
const cacheKey = relativePath.startsWith('/') ? relativePath : `/${relativePath}`;
|
|
224
223
|
return { outputPath, cacheKey };
|
|
225
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Hashes a value for the render inputs, or returns a value that never matches when it
|
|
227
|
+
* cannot be serialized, so the page rebuilds instead of keeping stale output.
|
|
228
|
+
*/
|
|
229
|
+
function hashableData(value) {
|
|
230
|
+
try {
|
|
231
|
+
JSON.stringify(value);
|
|
232
|
+
return value ?? null;
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return `unserializable:${Date.now()}:${Math.random()}`;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
226
238
|
/**
|
|
227
239
|
* Processes pages with ISG caching logic.
|
|
228
240
|
*/
|
|
229
|
-
async function processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder = noopMetricRecorder, searchIndexFilename, imageRuntime = null) {
|
|
241
|
+
async function processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder = noopMetricRecorder, searchIndexFilename, imageRuntime = null, navigationHash = computeNavigationHash(navigation)) {
|
|
230
242
|
let cacheHits = 0;
|
|
231
243
|
let cacheMisses = 0;
|
|
232
244
|
const searchablePages = [];
|
|
@@ -238,6 +250,18 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
|
|
|
238
250
|
await config.hooks.beforeAll(buildContext);
|
|
239
251
|
endHookBeforeAll();
|
|
240
252
|
}
|
|
253
|
+
// Collection summaries, hashed after beforeAll because the hook can change front matter
|
|
254
|
+
const summaries = createSummaryHasher(pages, resolveSummaryFields(config));
|
|
255
|
+
// The pages of a listing share one array of page URLs, hashed once for all of them
|
|
256
|
+
const urlListHashes = new WeakMap();
|
|
257
|
+
const hashUrlList = (urls) => {
|
|
258
|
+
let hash = urlListHashes.get(urls);
|
|
259
|
+
if (hash === undefined) {
|
|
260
|
+
hash = computeContentHash('', { urls });
|
|
261
|
+
urlListHashes.set(urls, hash);
|
|
262
|
+
}
|
|
263
|
+
return hash;
|
|
264
|
+
};
|
|
241
265
|
// Render each page with progress tracking and ISG
|
|
242
266
|
if (logger.step) {
|
|
243
267
|
logger.step(2, 3, 'Rendering pages');
|
|
@@ -273,18 +297,56 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
|
|
|
273
297
|
searchIndexPath: `/${searchIndexFilename}`,
|
|
274
298
|
}),
|
|
275
299
|
};
|
|
276
|
-
|
|
300
|
+
// Collection the page sees as stati.collection; a page whose collection appears,
|
|
301
|
+
// disappears or changes rebuilds even when it never read it
|
|
302
|
+
const collectionPath = resolveCollectionPath(page, pages);
|
|
303
|
+
const staticRenderInputs = {
|
|
277
304
|
...assets,
|
|
278
305
|
bundleAutoInject: config.typescript?.autoInject !== false,
|
|
279
306
|
searchAutoInject: config.search?.autoInjectMetaTag !== false,
|
|
280
307
|
// Omitted with images: false, so the hash matches builds without the image pipeline
|
|
281
308
|
...(imageFingerprint && { images: imageFingerprint }),
|
|
309
|
+
collection: collectionPath ?? null,
|
|
310
|
+
// A listing page depends on its own slice and on the page count, which every page
|
|
311
|
+
// shows in its numbered links
|
|
312
|
+
...(page.pagination && {
|
|
313
|
+
pagination: {
|
|
314
|
+
pageNumber: page.pagination.pageNumber,
|
|
315
|
+
totalPages: page.pagination.totalPages,
|
|
316
|
+
urls: hashUrlList(page.pagination.urls),
|
|
317
|
+
items: summaries.pages(page.pagination.items),
|
|
318
|
+
},
|
|
319
|
+
}),
|
|
320
|
+
// A generated page depends on its own items only, not on the whole collection
|
|
321
|
+
...(page.generated && {
|
|
322
|
+
generated: {
|
|
323
|
+
key: page.generated.key,
|
|
324
|
+
label: page.generated.label,
|
|
325
|
+
template: page.generated.template,
|
|
326
|
+
items: summaries.pages(page.generated.items),
|
|
327
|
+
data: hashableData(page.generated.data),
|
|
328
|
+
},
|
|
329
|
+
}),
|
|
330
|
+
};
|
|
331
|
+
// The data a render read: the collection summary and the navigation tree
|
|
332
|
+
const renderInputsHashFor = (reads) => computeContentHash('', {
|
|
333
|
+
...staticRenderInputs,
|
|
334
|
+
...(reads?.collection && {
|
|
335
|
+
collectionSummary: collectionPath !== undefined ? summaries.collection(collectionPath) : 'none',
|
|
336
|
+
}),
|
|
337
|
+
...(reads?.nav && { navigation: navigationHash }),
|
|
282
338
|
});
|
|
339
|
+
// Entries written before Stati recorded what pages read are rebuilt once to record it
|
|
340
|
+
const readsKnown = existingEntry !== undefined && hasRecordedReads(existingEntry);
|
|
341
|
+
const renderInputsHash = renderInputsHashFor(readsKnown
|
|
342
|
+
? { collection: existingEntry.readsCollection, nav: existingEntry.readsNav }
|
|
343
|
+
: undefined);
|
|
283
344
|
// Check if we should rebuild this page (considering ISG logic)
|
|
284
345
|
const shouldRebuildStart = performance.now();
|
|
285
346
|
const shouldRebuild = options.force ||
|
|
286
347
|
config.isg?.enabled === false ||
|
|
287
348
|
!(await pathExists(outputPath)) ||
|
|
349
|
+
(existingEntry !== undefined && !readsKnown) ||
|
|
288
350
|
// A cached page whose generated images are gone is rebuilt to regenerate them
|
|
289
351
|
(imageRuntime !== null && !imageRuntime.outputsExist(existingEntry?.images)) ||
|
|
290
352
|
(await shouldRebuildPage(page, existingEntry, config, pages, buildTime, renderInputsHash));
|
|
@@ -300,7 +362,7 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
|
|
|
300
362
|
recorder.recordPageTiming(page.url, 0, true);
|
|
301
363
|
// Collect searchable page data for cached pages if search is enabled
|
|
302
364
|
// Extract TOC without full HTML rendering for efficiency
|
|
303
|
-
if (config.search?.enabled === true) {
|
|
365
|
+
if (config.search?.enabled === true && !isGeneratedPageExcludedFromSearch(page)) {
|
|
304
366
|
const tocEnabled = config.markdown?.toc !== false;
|
|
305
367
|
const toc = tocEnabled ? extractToc(page.content, md) : [];
|
|
306
368
|
searchablePages.push({ page, toc, markdownContent: page.content });
|
|
@@ -357,10 +419,17 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
|
|
|
357
419
|
if (config.typescript?.autoInject !== false && assets.bundlePaths.length > 0) {
|
|
358
420
|
finalHtml = autoInjectBundles(finalHtml, assets.bundlePaths);
|
|
359
421
|
}
|
|
360
|
-
// Start the image jobs the final HTML needs and record its image files for the cache
|
|
361
|
-
|
|
422
|
+
// Start the image jobs the final HTML needs and record its image files for the cache,
|
|
423
|
+
// along with what the render read, which decides the hash it is stored under
|
|
424
|
+
const committedImages = pageImages && imageRuntime
|
|
362
425
|
? imageRuntime.commitPage(pageImages, finalHtml, { outputPath, cacheKey })
|
|
363
426
|
: undefined;
|
|
427
|
+
const renderedAssets = {
|
|
428
|
+
assetDeps: committedImages?.assetDeps ?? [],
|
|
429
|
+
images: committedImages?.images ?? [],
|
|
430
|
+
reads: renderResult.reads,
|
|
431
|
+
};
|
|
432
|
+
const renderedInputsHash = renderInputsHashFor(renderResult.reads);
|
|
364
433
|
const renderTime = Math.round(performance.now() - startTime);
|
|
365
434
|
// Record page timing for rendered pages (includes template count)
|
|
366
435
|
recorder.recordPageTiming(page.url, renderTime, false, renderResult.templatesLoaded);
|
|
@@ -370,7 +439,13 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
|
|
|
370
439
|
// In dev mode, batch writes for parallel execution to avoid Windows filesystem slowdown
|
|
371
440
|
if (isDevMode) {
|
|
372
441
|
pendingWrites.push({ outputPath, content: finalHtml });
|
|
373
|
-
pendingCacheUpdates.push({
|
|
442
|
+
pendingCacheUpdates.push({
|
|
443
|
+
cacheKey,
|
|
444
|
+
page,
|
|
445
|
+
existingEntry,
|
|
446
|
+
renderInputsHash: renderedInputsHash,
|
|
447
|
+
renderedAssets,
|
|
448
|
+
});
|
|
374
449
|
}
|
|
375
450
|
else {
|
|
376
451
|
// Production mode: write immediately
|
|
@@ -382,17 +457,17 @@ async function processPagesWithCache(pages, manifest, config, outDir, md, eta, n
|
|
|
382
457
|
if (!options.skipCacheUpdate) {
|
|
383
458
|
const cacheEntryStart = performance.now();
|
|
384
459
|
if (existingEntry) {
|
|
385
|
-
manifest.entries[cacheKey] = await updateCacheEntry(existingEntry, page, config, pages, buildTime, false,
|
|
460
|
+
manifest.entries[cacheKey] = await updateCacheEntry(existingEntry, page, config, pages, buildTime, false, renderedInputsHash, renderedAssets);
|
|
386
461
|
}
|
|
387
462
|
else {
|
|
388
|
-
manifest.entries[cacheKey] = await createCacheEntry(page, config, pages, buildTime,
|
|
463
|
+
manifest.entries[cacheKey] = await createCacheEntry(page, config, pages, buildTime, renderedInputsHash, renderedAssets);
|
|
389
464
|
}
|
|
390
465
|
totalCacheEntryTime += performance.now() - cacheEntryStart;
|
|
391
466
|
}
|
|
392
467
|
}
|
|
393
468
|
// Collect searchable page data if search is enabled
|
|
394
469
|
// Uses TOC entries and markdown content instead of parsing rendered HTML
|
|
395
|
-
if (config.search?.enabled === true) {
|
|
470
|
+
if (config.search?.enabled === true && !isGeneratedPageExcludedFromSearch(page)) {
|
|
396
471
|
searchablePages.push({ page, toc, markdownContent: page.content });
|
|
397
472
|
}
|
|
398
473
|
// Run afterRender hook
|
|
@@ -567,14 +642,13 @@ async function buildInternal(options = {}) {
|
|
|
567
642
|
console.log(); // Add spacing before content loading
|
|
568
643
|
}
|
|
569
644
|
const endContentSpan = recorder.startSpan('contentDiscoveryMs');
|
|
570
|
-
const { pages, navigation, md, eta, navigationHash } = await loadContentAndBuildNavigation(config, options, logger);
|
|
645
|
+
const { pages, markdownFiles, navigation, md, eta, navigationHash } = await loadContentAndBuildNavigation(config, options, logger);
|
|
571
646
|
endContentSpan();
|
|
572
|
-
// Record content discovery counts.
|
|
573
|
-
//
|
|
574
|
-
// because loadContent() only processes *.md files, so all pages are markdown files.
|
|
647
|
+
// Record content discovery counts. Pages include the ones generated from [param].md
|
|
648
|
+
// templates and pagination; Markdown files include the templates.
|
|
575
649
|
recorder.increment('totalPages', pages.length);
|
|
576
|
-
recorder.increment('markdownFilesProcessed',
|
|
577
|
-
//
|
|
650
|
+
recorder.increment('markdownFilesProcessed', markdownFiles);
|
|
651
|
+
// Recorded in the manifest; pages that read the navigation also hash it into their inputs
|
|
578
652
|
manifest.navigationHash = navigationHash;
|
|
579
653
|
// Compile TypeScript if enabled
|
|
580
654
|
// In dev mode, skip compilation since esbuild watcher handles it
|
|
@@ -626,9 +700,23 @@ async function buildInternal(options = {}) {
|
|
|
626
700
|
}
|
|
627
701
|
const endPageRenderSpan = recorder.startSpan('pageRenderingMs');
|
|
628
702
|
const buildTime = new Date();
|
|
629
|
-
const pageProcessingResult = await processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder, searchIndexFilename, imageRuntime);
|
|
703
|
+
const pageProcessingResult = await processPagesWithCache(pages, manifest, config, outDir, md, eta, navigation, buildTime, options, logger, compiledBundles, recorder, searchIndexFilename, imageRuntime, navigationHash);
|
|
630
704
|
endPageRenderSpan();
|
|
631
705
|
const { cacheHits, cacheMisses, searchablePages } = pageProcessingResult;
|
|
706
|
+
// Remove the output of pages that no longer exist, such as a deleted post or a tag no post
|
|
707
|
+
// uses anymore. Runs before static assets are copied, so a file in the static directory at
|
|
708
|
+
// the same path is copied afterwards.
|
|
709
|
+
const liveCacheKeys = new Set(pages.map((page) => getPageOutput(page, outDir).cacheKey));
|
|
710
|
+
const pruned = await pruneOrphanedPages(manifest, liveCacheKeys, outDir);
|
|
711
|
+
for (const failure of pruned.failed) {
|
|
712
|
+
logger.warning(`Could not remove the stale page ${failure.key}: ${failure.message}`);
|
|
713
|
+
}
|
|
714
|
+
if (pruned.removed.length > 0) {
|
|
715
|
+
const count = pruned.removed.length;
|
|
716
|
+
logger.info(count === 1
|
|
717
|
+
? 'Removed 1 page that no longer exists'
|
|
718
|
+
: `Removed ${count} pages that no longer exist`);
|
|
719
|
+
}
|
|
632
720
|
// Wait for image jobs; pages whose images failed are rewritten to show plain images
|
|
633
721
|
let imageFailures = [];
|
|
634
722
|
if (imageRuntime) {
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { PageModel, CollectionData, PaginationData, GeneratedPageData, StatiConfig } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Front matter fields of the collection summary when `collections.summaryFields` is not set.
|
|
4
|
+
*/
|
|
5
|
+
export declare const DEFAULT_SUMMARY_FIELDS: readonly string[];
|
|
6
|
+
/**
|
|
7
|
+
* The collections of a page list.
|
|
8
|
+
*/
|
|
9
|
+
export interface CollectionIndex {
|
|
10
|
+
/** Collection data by the URL of its index page */
|
|
11
|
+
byPath: Map<string, CollectionData>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* What a render read from data shared between pages. Filled in while the page renders.
|
|
15
|
+
*/
|
|
16
|
+
export interface PageReads {
|
|
17
|
+
/** The page read `stati.collection` */
|
|
18
|
+
collection: boolean;
|
|
19
|
+
/** The page read the navigation tree (`stati.nav` or `stati.page.navNode`) */
|
|
20
|
+
nav: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Returns the collections of a page list, computing them once per array.
|
|
24
|
+
*
|
|
25
|
+
* @param allPages - All pages of the build
|
|
26
|
+
* @returns The collections by index page URL
|
|
27
|
+
*/
|
|
28
|
+
export declare function getCollections(allPages: PageModel[]): CollectionIndex;
|
|
29
|
+
/**
|
|
30
|
+
* Computes every collection of a page list in one pass.
|
|
31
|
+
*
|
|
32
|
+
* A collection belongs to a collection index page (see `isCollectionIndexPage`), or to a page
|
|
33
|
+
* loaded from an `index.md`, whose collection is empty while its folder holds no pages. The
|
|
34
|
+
* root collection holds the top-level pages; any other collection holds every page below its
|
|
35
|
+
* folder, at any depth. The index page itself, generated pages and pagination pages are
|
|
36
|
+
* never members.
|
|
37
|
+
*
|
|
38
|
+
* @param allPages - All pages of the build
|
|
39
|
+
* @returns The collections by index page URL
|
|
40
|
+
*/
|
|
41
|
+
export declare function buildCollections(allPages: PageModel[]): CollectionIndex;
|
|
42
|
+
/**
|
|
43
|
+
* Sorts pages by publishedAt date (most recent first), falling back to alphabetical by title.
|
|
44
|
+
*
|
|
45
|
+
* @param pages - Pages to sort, in place
|
|
46
|
+
* @returns The sorted array
|
|
47
|
+
*/
|
|
48
|
+
export declare function sortPagesByDate(pages: PageModel[]): PageModel[];
|
|
49
|
+
/**
|
|
50
|
+
* Finds the collection a `collection` front matter value names. Accepts the index page URL
|
|
51
|
+
* with or without a leading or trailing slash: `posts`, `/posts` and `/posts/` all name the
|
|
52
|
+
* collection of `site/posts/index.md`.
|
|
53
|
+
*
|
|
54
|
+
* @param index - The collections of the build
|
|
55
|
+
* @param value - The front matter value
|
|
56
|
+
* @returns The index page URL, or undefined when the value names no collection
|
|
57
|
+
*/
|
|
58
|
+
export declare function findCollectionPath(index: CollectionIndex, value: unknown): string | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Finds the collection a page reads as `stati.collection`:
|
|
61
|
+
*
|
|
62
|
+
* 1. The collection its `collection` front matter field names, when that collection exists.
|
|
63
|
+
* 2. Its own collection, when the page is a collection index page or an `index.md`, even
|
|
64
|
+
* when that collection is empty.
|
|
65
|
+
* 3. Otherwise the nearest collection with pages that the page belongs to: `/blog/post` reads
|
|
66
|
+
* `/blog`, and a top-level page reads the root collection.
|
|
67
|
+
*
|
|
68
|
+
* Pagination pages after the first read the collection of the first page. Generated pages
|
|
69
|
+
* read only the collection their template names; expansion sets it in their front matter.
|
|
70
|
+
*
|
|
71
|
+
* @param page - The page
|
|
72
|
+
* @param allPages - All pages of the build
|
|
73
|
+
* @returns The index page URL of the collection, or undefined when the page has none
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolveCollectionPath(page: PageModel, allPages: PageModel[]): string | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Returns the front matter fields of the collection summary.
|
|
78
|
+
*
|
|
79
|
+
* @param config - Stati configuration
|
|
80
|
+
* @returns `collections.summaryFields`, or the default fields
|
|
81
|
+
*/
|
|
82
|
+
export declare function resolveSummaryFields(config: StatiConfig): readonly string[];
|
|
83
|
+
/**
|
|
84
|
+
* Hashes what a listing shows of a list of pages: each page's URL, publish date and summary
|
|
85
|
+
* fields, in list order. The Markdown body only counts when `fields` includes `content`.
|
|
86
|
+
*
|
|
87
|
+
* @param pages - The listed pages
|
|
88
|
+
* @param fields - Front matter fields to include
|
|
89
|
+
* @returns A `sha256-…` hash
|
|
90
|
+
*/
|
|
91
|
+
export declare function computePagesSummaryHash(pages: readonly PageModel[], fields: readonly string[]): string;
|
|
92
|
+
/**
|
|
93
|
+
* Collection summary hashes for one build, computed at most once per collection.
|
|
94
|
+
*/
|
|
95
|
+
export interface SummaryHasher {
|
|
96
|
+
/** Summary hash of a collection, or `'none'` when there is no such collection */
|
|
97
|
+
collection(path: string): string;
|
|
98
|
+
/** Summary hash of a list of pages */
|
|
99
|
+
pages(pages: readonly PageModel[]): string;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Creates the summary hasher of a build. Call it after the `beforeAll` hook, which can change
|
|
103
|
+
* the front matter the summaries include.
|
|
104
|
+
*
|
|
105
|
+
* @param allPages - All pages of the build
|
|
106
|
+
* @param fields - Summary fields
|
|
107
|
+
* @returns The hasher
|
|
108
|
+
*/
|
|
109
|
+
export declare function createSummaryHasher(allPages: PageModel[], fields: readonly string[]): SummaryHasher;
|
|
110
|
+
/**
|
|
111
|
+
* Wraps collection data for one render. Reading any of its properties sets `reads.collection`,
|
|
112
|
+
* so the cache knows the page depends on the collection. Arrays and objects are copied on
|
|
113
|
+
* first read, so a template that sorts or changes them cannot affect other pages, which share
|
|
114
|
+
* the collection data within a build.
|
|
115
|
+
*
|
|
116
|
+
* The properties are accessors on a plain object, not a Proxy, so spreading the template
|
|
117
|
+
* context (as callable partials do) passes the view along without reading it.
|
|
118
|
+
*
|
|
119
|
+
* @param data - The shared collection data
|
|
120
|
+
* @param reads - The render's read flags
|
|
121
|
+
* @returns The view for `stati.collection`
|
|
122
|
+
*/
|
|
123
|
+
export declare function createCollectionView(data: CollectionData, reads: PageReads): CollectionData;
|
|
124
|
+
/**
|
|
125
|
+
* Copies a page's pagination data for `stati.pagination`, so a template that changes the
|
|
126
|
+
* lists cannot change what the cache hashes.
|
|
127
|
+
*
|
|
128
|
+
* @param data - The page's pagination data
|
|
129
|
+
* @returns A copy with its own arrays
|
|
130
|
+
*/
|
|
131
|
+
export declare function copyPaginationData(data: PaginationData): PaginationData;
|
|
132
|
+
/**
|
|
133
|
+
* Copies a page's generated data for `stati.generated`.
|
|
134
|
+
*
|
|
135
|
+
* @param data - The page's generated data
|
|
136
|
+
* @returns A copy with its own items array
|
|
137
|
+
*/
|
|
138
|
+
export declare function copyGeneratedData(data: GeneratedPageData): GeneratedPageData;
|
|
139
|
+
//# sourceMappingURL=collections.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collections.d.ts","sourceRoot":"","sources":["../../src/core/collections.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,SAAS,EACT,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,WAAW,EACZ,MAAM,mBAAmB,CAAC;AAU3B;;GAEG;AACH,eAAO,MAAM,sBAAsB,EAAE,SAAS,MAAM,EAA8C,CAAC;AAEnG;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,mDAAmD;IACnD,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,uCAAuC;IACvC,UAAU,EAAE,OAAO,CAAC;IACpB,8EAA8E;IAC9E,GAAG,EAAE,OAAO,CAAC;CACd;AASD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,eAAe,CAQrE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,eAAe,CAwBvE;AAwFD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,SAAS,EAAE,CAc/D;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAc7F;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,MAAM,GAAG,SAAS,CAuBhG;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,MAAM,EAAE,CAM3E;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,SAAS,SAAS,EAAE,EAC3B,MAAM,EAAE,SAAS,MAAM,EAAE,GACxB,MAAM,CAWR;AAMD;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,iFAAiF;IACjF,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACjC,sCAAsC;IACtC,KAAK,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,GAAG,MAAM,CAAC;CAC5C;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,SAAS,EAAE,EACrB,MAAM,EAAE,SAAS,MAAM,EAAE,GACxB,aAAa,CAcf;AAID;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,SAAS,GAAG,cAAc,CAwB3F;AAaD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,cAAc,GAAG,cAAc,CAEvE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,iBAAiB,GAAG,iBAAiB,CAE5E"}
|