@riebeckite/honox 0.0.13 → 0.0.15

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/dist/index.js CHANGED
@@ -255,11 +255,41 @@ function isJavaScriptModule(filePath) {
255
255
  }
256
256
 
257
257
  // src/scaffold/index.ts
258
- import fs7 from "node:fs/promises";
259
- import path7 from "node:path";
258
+ import fs8 from "node:fs/promises";
259
+ import path9 from "node:path";
260
+
261
+ // src/scaffold/wrangler-defaults.ts
262
+ var WRANGLER_DEFAULTS = {
263
+ /** JSON schema for IDE support and validation */
264
+ $schema: "node_modules/wrangler/config-schema.json",
265
+ /** Worker name - becomes <name>.<account>.workers.dev */
266
+ name: "riebeckite-site",
267
+ /** Compatibility date - update when Workers runtime changes require it */
268
+ compatibility_date: "2026-06-09",
269
+ /** Required for Node.js APIs used by Riebeckite */
270
+ compatibility_flags: ["nodejs_compat"],
271
+ /** Static assets directory - Riebeckite builds to ./dist */
272
+ assets: { directory: "./dist" }
273
+ };
274
+ var GITHUB_ACTIONS_SECRETS = {
275
+ /** Cloudflare API token with Workers Scripts: Edit permission */
276
+ CLOUDFLARE_API_TOKEN: "CLOUDFLARE_API_TOKEN",
277
+ /** Cloudflare Account ID (found in Workers & Pages dashboard URL) */
278
+ CLOUDFLARE_ACCOUNT_ID: "CLOUDFLARE_ACCOUNT_ID",
279
+ /** Optional: Token for content repo to dispatch to site repo */
280
+ SITE_DISPATCH_TOKEN: "SITE_DISPATCH_TOKEN",
281
+ /** Optional: Token for site repo to read content repo */
282
+ RIEBECKITE_CONTENT_READ_TOKEN: "RIEBECKITE_CONTENT_READ_TOKEN"
283
+ };
260
284
 
261
285
  // src/scaffold/deployment.ts
262
286
  var GITHUB_REPOSITORY_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
287
+ var BUILD_STATE_CACHE_KEY_PREFIX = "riebeckite-build-v1";
288
+ var LEGACY_BUILD_CACHE_KEY_PREFIX = "riebeckite-content-v3";
289
+ var BUILD_STATE_CACHE_PATHS = [
290
+ ".riebeckite/cache",
291
+ ".riebeckite/build/content-state.json"
292
+ ];
263
293
  function assertGitHubRepository(value, option) {
264
294
  if (!GITHUB_REPOSITORY_PATTERN.test(value)) {
265
295
  throw new Error(
@@ -295,16 +325,7 @@ function deploymentTemplateFiles(options) {
295
325
  ];
296
326
  }
297
327
  function wranglerConfig() {
298
- return `${JSON.stringify(
299
- {
300
- name: "riebeckite-site",
301
- compatibility_date: "2026-03-10",
302
- compatibility_flags: ["nodejs_compat"],
303
- assets: { directory: "./dist" }
304
- },
305
- null,
306
- 2
307
- )}
328
+ return `${JSON.stringify(WRANGLER_DEFAULTS, null, 2)}
308
329
  `;
309
330
  }
310
331
  function deployWorkflow(contentRepository) {
@@ -347,6 +368,16 @@ ${contentCheckout}
347
368
  - name: Install dependencies
348
369
  run: npm ci
349
370
 
371
+ - name: Restore Riebeckite build state
372
+ uses: actions/cache@v4
373
+ with:
374
+ path: |
375
+ ${BUILD_STATE_CACHE_PATHS.map((entry) => ` ${entry}`).join("\n")}
376
+ key: ${BUILD_STATE_CACHE_KEY_PREFIX}-\${{ runner.os }}-\${{ hashFiles('package-lock.json') }}-\${{ github.run_id }}-\${{ github.run_attempt }}
377
+ restore-keys: |
378
+ ${BUILD_STATE_CACHE_KEY_PREFIX}-\${{ runner.os }}-\${{ hashFiles('package-lock.json') }}-
379
+ ${LEGACY_BUILD_CACHE_KEY_PREFIX}-\${{ runner.os }}-\${{ hashFiles('package-lock.json') }}
380
+
350
381
  - name: Validate configuration and plugins
351
382
  run: npm exec riebeckite check
352
383
 
@@ -356,8 +387,8 @@ ${contentCheckout}
356
387
  - name: Deploy the built assets
357
388
  uses: cloudflare/wrangler-action@v3
358
389
  with:
359
- apiToken: \${{ secrets.CLOUDFLARE_API_TOKEN }}
360
- accountId: \${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
390
+ apiToken: \${{ secrets.${GITHUB_ACTIONS_SECRETS.CLOUDFLARE_API_TOKEN} }}
391
+ accountId: \${{ secrets.${GITHUB_ACTIONS_SECRETS.CLOUDFLARE_ACCOUNT_ID} }}
361
392
  command: deploy
362
393
  `;
363
394
  }
@@ -377,7 +408,7 @@ jobs:
377
408
  steps:
378
409
  - name: Validate dispatch configuration
379
410
  env:
380
- SITE_DISPATCH_TOKEN: \${{ secrets.SITE_DISPATCH_TOKEN }}
411
+ SITE_DISPATCH_TOKEN: \${{ secrets.${GITHUB_ACTIONS_SECRETS.SITE_DISPATCH_TOKEN} }}
381
412
  run: |
382
413
  if [ -z "$SITE_DISPATCH_TOKEN" ]; then
383
414
  echo "::error::SITE_DISPATCH_TOKEN is not configured. Add a token that can dispatch to ${siteRepository}."
@@ -387,7 +418,7 @@ jobs:
387
418
  - name: Notify the site repository
388
419
  uses: actions/github-script@v7
389
420
  with:
390
- github-token: \${{ secrets.SITE_DISPATCH_TOKEN }}
421
+ github-token: \${{ secrets.${GITHUB_ACTIONS_SECRETS.SITE_DISPATCH_TOKEN} }}
391
422
  script: |
392
423
  await github.rest.repos.createDispatchEvent({
393
424
  owner: ${JSON.stringify(owner)},
@@ -924,10 +955,10 @@ function resolveScaffoldPreset(value) {
924
955
  }
925
956
 
926
957
  // src/scaffold/templates.ts
927
- import fs6 from "node:fs";
958
+ import fs7 from "node:fs";
928
959
 
929
960
  // src/vite_plugin.ts
930
- import path6 from "node:path";
961
+ import path8 from "node:path";
931
962
 
932
963
  // src/asset_entries.ts
933
964
  import fs4 from "node:fs";
@@ -1037,11 +1068,288 @@ ${initializers.map((initializer) => ` ${initializer};`).join("\n")}
1037
1068
  `;
1038
1069
  }
1039
1070
 
1071
+ // src/content_assets.ts
1072
+ import fs5 from "node:fs/promises";
1073
+ import path6 from "node:path";
1074
+ import {
1075
+ isIgnoredContentPath as isIgnoredContentPath2,
1076
+ isImagePath as isImagePath2,
1077
+ normalizeContentPath as normalizeContentPath2
1078
+ } from "@riebeckite/core";
1079
+
1080
+ // src/content_watch.ts
1081
+ import path5 from "node:path";
1082
+ import {
1083
+ isAttachmentPath,
1084
+ isIgnoredContentPath,
1085
+ isImagePath,
1086
+ normalizeContentPath
1087
+ } from "@riebeckite/core";
1088
+ var CONTENT_RELOAD_DEBOUNCE_MS = 60;
1089
+ var configuredWatchers = /* @__PURE__ */ new WeakSet();
1090
+ function riebeckiteContentWatch(options) {
1091
+ return {
1092
+ name: "riebeckite-content-watch",
1093
+ apply: "serve",
1094
+ enforce: "post",
1095
+ configureServer(server2) {
1096
+ if (configuredWatchers.has(server2.watcher)) return;
1097
+ configuredWatchers.add(server2.watcher);
1098
+ const appRoot = options.appRoot();
1099
+ const contentRoot = options.contentRoot();
1100
+ const exclude = options.exclude();
1101
+ const isContentFile = (file) => isPathInsideDirectory(contentRoot, file);
1102
+ server2.watcher.add(contentRoot);
1103
+ suppressRestartOnContentEvents(server2, isContentFile);
1104
+ let timer;
1105
+ const invalidate = () => {
1106
+ if (timer) clearTimeout(timer);
1107
+ timer = setTimeout(() => {
1108
+ timer = void 0;
1109
+ invalidateApplicationRuntime(server2, appRoot);
1110
+ server2.hot.send({ type: "full-reload" });
1111
+ }, CONTENT_RELOAD_DEBOUNCE_MS);
1112
+ };
1113
+ server2.httpServer?.once("close", () => {
1114
+ if (timer) clearTimeout(timer);
1115
+ });
1116
+ const handle = (event) => (file) => {
1117
+ if (!isContentFile(file)) return;
1118
+ const logicalPath = toLogicalPath(contentRoot, file);
1119
+ if (isIgnoredContentPath(logicalPath, exclude)) return;
1120
+ if (event === "change" && isSiteAssetPath(logicalPath)) {
1121
+ server2.hot.send({ type: "full-reload" });
1122
+ return;
1123
+ }
1124
+ invalidate();
1125
+ };
1126
+ server2.watcher.on("add", handle("add"));
1127
+ server2.watcher.on("change", handle("change"));
1128
+ server2.watcher.on("unlink", handle("unlink"));
1129
+ }
1130
+ };
1131
+ }
1132
+ function isSiteAssetPath(logicalPath) {
1133
+ return isImagePath(logicalPath) || isAttachmentPath(logicalPath);
1134
+ }
1135
+ function isPathInsideDirectory(directory, file) {
1136
+ const root = normalizeForCompare(directory).replace(/\/+$/, "");
1137
+ if (!root) return false;
1138
+ const target = normalizeForCompare(file);
1139
+ return target === root || target.startsWith(`${root}/`);
1140
+ }
1141
+ function toLogicalPath(directory, file) {
1142
+ return normalizeContentPath(
1143
+ path5.relative(normalizeContentPath(directory), normalizeContentPath(file))
1144
+ );
1145
+ }
1146
+ function invalidateApplicationRuntime(server2, appRoot) {
1147
+ const moduleGraph = server2.environments?.ssr?.moduleGraph;
1148
+ if (!moduleGraph) return 0;
1149
+ const appDirectory = path5.join(appRoot, "app") + path5.sep;
1150
+ const seen = /* @__PURE__ */ new Set();
1151
+ const timestamp = Date.now();
1152
+ let invalidated = 0;
1153
+ for (const mod of moduleGraph.idToModuleMap.values()) {
1154
+ const file = mod.file;
1155
+ if (!file || !isPathInsideDirectory(appDirectory, file)) continue;
1156
+ moduleGraph.invalidateModule(mod, seen, timestamp);
1157
+ invalidated += 1;
1158
+ }
1159
+ return invalidated;
1160
+ }
1161
+ function suppressRestartOnContentEvents(server2, isContentFile) {
1162
+ for (const event of ["add", "unlink"]) {
1163
+ const listeners = server2.watcher.rawListeners(event).slice();
1164
+ server2.watcher.removeAllListeners(event);
1165
+ for (const listener of listeners) {
1166
+ server2.watcher.on(event, (file) => {
1167
+ if (isContentFile(file)) return;
1168
+ listener(file);
1169
+ });
1170
+ }
1171
+ }
1172
+ }
1173
+ function normalizeForCompare(value) {
1174
+ const normalized = normalizeContentPath(value);
1175
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
1176
+ }
1177
+
1178
+ // src/content_assets.ts
1179
+ var IMAGE_CONTENT_TYPES = {
1180
+ png: "image/png",
1181
+ jpg: "image/jpeg",
1182
+ jpeg: "image/jpeg",
1183
+ gif: "image/gif",
1184
+ svg: "image/svg+xml",
1185
+ webp: "image/webp",
1186
+ bmp: "image/bmp"
1187
+ };
1188
+ function riebeckiteContentAssets(options) {
1189
+ return {
1190
+ name: "riebeckite-content-assets",
1191
+ apply: "serve",
1192
+ enforce: "pre",
1193
+ configureServer(server2) {
1194
+ const contentRoot = options.contentRoot();
1195
+ const entry = path6.join(options.appRoot(), "app", "server.ts");
1196
+ const base = server2.config.base;
1197
+ const publicDir = server2.config.publicDir;
1198
+ let cachedManifest;
1199
+ let cachedPaths = /* @__PURE__ */ new Set();
1200
+ const resolvePublicImagePaths = async () => {
1201
+ const module = await server2.ssrLoadModule(entry);
1202
+ const manifest = await module.content?.getManifest();
1203
+ if (!manifest) return /* @__PURE__ */ new Set();
1204
+ if (manifest !== cachedManifest) {
1205
+ cachedManifest = manifest;
1206
+ cachedPaths = collectPublicImagePaths(manifest);
1207
+ }
1208
+ return cachedPaths;
1209
+ };
1210
+ const middleware = async (request, response, next) => {
1211
+ try {
1212
+ if (request.method !== "GET" && request.method !== "HEAD") {
1213
+ next();
1214
+ return;
1215
+ }
1216
+ const logicalPath = toContentPath(request.url, base);
1217
+ if (!logicalPath || !isImagePath2(logicalPath)) {
1218
+ next();
1219
+ return;
1220
+ }
1221
+ if (isIgnoredContentPath2(logicalPath)) {
1222
+ next();
1223
+ return;
1224
+ }
1225
+ const contentType = imageContentType(logicalPath);
1226
+ if (!contentType) {
1227
+ next();
1228
+ return;
1229
+ }
1230
+ const available = await resolvePublicImagePaths();
1231
+ if (!available.has(logicalPath)) {
1232
+ next();
1233
+ return;
1234
+ }
1235
+ const file = path6.join(contentRoot, logicalPath);
1236
+ if (!isPathInsideDirectory(contentRoot, file)) {
1237
+ next();
1238
+ return;
1239
+ }
1240
+ if (await hasSiteOwnedFile(publicDir, logicalPath)) {
1241
+ next();
1242
+ return;
1243
+ }
1244
+ const content = await fs5.readFile(file);
1245
+ response.statusCode = 200;
1246
+ response.setHeader("Content-Type", contentType);
1247
+ response.setHeader("Content-Length", String(content.byteLength));
1248
+ response.setHeader("Cache-Control", "no-cache");
1249
+ if (request.method === "HEAD") {
1250
+ response.end();
1251
+ return;
1252
+ }
1253
+ response.end(content);
1254
+ return;
1255
+ } catch {
1256
+ next();
1257
+ }
1258
+ };
1259
+ server2.middlewares.use((request, response, next) => {
1260
+ void middleware(
1261
+ request,
1262
+ response,
1263
+ next
1264
+ );
1265
+ });
1266
+ }
1267
+ };
1268
+ }
1269
+ function collectPublicImagePaths(manifest) {
1270
+ const paths2 = /* @__PURE__ */ new Set();
1271
+ for (const entry of manifest.publicEntries ?? []) {
1272
+ for (const asset of entry.assets ?? []) {
1273
+ const normalized = normalizeContentPath2(asset.path);
1274
+ if (isImagePath2(normalized)) paths2.add(normalized);
1275
+ }
1276
+ }
1277
+ return paths2;
1278
+ }
1279
+ async function collectSiteOwnedOutputPaths(publicDir) {
1280
+ const paths2 = /* @__PURE__ */ new Set();
1281
+ if (!publicDir) return paths2;
1282
+ const root = path6.resolve(publicDir);
1283
+ const walk = async (directory) => {
1284
+ const entries = await fs5.readdir(directory, { withFileTypes: true }).catch(() => void 0);
1285
+ if (!entries) return;
1286
+ for (const entry of entries) {
1287
+ const full = path6.join(directory, entry.name);
1288
+ if (entry.isDirectory()) {
1289
+ await walk(full);
1290
+ } else {
1291
+ paths2.add(path6.relative(root, full).replaceAll("\\", "/"));
1292
+ }
1293
+ }
1294
+ };
1295
+ await walk(root);
1296
+ return paths2;
1297
+ }
1298
+ function toContentPath(url, base) {
1299
+ if (!url) return void 0;
1300
+ const withoutQuery = url.split("?")[0]?.split("#")[0];
1301
+ if (!withoutQuery) return void 0;
1302
+ let decoded = withoutQuery;
1303
+ try {
1304
+ decoded = decodeURIComponent(withoutQuery);
1305
+ } catch {
1306
+ return void 0;
1307
+ }
1308
+ let pathname = normalizeContentPath2(decoded);
1309
+ if (base && base !== "/" && pathname.startsWith(base)) {
1310
+ pathname = normalizeContentPath2(pathname.slice(base.length));
1311
+ }
1312
+ const logicalPath = pathname.replace(/^\/+/, "");
1313
+ if (!logicalPath) return void 0;
1314
+ if (logicalPath.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
1315
+ return void 0;
1316
+ }
1317
+ return logicalPath;
1318
+ }
1319
+ function imageContentType(logicalPath) {
1320
+ const extension = logicalPath.split("/").at(-1)?.split(".").at(-1);
1321
+ if (!extension) return void 0;
1322
+ return IMAGE_CONTENT_TYPES[extension.toLowerCase()];
1323
+ }
1324
+ async function hasSiteOwnedFile(publicDir, logicalPath) {
1325
+ if (!publicDir) return false;
1326
+ const file = path6.join(publicDir, logicalPath);
1327
+ try {
1328
+ return (await fs5.stat(file)).isFile();
1329
+ } catch {
1330
+ return false;
1331
+ }
1332
+ }
1333
+
1040
1334
  // src/ssg_plugin.ts
1041
- import { relative } from "node:path";
1335
+ import { createHash, randomUUID } from "node:crypto";
1336
+ import {
1337
+ mkdir,
1338
+ readdir,
1339
+ readFile,
1340
+ rename as rename2,
1341
+ rm,
1342
+ stat,
1343
+ writeFile
1344
+ } from "node:fs/promises";
1345
+ import { dirname, join, relative, resolve } from "node:path";
1346
+ import { performance } from "node:perf_hooks";
1042
1347
  import { defaultExtensionMap, toSSG } from "hono/ssg";
1043
- import { createServer } from "vite";
1348
+ import {
1349
+ createServer
1350
+ } from "vite";
1044
1351
  var defaultEntry = "./src/index.tsx";
1352
+ var cloudflareWorkerAssetLimit = 25 * 1024 * 1024;
1045
1353
  function riebeckiteSsg(options = {}) {
1046
1354
  const virtualId = "virtual:riebeckite-ssg-void-entry";
1047
1355
  const resolvedVirtualId = `\0${virtualId}`;
@@ -1049,7 +1357,7 @@ function riebeckiteSsg(options = {}) {
1049
1357
  let resolvedConfig;
1050
1358
  return {
1051
1359
  name: "riebeckite-ssg",
1052
- apply: "build",
1360
+ apply: shouldApplyRiebeckiteSsg,
1053
1361
  enforce: "post",
1054
1362
  config() {
1055
1363
  return {
@@ -1076,53 +1384,224 @@ function riebeckiteSsg(options = {}) {
1076
1384
  "Riebeckite SSG could not resolve the Vite configuration."
1077
1385
  );
1078
1386
  }
1387
+ const timingsMs = {};
1079
1388
  removeVirtualEntryChunk(bundle, resolvedVirtualId);
1080
- const server2 = await createServer({
1081
- root: config.root,
1082
- define: config.define,
1083
- resolve: {
1084
- ...config.resolve,
1085
- builtins: [...config.resolve.builtins, /^node:/]
1086
- },
1087
- plugins: [],
1088
- build: { ssr: true },
1089
- mode: config.mode
1090
- });
1389
+ const server2 = await measure(
1390
+ timingsMs,
1391
+ "ssgSetup",
1392
+ () => createServer({
1393
+ root: config.root,
1394
+ define: config.define,
1395
+ resolve: {
1396
+ ...config.resolve,
1397
+ builtins: [...config.resolve.builtins, /^node:/]
1398
+ },
1399
+ plugins: [],
1400
+ build: { ssr: true },
1401
+ mode: config.mode,
1402
+ server: { watch: null }
1403
+ })
1404
+ );
1091
1405
  try {
1092
- const module = await server2.ssrLoadModule(entry);
1406
+ const module = await measure(
1407
+ timingsMs,
1408
+ "ssrModuleLoad",
1409
+ () => server2.ssrLoadModule(entry)
1410
+ );
1093
1411
  const app = module.default;
1094
1412
  if (!app) {
1095
1413
  throw new Error(`Failed to find a default export from ${entry}.`);
1096
1414
  }
1415
+ const outputChangeSet = await measure(
1416
+ timingsMs,
1417
+ "outputDependencyResolution",
1418
+ async () => module.content?.getOutputChangeSet()
1419
+ );
1420
+ const appFingerprint = await measure(
1421
+ timingsMs,
1422
+ "appFingerprint",
1423
+ () => computeAppFingerprint(config.root, config.configFile)
1424
+ );
1425
+ const outputCachePath = join(
1426
+ config.root,
1427
+ ".riebeckite",
1428
+ "ssg-output-cache.json"
1429
+ );
1430
+ const previousOutputCache = await loadOutputCache(
1431
+ outputCachePath,
1432
+ timingsMs,
1433
+ (message) => this.warn(message)
1434
+ );
1435
+ const siteOwnedOutputPaths = await measure(
1436
+ timingsMs,
1437
+ "siteOwnedOutputs",
1438
+ () => collectSiteOwnedOutputPaths(config.publicDir)
1439
+ );
1440
+ const siteOutputCollisions = findBuildOutputSiteCollisions(
1441
+ Object.keys(bundle),
1442
+ siteOwnedOutputPaths
1443
+ );
1444
+ if (siteOutputCollisions.length > 0) {
1445
+ this.warn(
1446
+ `Build outputs take precedence over site public assets: ${siteOutputCollisions.join(", ")}`
1447
+ );
1448
+ }
1449
+ const canUseIncremental = outputChangeSet && !outputChangeSet.fullRegenerationRequired && previousOutputCache && previousOutputCache.appFingerprint === appFingerprint && outputChangeSet.unchanged.every(
1450
+ (output) => siteOwnedOutputPaths.has(output.path) || previousOutputCache.outputs[output.path]
1451
+ );
1452
+ const reusablePaths = new Set(
1453
+ canUseIncremental ? outputChangeSet.unchanged.map((output) => output.path) : []
1454
+ );
1455
+ const shadowedOutputs = /* @__PURE__ */ new Set();
1456
+ const skippedRouteCount = canUseIncremental ? outputChangeSet.unchanged.filter(
1457
+ (output) => output.kind !== "generated"
1458
+ ).length : 0;
1097
1459
  const generatedHtml = [];
1098
- const result = await toSSG(
1099
- app,
1100
- {
1101
- writeFile: async (filePath, data) => {
1102
- const fileName = relative(
1103
- config.build.outDir,
1104
- filePath
1105
- ).replaceAll("\\", "/");
1106
- if (fileName.endsWith(".html") && typeof data === "string") {
1107
- generatedHtml.push({ path: fileName, html: data });
1460
+ const nextOutputCache = {
1461
+ version: 1,
1462
+ appFingerprint,
1463
+ outputs: {}
1464
+ };
1465
+ const metrics = {
1466
+ candidateOutputCount: outputChangeSet?.candidateOutputCount ?? 0,
1467
+ affectedOutputCount: outputChangeSet?.affectedOutputCount ?? 0,
1468
+ removedOutputCount: outputChangeSet?.removedOutputCount ?? 0,
1469
+ unchangedOutputCount: outputChangeSet?.unchangedOutputCount ?? 0,
1470
+ renderedOutputCount: 0,
1471
+ emittedOutputCount: 0,
1472
+ reusedOutputCount: 0,
1473
+ deletedOutputCount: 0,
1474
+ shadowedOutputCount: 0,
1475
+ fullRegenerationRequired: !canUseIncremental,
1476
+ htmlOutputCount: 0,
1477
+ assetOutputCount: 0,
1478
+ cacheFileSizeBytes: previousOutputCache?.fileSizeBytes ?? null,
1479
+ cacheEntriesRead: previousOutputCache ? Object.keys(previousOutputCache.outputs).length : 0,
1480
+ cacheEntriesSerialized: 0,
1481
+ enumeratedRouteCount: 0,
1482
+ renderedRouteCount: 0,
1483
+ skippedRouteCount,
1484
+ timingsMs
1485
+ };
1486
+ process.env.RIEBECKITE_SSG_FULL_REGENERATION = canUseIncremental ? "" : "1";
1487
+ const outDir = resolve(config.root, config.build.outDir);
1488
+ const result = await measure(
1489
+ timingsMs,
1490
+ "toSSG",
1491
+ () => toSSG(
1492
+ app,
1493
+ {
1494
+ writeFile: async (filePath, data) => {
1495
+ const fileName = relative(outDir, filePath).replaceAll(
1496
+ "\\",
1497
+ "/"
1498
+ );
1499
+ metrics.enumeratedRouteCount += 1;
1500
+ if (siteOwnedOutputPaths.has(fileName)) {
1501
+ shadowedOutputs.add(fileName);
1502
+ return;
1503
+ }
1504
+ if (canUseIncremental && reusablePaths.has(fileName)) {
1505
+ return;
1506
+ }
1507
+ if (fileName.endsWith(".html") && typeof data === "string") {
1508
+ metrics.renderedOutputCount += 1;
1509
+ metrics.htmlOutputCount += 1;
1510
+ generatedHtml.push({ path: fileName, html: data });
1511
+ } else {
1512
+ metrics.assetOutputCount += 1;
1513
+ }
1514
+ this.emitFile({
1515
+ type: "asset",
1516
+ fileName,
1517
+ source: data
1518
+ });
1519
+ metrics.emittedOutputCount += 1;
1520
+ nextOutputCache.outputs[fileName] = encodeOutput(data);
1521
+ },
1522
+ async mkdir() {
1523
+ }
1524
+ },
1525
+ {
1526
+ dir: outDir,
1527
+ plugins: [...options.plugins ?? []],
1528
+ extensionMap: options.extensionMap ?? defaultExtensionMap
1529
+ }
1530
+ )
1531
+ );
1532
+ delete process.env.RIEBECKITE_SSG_FULL_REGENERATION;
1533
+ metrics.renderedRouteCount = result.files.length;
1534
+ if (!result.success) throw result.error;
1535
+ const emittedGeneratedOutputCount = await measure(
1536
+ timingsMs,
1537
+ "generatedOutputs",
1538
+ () => emitGeneratedOutputs(
1539
+ module,
1540
+ (asset) => {
1541
+ this.emitFile(asset);
1542
+ metrics.emittedOutputCount += 1;
1543
+ nextOutputCache.outputs[asset.fileName] = encodeOutput(
1544
+ asset.source
1545
+ );
1546
+ },
1547
+ canUseIncremental ? new Set(
1548
+ outputChangeSet.affected.filter((output) => output.kind === "generated").map((output) => output.path)
1549
+ ) : void 0,
1550
+ siteOwnedOutputPaths,
1551
+ (path10) => shadowedOutputs.add(path10)
1552
+ )
1553
+ );
1554
+ if (!outputChangeSet)
1555
+ metrics.candidateOutputCount = result.files.length + emittedGeneratedOutputCount;
1556
+ if (canUseIncremental) {
1557
+ await measure(timingsMs, "unchangedOutputReuse", async () => {
1558
+ for (const output of outputChangeSet.unchanged) {
1559
+ const path10 = output.path;
1560
+ if (siteOwnedOutputPaths.has(path10)) {
1561
+ shadowedOutputs.add(path10);
1562
+ continue;
1108
1563
  }
1564
+ const cached = previousOutputCache.outputs[path10];
1565
+ if (!cached) continue;
1109
1566
  this.emitFile({
1110
1567
  type: "asset",
1111
- fileName,
1112
- source: data
1568
+ fileName: path10,
1569
+ source: decodeOutput(cached)
1113
1570
  });
1114
- },
1115
- async mkdir() {
1571
+ metrics.emittedOutputCount += 1;
1572
+ metrics.reusedOutputCount += 1;
1573
+ nextOutputCache.outputs[path10] = cached;
1116
1574
  }
1117
- },
1118
- {
1119
- dir: config.build.outDir,
1120
- plugins: options.plugins ?? [],
1121
- extensionMap: options.extensionMap ?? defaultExtensionMap
1575
+ });
1576
+ }
1577
+ if (outputChangeSet) {
1578
+ for (const output of outputChangeSet.removed) {
1579
+ if (siteOwnedOutputPaths.has(output.path)) continue;
1580
+ await rm(join(outDir, output.path), {
1581
+ force: true
1582
+ });
1583
+ metrics.deletedOutputCount += 1;
1122
1584
  }
1123
- );
1124
- if (!result.success) throw result.error;
1125
- await emitGeneratedOutputs(module, (asset) => this.emitFile(asset));
1585
+ }
1586
+ metrics.shadowedOutputCount = shadowedOutputs.size;
1587
+ if (shadowedOutputs.size > 0) {
1588
+ this.warn(
1589
+ `Site public assets take precedence over generated outputs: ${[
1590
+ ...shadowedOutputs
1591
+ ].sort().join(", ")}`
1592
+ );
1593
+ }
1594
+ metrics.cacheEntriesSerialized = Object.keys(
1595
+ nextOutputCache.outputs
1596
+ ).length;
1597
+ try {
1598
+ await saveOutputCache(outputCachePath, nextOutputCache, timingsMs);
1599
+ } catch (error) {
1600
+ this.warn(
1601
+ `SSG output cache could not be saved; the next build regenerates every output. ${error instanceof Error ? error.message : String(error)}`
1602
+ );
1603
+ }
1604
+ await writeSsgOutputMetrics(metrics);
1126
1605
  inspectGeneratedHtmlPages(module, generatedHtml, {
1127
1606
  warn: (message) => this.warn(message),
1128
1607
  info: (message) => this.info(message)
@@ -1130,19 +1609,247 @@ function riebeckiteSsg(options = {}) {
1130
1609
  } finally {
1131
1610
  await server2.close();
1132
1611
  }
1612
+ },
1613
+ async closeBundle() {
1614
+ if (!resolvedConfig) return;
1615
+ const outDir = resolve(resolvedConfig.root, resolvedConfig.build.outDir);
1616
+ const oversizedAssets = await findOversizedAssets(outDir);
1617
+ for (const asset of oversizedAssets) {
1618
+ this.warn(
1619
+ `Generated asset ${asset.path} is ${formatBytes(asset.size)}, exceeding Cloudflare Workers' ${formatBytes(cloudflareWorkerAssetLimit)} asset limit.`
1620
+ );
1621
+ }
1133
1622
  }
1134
1623
  };
1135
1624
  }
1136
- async function emitGeneratedOutputs(module, emit) {
1625
+ async function findOversizedAssets(directory, relativeDirectory = "") {
1626
+ const entries = await readdir(directory, { withFileTypes: true }).catch(
1627
+ (error) => {
1628
+ if (isMissingDirectoryError(error)) return [];
1629
+ throw error;
1630
+ }
1631
+ );
1632
+ const nestedAssets = await Promise.all(
1633
+ entries.map(async (entry) => {
1634
+ const path10 = join(directory, entry.name);
1635
+ const relativePath = join(relativeDirectory, entry.name).replaceAll(
1636
+ "\\",
1637
+ "/"
1638
+ );
1639
+ if (entry.isDirectory()) {
1640
+ return findOversizedAssets(path10, relativePath);
1641
+ }
1642
+ if (!entry.isFile()) return [];
1643
+ const size = (await stat(path10)).size;
1644
+ return size > cloudflareWorkerAssetLimit ? [{ path: relativePath, size }] : [];
1645
+ })
1646
+ );
1647
+ return nestedAssets.flat();
1648
+ }
1649
+ function isMissingDirectoryError(error) {
1650
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
1651
+ }
1652
+ function formatBytes(bytes) {
1653
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
1654
+ }
1655
+ function shouldApplyRiebeckiteSsg(_config, env) {
1656
+ return env.command === "build" && env.mode !== "client";
1657
+ }
1658
+ function findBuildOutputSiteCollisions(buildOutputPaths, siteOwnedOutputPaths) {
1659
+ const collisions = /* @__PURE__ */ new Set();
1660
+ for (const path10 of buildOutputPaths) {
1661
+ if (siteOwnedOutputPaths.has(path10)) collisions.add(path10);
1662
+ }
1663
+ return [...collisions].sort();
1664
+ }
1665
+ async function emitGeneratedOutputs(module, emit, affectedPaths, shadowedPaths, onShadowed) {
1137
1666
  const content = module.content;
1138
- if (!content) return;
1667
+ if (!content) return 0;
1139
1668
  const manifest = await content.getManifest();
1669
+ let count = 0;
1140
1670
  for (const output of manifest.generatedOutputs ?? []) {
1671
+ if (affectedPaths && !affectedPaths.has(output.path)) continue;
1672
+ if (shadowedPaths?.has(output.path)) {
1673
+ onShadowed?.(output.path);
1674
+ continue;
1675
+ }
1141
1676
  emit({
1142
1677
  type: "asset",
1143
1678
  fileName: output.path,
1144
1679
  source: output.content
1145
1680
  });
1681
+ count += 1;
1682
+ }
1683
+ return count;
1684
+ }
1685
+ var APP_FINGERPRINT_IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
1686
+ "node_modules",
1687
+ ".riebeckite",
1688
+ "dist"
1689
+ ]);
1690
+ var APP_FINGERPRINT_CONFIG_FILES = [
1691
+ "riebeckite.config.ts",
1692
+ "riebeckite.config.mts",
1693
+ "riebeckite.config.cts",
1694
+ "riebeckite.config.js",
1695
+ "riebeckite.config.mjs",
1696
+ "riebeckite.config.cjs"
1697
+ ];
1698
+ async function computeAppFingerprint(root, configFile) {
1699
+ const files = [];
1700
+ await collectAppFiles(join(root, "app"), files);
1701
+ if (configFile) files.push(configFile);
1702
+ for (const name of APP_FINGERPRINT_CONFIG_FILES) {
1703
+ files.push(join(root, name));
1704
+ }
1705
+ files.sort();
1706
+ const hash = createHash("sha256");
1707
+ for (const file of files) {
1708
+ let content;
1709
+ try {
1710
+ content = await readFile(file);
1711
+ } catch {
1712
+ continue;
1713
+ }
1714
+ hash.update(relative(root, file).replaceAll("\\", "/"));
1715
+ hash.update("\0");
1716
+ hash.update(content);
1717
+ hash.update("\0");
1718
+ }
1719
+ return hash.digest("hex");
1720
+ }
1721
+ async function collectAppFiles(directory, files) {
1722
+ const entries = await readdir(directory, { withFileTypes: true }).catch(
1723
+ () => void 0
1724
+ );
1725
+ if (!entries) return;
1726
+ for (const entry of entries) {
1727
+ const full = join(directory, entry.name);
1728
+ if (entry.isDirectory()) {
1729
+ if (APP_FINGERPRINT_IGNORED_DIRECTORIES.has(entry.name)) continue;
1730
+ await collectAppFiles(full, files);
1731
+ } else if (entry.isFile()) {
1732
+ files.push(full);
1733
+ }
1734
+ }
1735
+ }
1736
+ async function loadOutputCache(path10, timingsMs, log = () => {
1737
+ }) {
1738
+ let raw;
1739
+ try {
1740
+ raw = await measure(
1741
+ timingsMs,
1742
+ "outputCacheRead",
1743
+ () => readFile(path10, "utf8")
1744
+ );
1745
+ } catch (error) {
1746
+ if (isNotFoundError2(error)) return void 0;
1747
+ log(
1748
+ `SSG output cache could not be read; the build regenerates every output. ${error instanceof Error ? error.message : String(error)}`
1749
+ );
1750
+ return void 0;
1751
+ }
1752
+ let parsed;
1753
+ try {
1754
+ parsed = measureSync(timingsMs, "outputCacheParse", () => JSON.parse(raw));
1755
+ } catch (error) {
1756
+ log(
1757
+ `SSG output cache is corrupted and will be rebuilt. ${error instanceof Error ? error.message : String(error)}`
1758
+ );
1759
+ return void 0;
1760
+ }
1761
+ if (!isOutputCacheState(parsed)) {
1762
+ log("SSG output cache uses an unsupported format and will be rebuilt.");
1763
+ return void 0;
1764
+ }
1765
+ return timingsMs ? { ...parsed, fileSizeBytes: Buffer.byteLength(raw) } : parsed;
1766
+ }
1767
+ function isOutputCacheState(value) {
1768
+ if (typeof value !== "object" || value === null) return false;
1769
+ const candidate = value;
1770
+ return candidate.version === 1 && (candidate.appFingerprint === void 0 || typeof candidate.appFingerprint === "string") && isOutputCacheEntries(candidate.outputs);
1771
+ }
1772
+ function isOutputCacheEntries(value) {
1773
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1774
+ return false;
1775
+ }
1776
+ return Object.values(value).every(
1777
+ (entry) => typeof entry === "object" && entry !== null && typeof entry.source === "string" && (entry.encoding === "utf8" || entry.encoding === "base64")
1778
+ );
1779
+ }
1780
+ async function saveOutputCache(path10, state, timingsMs) {
1781
+ const directory = dirname(path10);
1782
+ await mkdir(directory, { recursive: true });
1783
+ const temporaryPath = `${path10}.${randomUUID()}.tmp`;
1784
+ const serialized = measureSync(
1785
+ timingsMs,
1786
+ "outputCacheSerialize",
1787
+ () => `${JSON.stringify(state)}
1788
+ `
1789
+ );
1790
+ try {
1791
+ await measure(
1792
+ timingsMs,
1793
+ "outputCacheWrite",
1794
+ () => writeFile(temporaryPath, serialized, "utf8")
1795
+ );
1796
+ await measure(
1797
+ timingsMs,
1798
+ "outputCacheRename",
1799
+ () => replaceFile(temporaryPath, path10)
1800
+ );
1801
+ } catch (error) {
1802
+ await rm(temporaryPath, { force: true }).catch(() => void 0);
1803
+ throw error;
1804
+ }
1805
+ }
1806
+ async function replaceFile(temporaryPath, targetPath) {
1807
+ try {
1808
+ await rename2(temporaryPath, targetPath);
1809
+ } catch (error) {
1810
+ if (!isReplacementFailure(error)) throw error;
1811
+ await rm(targetPath, { force: true });
1812
+ await rename2(temporaryPath, targetPath);
1813
+ }
1814
+ }
1815
+ function isReplacementFailure(error) {
1816
+ if (typeof error !== "object" || error === null || !("code" in error)) {
1817
+ return false;
1818
+ }
1819
+ const code = String(error.code);
1820
+ return code === "EPERM" || code === "EEXIST" || code === "EACCES";
1821
+ }
1822
+ function isNotFoundError2(error) {
1823
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
1824
+ }
1825
+ function encodeOutput(source) {
1826
+ return typeof source === "string" ? { source, encoding: "utf8" } : { source: Buffer.from(source).toString("base64"), encoding: "base64" };
1827
+ }
1828
+ function decodeOutput(entry) {
1829
+ return entry.encoding === "utf8" ? entry.source : Buffer.from(entry.source, "base64");
1830
+ }
1831
+ async function writeSsgOutputMetrics(metrics) {
1832
+ const file = process.env.RIEBECKITE_SSG_METRICS_FILE;
1833
+ if (!file) return;
1834
+ await writeFile(file, `${JSON.stringify(metrics)}
1835
+ `, "utf8");
1836
+ }
1837
+ async function measure(timingsMs, name, fn) {
1838
+ const start = performance.now();
1839
+ try {
1840
+ return await fn();
1841
+ } finally {
1842
+ if (timingsMs)
1843
+ timingsMs[name] = (timingsMs[name] ?? 0) + performance.now() - start;
1844
+ }
1845
+ }
1846
+ function measureSync(timingsMs, name, fn) {
1847
+ const start = performance.now();
1848
+ try {
1849
+ return fn();
1850
+ } finally {
1851
+ if (timingsMs)
1852
+ timingsMs[name] = (timingsMs[name] ?? 0) + performance.now() - start;
1146
1853
  }
1147
1854
  }
1148
1855
  function inspectGeneratedHtmlPages(module, pages, log) {
@@ -1192,8 +1899,8 @@ function createRiebeckiteSsg(options = {}) {
1192
1899
  }
1193
1900
 
1194
1901
  // src/vite_runner.ts
1195
- import fs5 from "node:fs/promises";
1196
- import path5 from "node:path";
1902
+ import fs6 from "node:fs/promises";
1903
+ import path7 from "node:path";
1197
1904
  import { build, createServer as createServer2 } from "vite";
1198
1905
  var viteConfigFileNames = [
1199
1906
  "vite.config.ts",
@@ -1219,8 +1926,8 @@ var HonoxApplicationRootError = class extends Error {
1219
1926
  }
1220
1927
  };
1221
1928
  async function resolveHonoxApplication(options) {
1222
- const configRoot = path5.resolve(options.configRoot);
1223
- const appRoot = options.appRoot ? path5.resolve(options.appRoot) : await resolveHonoxApplicationRoot(configRoot);
1929
+ const configRoot = path7.resolve(options.configRoot);
1930
+ const appRoot = options.appRoot ? path7.resolve(options.appRoot) : await resolveHonoxApplicationRoot(configRoot);
1224
1931
  const workspaceRoot = options.workspaceRoot ?? await findWorkspaceRoot(configRoot);
1225
1932
  const rawConfig = await loadRiebeckiteConfig({
1226
1933
  configRoot,
@@ -1237,8 +1944,8 @@ async function resolveHonoxApplication(options) {
1237
1944
  }
1238
1945
  async function findWorkspaceRoot(configRoot) {
1239
1946
  try {
1240
- const workspaceConfig = path5.join(configRoot, "pnpm-workspace.yaml");
1241
- return (await fs5.stat(workspaceConfig)).isFile() ? configRoot : void 0;
1947
+ const workspaceConfig = path7.join(configRoot, "pnpm-workspace.yaml");
1948
+ return (await fs6.stat(workspaceConfig)).isFile() ? configRoot : void 0;
1242
1949
  } catch {
1243
1950
  return void 0;
1244
1951
  }
@@ -1265,9 +1972,9 @@ async function runTraced(tracer, name, buildTask) {
1265
1972
  return tracer ? await tracer.span(name, {}, buildTask) : await buildTask();
1266
1973
  }
1267
1974
  async function waitForShutdown(server2) {
1268
- await new Promise((resolve) => {
1975
+ await new Promise((resolve2) => {
1269
1976
  const close = () => {
1270
- void server2.close().finally(resolve);
1977
+ void server2.close().finally(resolve2);
1271
1978
  };
1272
1979
  process.once("SIGINT", close);
1273
1980
  process.once("SIGTERM", close);
@@ -1280,12 +1987,12 @@ async function findViteApplicationRoots(root) {
1280
1987
  const directory = directories.pop();
1281
1988
  if (!directory) continue;
1282
1989
  if (await hasViteConfig(directory)) applicationRoots.push(directory);
1283
- const entries = await fs5.readdir(directory, { withFileTypes: true });
1990
+ const entries = await fs6.readdir(directory, { withFileTypes: true });
1284
1991
  for (const entry of entries) {
1285
1992
  if (!entry.isDirectory() || entry.name === "node_modules" || entry.name === ".git" || entry.name === "tests") {
1286
1993
  continue;
1287
1994
  }
1288
- directories.push(path5.join(directory, entry.name));
1995
+ directories.push(path7.join(directory, entry.name));
1289
1996
  }
1290
1997
  }
1291
1998
  return applicationRoots;
@@ -1293,7 +2000,7 @@ async function findViteApplicationRoots(root) {
1293
2000
  async function hasViteConfig(directory) {
1294
2001
  for (const fileName of viteConfigFileNames) {
1295
2002
  try {
1296
- if ((await fs5.stat(path5.join(directory, fileName))).isFile()) return true;
2003
+ if ((await fs6.stat(path7.join(directory, fileName))).isFile()) return true;
1297
2004
  } catch {
1298
2005
  }
1299
2006
  }
@@ -1313,6 +2020,7 @@ var defaultSsrExternals = [
1313
2020
  ];
1314
2021
  function riebeckite(options = {}) {
1315
2022
  let resolvedConfig;
2023
+ let contentWatchRoots;
1316
2024
  const getConfig = () => {
1317
2025
  if (!resolvedConfig) {
1318
2026
  throw new Error("Riebeckite config has not been loaded yet.");
@@ -1323,7 +2031,7 @@ function riebeckite(options = {}) {
1323
2031
  {
1324
2032
  name: "riebeckite-host-integration",
1325
2033
  async config(userConfig) {
1326
- const root = userConfig.root ? path6.resolve(userConfig.root) : process.cwd();
2034
+ const root = userConfig.root ? path8.resolve(userConfig.root) : process.cwd();
1327
2035
  const appRoot = options.appRoot ?? root;
1328
2036
  const application = await resolveHonoxApplication({
1329
2037
  configRoot: options.configRoot ?? appRoot,
@@ -1332,9 +2040,10 @@ function riebeckite(options = {}) {
1332
2040
  workspaceRoot: options.workspaceRoot
1333
2041
  });
1334
2042
  resolvedConfig = application.config;
2043
+ contentWatchRoots = application;
1335
2044
  writeRiebeckiteAssetEntries(resolvedConfig, {
1336
- pluginStyles: path6.join(appRoot, "app/.riebeckite/plugin-styles.css"),
1337
- themeStyles: path6.join(appRoot, "app/.riebeckite/theme-styles.css")
2045
+ pluginStyles: path8.join(appRoot, "app/.riebeckite/plugin-styles.css"),
2046
+ themeStyles: path8.join(appRoot, "app/.riebeckite/theme-styles.css")
1338
2047
  });
1339
2048
  return {
1340
2049
  define: {
@@ -1351,9 +2060,24 @@ function riebeckite(options = {}) {
1351
2060
  };
1352
2061
  }
1353
2062
  },
1354
- riebeckiteClientModule(getConfig)
2063
+ riebeckiteClientModule(getConfig),
2064
+ riebeckiteContentAssets({
2065
+ appRoot: () => requireContentWatchRoots(contentWatchRoots).appRoot,
2066
+ contentRoot: () => requireContentWatchRoots(contentWatchRoots).contentRoot
2067
+ }),
2068
+ riebeckiteContentWatch({
2069
+ appRoot: () => requireContentWatchRoots(contentWatchRoots).appRoot,
2070
+ contentRoot: () => requireContentWatchRoots(contentWatchRoots).contentRoot,
2071
+ exclude: () => getConfig().content.exclude ?? []
2072
+ })
1355
2073
  ];
1356
2074
  }
2075
+ function requireContentWatchRoots(roots) {
2076
+ if (!roots) {
2077
+ throw new Error("Riebeckite application has not been resolved yet.");
2078
+ }
2079
+ return roots;
2080
+ }
1357
2081
  function riebeckiteVite(options = {}) {
1358
2082
  const { ssg, ssrExternals, ...integration } = options;
1359
2083
  return [
@@ -1380,6 +2104,17 @@ function riebeckiteSsrExternals(externals) {
1380
2104
  }
1381
2105
 
1382
2106
  // src/scaffold/app-templates.ts
2107
+ function hasScaffoldPlugin(preset, packageName) {
2108
+ return preset.plugins.some((plugin) => plugin.package === packageName);
2109
+ }
2110
+ function articleTitleHelper() {
2111
+ return `
2112
+ function getArticleTitle(slug: string, title: unknown): string {
2113
+ if (typeof title === "string" && title.trim().length > 0) return title;
2114
+ return slug.split("/").at(-1) ?? slug;
2115
+ }
2116
+ `;
2117
+ }
1383
2118
  function server() {
1384
2119
  return `import { mountRiebeckiteEndpoints } from "@riebeckite/honox/server";
1385
2120
  import { createApp } from "honox/server";
@@ -1532,6 +2267,56 @@ function style(preset) {
1532
2267
  " padding: 2rem 1rem;",
1533
2268
  " margin: 0 auto;",
1534
2269
  "}",
2270
+ "",
2271
+ ".article-shell__layout {",
2272
+ " display: grid;",
2273
+ " grid-template-columns: minmax(0, 1fr);",
2274
+ " justify-content: center;",
2275
+ " gap: var(--rb-layout-gap, 2rem);",
2276
+ "}",
2277
+ "",
2278
+ ".site-article__aside {",
2279
+ " display: none;",
2280
+ "}",
2281
+ "",
2282
+ ".site-article__footer {",
2283
+ " min-width: 0;",
2284
+ "}",
2285
+ "",
2286
+ "@media (min-width: 88rem) {",
2287
+ ' :is(:root, .rb-theme-root)[data-article-layout="article"] .article-shell__layout {',
2288
+ " grid-template-columns: minmax(0, var(--rb-layout-article-max, 48rem));",
2289
+ " position: relative;",
2290
+ " }",
2291
+ "",
2292
+ ' :is(:root, .rb-theme-root)[data-article-layout="article"] .article-shell__body {',
2293
+ " grid-column: 1;",
2294
+ " grid-row: 1;",
2295
+ " }",
2296
+ "",
2297
+ ' :is(:root, .rb-theme-root)[data-article-layout="article"] .site-article__footer {',
2298
+ " grid-column: 1;",
2299
+ " }",
2300
+ "",
2301
+ ' :is(:root, .rb-theme-root)[data-article-layout="article"] .site-article__aside {',
2302
+ " display: block;",
2303
+ " grid-column: 1;",
2304
+ " grid-row: 1;",
2305
+ " position: sticky;",
2306
+ " top: 4rem;",
2307
+ " align-self: start;",
2308
+ " width: var(--rb-layout-sidebar, 14rem);",
2309
+ " max-height: calc(100dvh - 8rem);",
2310
+ " margin-inline-start: calc(",
2311
+ " -1 * var(--rb-layout-sidebar, 14rem) -",
2312
+ " var(--rb-layout-gap, 2rem)",
2313
+ " );",
2314
+ " overflow: auto;",
2315
+ " padding-inline-end: 1rem;",
2316
+ " border-inline-end: 1px solid",
2317
+ " color-mix(in oklab, currentColor 18%, transparent);",
2318
+ " }",
2319
+ "}",
1535
2320
  ""
1536
2321
  );
1537
2322
  }
@@ -1576,23 +2361,81 @@ import { Article, ArticleContent, ArticleLayout } from "@riebeckite/honox/ui";
1576
2361
 
1577
2362
  export function SiteArticle({
1578
2363
  post,
1579
- afterMeta,
2364
+ propertiesHtml,
2365
+ afterHeaderHtml,
2366
+ afterMetaHtml,
2367
+ beforeContentHtml,
2368
+ afterContentHtml,
2369
+ asideHtml,
2370
+ footerHtml,
2371
+ asideContent,
2372
+ afterContent,
2373
+ footerContent,
1580
2374
  }: {
1581
2375
  post: PostContent;
1582
- afterMeta?: string;
2376
+ propertiesHtml?: string;
2377
+ afterHeaderHtml?: string;
2378
+ afterMetaHtml?: string;
2379
+ beforeContentHtml?: string;
2380
+ afterContentHtml?: string;
2381
+ asideHtml?: string;
2382
+ footerHtml?: string;
2383
+ asideContent?: unknown;
2384
+ afterContent?: unknown;
2385
+ footerContent?: unknown;
1583
2386
  }) {
1584
2387
  return (
1585
2388
  <Article class="site-article">
1586
2389
  <ArticleLayout>
2390
+ {asideContent}
2391
+ {asideHtml ? (
2392
+ <div
2393
+ class="site-article__aside"
2394
+ dangerouslySetInnerHTML={{ __html: asideHtml }}
2395
+ />
2396
+ ) : null}
1587
2397
  <ArticleContent>
1588
- {afterMeta ? (
2398
+ {afterHeaderHtml ? (
2399
+ <div
2400
+ class="site-article__after-header"
2401
+ dangerouslySetInnerHTML={{ __html: afterHeaderHtml }}
2402
+ />
2403
+ ) : null}
2404
+ {propertiesHtml ? (
2405
+ <div
2406
+ class="article-properties"
2407
+ dangerouslySetInnerHTML={{ __html: propertiesHtml }}
2408
+ />
2409
+ ) : null}
2410
+ {afterMetaHtml ? (
1589
2411
  <div
1590
2412
  class="site-article__after-meta"
1591
- dangerouslySetInnerHTML={{ __html: afterMeta }}
2413
+ dangerouslySetInnerHTML={{ __html: afterMetaHtml }}
2414
+ />
2415
+ ) : null}
2416
+ {beforeContentHtml ? (
2417
+ <div
2418
+ class="site-article__before-content"
2419
+ dangerouslySetInnerHTML={{ __html: beforeContentHtml }}
1592
2420
  />
1593
2421
  ) : null}
1594
2422
  <div dangerouslySetInnerHTML={{ __html: post.html ?? "" }} />
2423
+ {afterContent}
2424
+ {afterContentHtml ? (
2425
+ <div
2426
+ class="site-article__after-content"
2427
+ dangerouslySetInnerHTML={{ __html: afterContentHtml }}
2428
+ />
2429
+ ) : null}
1595
2430
  </ArticleContent>
2431
+ {footerHtml || footerContent ? (
2432
+ <div class="site-article__footer">
2433
+ {footerHtml ? (
2434
+ <div dangerouslySetInnerHTML={{ __html: footerHtml }} />
2435
+ ) : null}
2436
+ {footerContent}
2437
+ </div>
2438
+ ) : null}
1596
2439
  </ArticleLayout>
1597
2440
  </Article>
1598
2441
  );
@@ -1605,10 +2448,12 @@ function renderer(preset) {
1605
2448
  const hasColorMode = preset.plugins.some(
1606
2449
  (plugin) => plugin.package === "@riebeckite/plugin-color-mode"
1607
2450
  );
2451
+ const hasSearch = hasScaffoldPlugin(preset, "@riebeckite/plugin-search");
1608
2452
  return `import type { PluginHeadTag } from "@riebeckite/core";
1609
2453
  import { jsxRenderer } from "hono/jsx-renderer";
1610
2454
  import { Link, Script } from "honox/server";
1611
2455
  ${hasColorMode ? `import { ColorModeScript } from "@riebeckite/plugin-color-mode";
2456
+ ` : ""}${hasSearch ? `import { SearchBar } from "@riebeckite/plugin-search";
1612
2457
  ` : ""}${hasHeader ? `import { SiteHeader } from "../components/site-header";
1613
2458
  ` : ""}import { config } from "../config";
1614
2459
 
@@ -1648,6 +2493,7 @@ ${hasColorMode ? ` <ColorModeScript />
1648
2493
  </head>
1649
2494
  <body class="riebeckite-page">
1650
2495
  ${hasHeader ? ` <SiteHeader />
2496
+ ` : ""}${hasSearch ? ` <SearchBar />
1651
2497
  ` : ""} {children}
1652
2498
  </body>
1653
2499
  </html>
@@ -1696,11 +2542,96 @@ export default jsxRenderer(({ children }, c) => (
1696
2542
  }
1697
2543
  function indexRoute(preset) {
1698
2544
  if (!preset.appFiles.includes("article")) return staticIndexRoute();
1699
- return `import { isPublished } from "@riebeckite/core";
1700
- import { createRoute } from "honox/factory";
1701
- import { SiteArticle } from "../components/article";
1702
- import { config } from "../config";
1703
- import { content } from "../content";
2545
+ const hasToc = hasScaffoldPlugin(preset, "@riebeckite/plugin-toc");
2546
+ const hasBacklinks = hasScaffoldPlugin(
2547
+ preset,
2548
+ "@riebeckite/plugin-backlinks"
2549
+ );
2550
+ const hasRecentPosts = hasScaffoldPlugin(
2551
+ preset,
2552
+ "@riebeckite/plugin-recent-posts"
2553
+ );
2554
+ const hasDailyNotes = hasScaffoldPlugin(
2555
+ preset,
2556
+ "@riebeckite/plugin-daily-notes"
2557
+ );
2558
+ const needsTitle = hasBacklinks || hasRecentPosts;
2559
+ const needsConfig = hasBacklinks || hasRecentPosts || hasDailyNotes;
2560
+ const importLines = [
2561
+ 'import { createRoute } from "honox/factory";'
2562
+ ];
2563
+ if (hasBacklinks) {
2564
+ importLines.push(
2565
+ 'import { Backlinks, getPublishedBacklinks } from "@riebeckite/plugin-backlinks";'
2566
+ );
2567
+ }
2568
+ if (hasDailyNotes) {
2569
+ importLines.push(
2570
+ 'import { DailyNotes, getDailyNotes } from "@riebeckite/plugin-daily-notes";'
2571
+ );
2572
+ }
2573
+ if (hasRecentPosts) {
2574
+ importLines.push(
2575
+ 'import { RecentPosts, getRecentPosts } from "@riebeckite/plugin-recent-posts";'
2576
+ );
2577
+ }
2578
+ if (hasToc) {
2579
+ importLines.push(
2580
+ 'import { extractTableOfContents, TableOfContents } from "@riebeckite/plugin-toc";'
2581
+ );
2582
+ }
2583
+ importLines.push('import { SiteArticle } from "../components/article";');
2584
+ if (needsConfig) importLines.push('import { config } from "../config";');
2585
+ importLines.push('import { content } from "../content";');
2586
+ const dataLines = [];
2587
+ if (hasBacklinks) {
2588
+ dataLines.push(
2589
+ ' const backlinks = getPublishedBacklinks({ manifest, config, slug: "index", resolveTitle: getArticleTitle });'
2590
+ );
2591
+ }
2592
+ if (hasRecentPosts) {
2593
+ dataLines.push(
2594
+ " const recentPosts = await getRecentPosts({ posts: manifest.discoverableEntries, config, getProcessedContent: (slug) => content.getProcessedContent(slug), resolveTitle: getArticleTitle });"
2595
+ );
2596
+ }
2597
+ if (hasDailyNotes) {
2598
+ dataLines.push(" const dailyNotes = getDailyNotes({ manifest, config });");
2599
+ }
2600
+ if (hasToc) {
2601
+ dataLines.push(
2602
+ ' const tableOfContents = extractTableOfContents(post.html ?? "");'
2603
+ );
2604
+ }
2605
+ const propLines = [
2606
+ " propertiesHtml={indexEntry?.bodySlots?.properties}",
2607
+ ' afterHeaderHtml={indexEntry?.bodySlots?.["article.after-header"]}',
2608
+ ' afterMetaHtml={indexEntry?.bodySlots?.["article.after-meta"]}',
2609
+ ' beforeContentHtml={indexEntry?.bodySlots?.["article.before-content"]}',
2610
+ ' afterContentHtml={indexEntry?.bodySlots?.["article.after-content"]}',
2611
+ ' asideHtml={indexEntry?.bodySlots?.["article.aside"]}',
2612
+ ' footerHtml={indexEntry?.bodySlots?.["article.footer"]}'
2613
+ ];
2614
+ if (hasToc) {
2615
+ propLines.push(
2616
+ ' asideContent={<TableOfContents className="table-of-contents--desktop" items={tableOfContents} />}'
2617
+ );
2618
+ }
2619
+ const afterChildren = [];
2620
+ if (hasRecentPosts) afterChildren.push("<RecentPosts posts={recentPosts} />");
2621
+ if (hasDailyNotes) afterChildren.push("<DailyNotes notes={dailyNotes} />");
2622
+ if (afterChildren.length > 0) {
2623
+ propLines.push(` afterContent={<>${afterChildren.join("")}</>}`);
2624
+ }
2625
+ if (hasBacklinks) {
2626
+ propLines.push(
2627
+ " footerContent={<Backlinks backlinks={backlinks} />}"
2628
+ );
2629
+ }
2630
+ const titleHelper = needsTitle ? articleTitleHelper() : "";
2631
+ const dataBlock = dataLines.length > 0 ? `
2632
+ ${dataLines.join("\n")}
2633
+ ` : "";
2634
+ return `${importLines.join("\n")}
1704
2635
 
1705
2636
  export default createRoute(async (c) => {
1706
2637
  const manifest = await content.getManifest();
@@ -1710,10 +2641,10 @@ export default createRoute(async (c) => {
1710
2641
  }
1711
2642
 
1712
2643
  const post = await content.getProcessedContent("index");
1713
- if (!isPublished(config, post.frontmatter)) {
2644
+ if (indexEntry?.publishing?.routable === false) {
1714
2645
  return c.notFound();
1715
2646
  }
1716
-
2647
+ ${dataBlock}
1717
2648
  if (indexEntry) {
1718
2649
  c.set("htmlLanguage", indexEntry.publicLocation.metadata?.["l10n.lang"]);
1719
2650
  c.set("headTags", indexEntry.headTags ?? []);
@@ -1722,11 +2653,10 @@ export default createRoute(async (c) => {
1722
2653
  return c.render(
1723
2654
  <SiteArticle
1724
2655
  post={post}
1725
- afterMeta={indexEntry?.bodySlots?.["article.after-meta"]}
2656
+ ${propLines.join("\n")}
1726
2657
  />,
1727
2658
  );
1728
- });
1729
- `;
2659
+ });${titleHelper}`;
1730
2660
  }
1731
2661
  function staticIndexRoute() {
1732
2662
  return `import { createRoute } from "honox/factory";
@@ -1742,23 +2672,98 @@ export default createRoute((c) =>
1742
2672
  );
1743
2673
  `;
1744
2674
  }
1745
- function slugRoute() {
1746
- return `import { isPublished } from "@riebeckite/core";
1747
- import {
1748
- contentRouteSsgParams,
1749
- pluginPageSsgParams,
1750
- resolveRiebeckiteRoute,
1751
- } from "@riebeckite/honox/server";
1752
- import { createRoute } from "honox/factory";
1753
- import { SiteArticle } from "../components/article";
1754
- import { config } from "../config";
1755
- import { content } from "../content";
2675
+ function slugRoute(preset) {
2676
+ const hasToc = hasScaffoldPlugin(preset, "@riebeckite/plugin-toc");
2677
+ const hasBacklinks = hasScaffoldPlugin(
2678
+ preset,
2679
+ "@riebeckite/plugin-backlinks"
2680
+ );
2681
+ const hasLocalGraph = hasScaffoldPlugin(
2682
+ preset,
2683
+ "@riebeckite/plugin-local-graph"
2684
+ );
2685
+ const needsTitle = hasBacklinks || hasLocalGraph;
2686
+ const needsConfig = hasBacklinks || hasLocalGraph;
2687
+ const needsManifest = hasBacklinks || hasLocalGraph;
2688
+ const importLines = [
2689
+ "import {",
2690
+ " contentRouteSsgParams,",
2691
+ " pluginPageSsgParams,",
2692
+ " resolveRiebeckiteRoute,",
2693
+ '} from "@riebeckite/honox/server";'
2694
+ ];
2695
+ if (hasBacklinks) {
2696
+ importLines.push(
2697
+ 'import { Backlinks, getPublishedBacklinks } from "@riebeckite/plugin-backlinks";'
2698
+ );
2699
+ }
2700
+ if (hasLocalGraph) {
2701
+ importLines.push(
2702
+ 'import { LocalGraph, getLocalGraph } from "@riebeckite/plugin-local-graph";'
2703
+ );
2704
+ }
2705
+ if (hasToc) {
2706
+ importLines.push(
2707
+ 'import { extractTableOfContents, TableOfContents } from "@riebeckite/plugin-toc";'
2708
+ );
2709
+ }
2710
+ importLines.push('import { createRoute } from "honox/factory";');
2711
+ importLines.push('import { SiteArticle } from "../components/article";');
2712
+ if (needsConfig) importLines.push('import { config } from "../config";');
2713
+ importLines.push('import { content } from "../content";');
2714
+ const dataLines = [];
2715
+ if (needsManifest) {
2716
+ dataLines.push(" const manifest = await content.getManifest();");
2717
+ }
2718
+ if (hasBacklinks) {
2719
+ dataLines.push(
2720
+ " const backlinks = getPublishedBacklinks({ manifest, config, slug: route.entry.slug, resolveTitle: getArticleTitle });"
2721
+ );
2722
+ }
2723
+ if (hasLocalGraph) {
2724
+ dataLines.push(
2725
+ " const localGraph = getLocalGraph({ manifest, config, slug: route.entry.slug, resolveTitle: getArticleTitle });"
2726
+ );
2727
+ }
2728
+ if (hasToc) {
2729
+ dataLines.push(
2730
+ ' const tableOfContents = extractTableOfContents(post.html ?? "");'
2731
+ );
2732
+ }
2733
+ const propLines = [
2734
+ " propertiesHtml={route.entry.bodySlots?.properties}",
2735
+ ' afterHeaderHtml={route.entry.bodySlots?.["article.after-header"]}',
2736
+ ' afterMetaHtml={route.entry.bodySlots?.["article.after-meta"]}',
2737
+ ' beforeContentHtml={route.entry.bodySlots?.["article.before-content"]}',
2738
+ ' afterContentHtml={route.entry.bodySlots?.["article.after-content"]}',
2739
+ ' asideHtml={route.entry.bodySlots?.["article.aside"]}',
2740
+ ' footerHtml={route.entry.bodySlots?.["article.footer"]}'
2741
+ ];
2742
+ if (hasToc) {
2743
+ propLines.push(
2744
+ ' asideContent={<TableOfContents className="table-of-contents--desktop" items={tableOfContents} />}'
2745
+ );
2746
+ }
2747
+ const footerChildren = [];
2748
+ if (hasLocalGraph) {
2749
+ footerChildren.push("{localGraph && <LocalGraph graph={localGraph} />}");
2750
+ }
2751
+ if (hasBacklinks) {
2752
+ footerChildren.push("<Backlinks backlinks={backlinks} />");
2753
+ }
2754
+ if (footerChildren.length > 0) {
2755
+ propLines.push(` footerContent={<>${footerChildren.join("")}</>}`);
2756
+ }
2757
+ const titleHelper = needsTitle ? articleTitleHelper() : "";
2758
+ const dataBlock = dataLines.length > 0 ? `
2759
+ ${dataLines.join("\n")}
2760
+ ` : "";
2761
+ return `${importLines.join("\n")}
1756
2762
 
1757
2763
  export default createRoute(
1758
2764
  contentRouteSsgParams("/:slug{.+}", async () => {
1759
2765
  const manifest = await content.getManifest();
1760
- const contentPaths = manifest.entries
1761
- .filter((entry) => isPublished(config, entry.frontmatter))
2766
+ const contentPaths = manifest.publicEntries
1762
2767
  .filter((entry) => entry.permalink !== "/")
1763
2768
  .map((entry) => ({ slug: entry.permalink.replace(/^\\/+/, "") }));
1764
2769
  return [...contentPaths, ...(await pluginPageSsgParams(content))];
@@ -1780,22 +2785,18 @@ export default createRoute(
1780
2785
  }
1781
2786
 
1782
2787
  const post = await content.getProcessedContent(route.entry.slug);
1783
- if (!isPublished(config, post.frontmatter)) {
1784
- return c.notFound();
1785
- }
1786
-
2788
+ ${dataBlock}
1787
2789
  c.set("htmlLanguage", route.entry.publicLocation.metadata?.["l10n.lang"]);
1788
2790
  c.set("headTags", route.entry.headTags ?? []);
1789
2791
 
1790
2792
  return c.render(
1791
2793
  <SiteArticle
1792
2794
  post={post}
1793
- afterMeta={route.entry.bodySlots?.["article.after-meta"]}
2795
+ ${propLines.join("\n")}
1794
2796
  />,
1795
2797
  );
1796
2798
  },
1797
- );
1798
- `;
2799
+ );${titleHelper}`;
1799
2800
  }
1800
2801
 
1801
2802
  // src/scaffold/localized-content.ts
@@ -2160,7 +3161,7 @@ function localizedContentFiles(variables, preset) {
2160
3161
  (language) => themesContent(language, preset)
2161
3162
  );
2162
3163
  } else if (page === "guide") {
2163
- pushEnglishOnly("guide", guideContent);
3164
+ pushEnglishOnly("guide", () => guideContent(preset));
2164
3165
  } else if (page === "examples") {
2165
3166
  pushLocalized(
2166
3167
  "examples",
@@ -2354,10 +3355,50 @@ var INDEX = {
2354
3355
  de: "Übersetzte Seiten folgen der Konvention `<base>.<lang>.md` neben der Standarddatei (z. B. `about.ja.md`). Das l10n-Plugin liefert sie unter `/lang/`-Pfaden und verlinkt sie automatisch.",
2355
3356
  fr: "Les pages localisées suivent la convention `<base>.<lang>.md` à côté du fichier par défaut (ex. `about.ja.md`). Le plugin l10n les sert sous des chemins `/lang/` et les relie automatiquement.",
2356
3357
  ko: "번역 페이지는 기본 파일 옆에 `<base>.<lang>.md` 규칙으로 둡니다(예: `about.ja.md`). l10n 플러그인이 `/lang/` 경로로 서빙하며 자동으로 링크합니다."
3358
+ },
3359
+ editFileNote: {
3360
+ en: "This page is `content/index.md`. Open that file, change it, and save — the browser updates while you write.",
3361
+ ja: "このページは `content/index.md` です。ファイルを開いて書き換えると、書いている途中でもブラウザの表示が更新されます。",
3362
+ "zh-CN": "这个页面来自 `content/index.md`。打开这个文件修改并保存,浏览器会随你的输入更新。",
3363
+ es: "Esta página viene de `content/index.md`. Abre ese archivo, edítalo y guarda: el navegador se actualiza mientras escribes.",
3364
+ de: "Diese Seite stammt aus `content/index.md`. Öffne die Datei, ändere sie und speichere — der Browser aktualisiert sich beim Schreiben.",
3365
+ fr: "Cette page provient de `content/index.md`. Ouvrez ce fichier, modifiez-le et enregistrez — le navigateur se met à jour pendant que vous écrivez.",
3366
+ ko: "이 페이지는 `content/index.md`에서 만들어집니다. 그 파일을 열어 수정하고 저장하면 글을 쓰는 동안 브라우저가 갱신됩니다."
3367
+ },
3368
+ linkLine: {
3369
+ en: "Link pages with a WikiLink, for example `[[guide]]`.",
3370
+ ja: "ページ同士は WikiLink でリンクできます(例:`[[guide]]`)。",
3371
+ "zh-CN": "用 WikiLink 连接页面,例如 `[[guide]]`。",
3372
+ es: "Enlaza páginas con un WikiLink, por ejemplo `[[guide]]`.",
3373
+ de: "Verbinde Seiten mit einem WikiLink, zum Beispiel `[[guide]]`.",
3374
+ fr: "Reliez les pages avec un WikiLink, par exemple `[[guide]]`.",
3375
+ ko: "WikiLink로 페이지를 연결하세요. 예: `[[guide]]`."
2357
3376
  }
2358
3377
  };
3378
+ function starterIndexContent(title, language) {
3379
+ return [
3380
+ frontmatter(),
3381
+ heading(1, title),
3382
+ "",
3383
+ read(INDEX.lead, language),
3384
+ "",
3385
+ read(INDEX.editFileNote, language),
3386
+ "",
3387
+ heading(2, read(INDEX.editHeading, language)),
3388
+ read(INDEX.editLead, language),
3389
+ "",
3390
+ read(INDEX.linkLine, language),
3391
+ "",
3392
+ heading(2, read(INDEX.exploreHeading, language)),
3393
+ "",
3394
+ "- [Getting started](/guide)",
3395
+ "- [[examples]]",
3396
+ ""
3397
+ ].join("\n");
3398
+ }
2359
3399
  function indexContent(variables, preset, language) {
2360
3400
  const title = variables.title;
3401
+ if (preset.name === "starter") return starterIndexContent(title, language);
2361
3402
  return [
2362
3403
  frontmatter(),
2363
3404
  heading(1, title),
@@ -2371,10 +3412,6 @@ function indexContent(variables, preset, language) {
2371
3412
  `- [${read(INDEX.exploreThemes, language)}](/framework/themes)`,
2372
3413
  "- [Working examples](/examples/)",
2373
3414
  "- [Plugin reference](/reference/plugins/)"
2374
- ] : preset.name === "starter" ? [
2375
- heading(2, read(INDEX.exploreHeading, language)),
2376
- "- [Getting started](/guide/)",
2377
- "- [Example notes](/examples/)"
2378
3415
  ] : [],
2379
3416
  heading(2, read(INDEX.editHeading, language)),
2380
3417
  read(INDEX.editLead, language),
@@ -2426,7 +3463,7 @@ var THEMES_COPY = {
2426
3463
  es: "Este starter usa `@riebeckite/theme-default`. Para probar otro tema:",
2427
3464
  de: "Dieser Starter verwendet `@riebeckite/theme-default`. Um ein anderes Theme zu testen:",
2428
3465
  fr: "Ce starter utilise `@riebeckite/theme-default`. Pour essayer un autre thème :",
2429
- ko: "이 스타터는 `@riebeckite/theme-default`를 사용합니다. 다른 테마를 시도하려면:"
3466
+ ko: "이 스타터는 `@riebeckite/theme-default`를 사용합니다. 다른 테마를 시도하려면:"
2430
3467
  },
2431
3468
  switchStep1: {
2432
3469
  en: "Install the package:",
@@ -2435,7 +3472,7 @@ var THEMES_COPY = {
2435
3472
  es: "Instala el paquete:",
2436
3473
  de: "Installiere das Paket:",
2437
3474
  fr: "Installez le paquet :",
2438
- ko: "패키지를 설치합니다:"
3475
+ ko: "패키지를 설치합니다:"
2439
3476
  },
2440
3477
  switchStep2: {
2441
3478
  en: "Point `theme` at the new factory in `riebeckite.config.ts`:",
@@ -2444,7 +3481,7 @@ var THEMES_COPY = {
2444
3481
  es: "Apunta `theme` al nuevo factory en `riebeckite.config.ts`:",
2445
3482
  de: "Setze `theme` in `riebeckite.config.ts` auf den neuen Factory:",
2446
3483
  fr: "Pointez `theme` vers le nouveau factory dans `riebeckite.config.ts` :",
2447
- ko: "`riebeckite.config.ts`의 `theme`를 새 팩토리로 변경합니다:"
3484
+ ko: "`riebeckite.config.ts`의 `theme`를 새 팩토리로 변경합니다:"
2448
3485
  },
2449
3486
  tableHeading: {
2450
3487
  en: "The bundled themes",
@@ -2664,7 +3701,7 @@ var PLUGINS_COPY = {
2664
3701
  es: "Instala el paquete:",
2665
3702
  de: "Installiere das Paket:",
2666
3703
  fr: "Installez le paquet :",
2667
- ko: "패키지를 설치합니다:"
3704
+ ko: "패키지를 설치합니다:"
2668
3705
  },
2669
3706
  installStep2: {
2670
3707
  en: "Register it in the `plugins` array of `riebeckite.config.ts`:",
@@ -2673,7 +3710,7 @@ var PLUGINS_COPY = {
2673
3710
  es: "Regístralo en el array `plugins` de `riebeckite.config.ts`:",
2674
3711
  de: "Registriere es im `plugins`-Array der `riebeckite.config.ts`:",
2675
3712
  fr: "Enregistrez-le dans le tableau `plugins` de `riebeckite.config.ts` :",
2676
- ko: "`riebeckite.config.ts`의 `plugins` 배열에 등록합니다:"
3713
+ ko: "`riebeckite.config.ts`의 `plugins` 배열에 등록합니다:"
2677
3714
  },
2678
3715
  fullList: {
2679
3716
  en: "The complete plugin index lives in the repository:",
@@ -2682,7 +3719,7 @@ var PLUGINS_COPY = {
2682
3719
  es: "El índice completo de plugins vive en el repositorio:",
2683
3720
  de: "Der vollständige Plugin-Index liegt im Repository:",
2684
3721
  fr: "L'index complet des plugins se trouve dans le dépôt :",
2685
- ko: "전체 플러그인 목록은 저장소에 있습니다:"
3722
+ ko: "전체 플러그인 목록은 저장소에 있습니다:"
2686
3723
  },
2687
3724
  fullListLabel: {
2688
3725
  en: "Riebeckite plugins on GitHub",
@@ -3188,7 +4225,65 @@ function pluginsContent(language) {
3188
4225
  lines.push("");
3189
4226
  return lines.join("\n");
3190
4227
  }
3191
- function guideContent() {
4228
+ function starterGuideContent() {
4229
+ return [
4230
+ frontmatter(),
4231
+ heading(1, "Getting started"),
4232
+ "",
4233
+ "This site was generated from a Riebeckite scaffold preset.",
4234
+ "Everything below lives in this repository, ready to edit.",
4235
+ "",
4236
+ heading(2, "Run the site"),
4237
+ "",
4238
+ codeBlock("sh", ["npm install", "npm exec riebeckite dev"].join("\n")),
4239
+ "",
4240
+ "Open the URL printed in the terminal. The page reloads every time",
4241
+ "you save a Markdown file.",
4242
+ "",
4243
+ heading(2, "Add a page"),
4244
+ "",
4245
+ "Create `content/hello.md`:",
4246
+ "",
4247
+ codeBlock(
4248
+ "md",
4249
+ [
4250
+ "---",
4251
+ "publish: true",
4252
+ "---",
4253
+ "",
4254
+ "# Hello",
4255
+ "",
4256
+ "This is my second page."
4257
+ ].join("\n")
4258
+ ),
4259
+ "",
4260
+ "The dev server serves it at `/hello`.",
4261
+ "",
4262
+ heading(2, "Link the pages"),
4263
+ "",
4264
+ "Write `[[hello]]` anywhere and it becomes a link to that page.",
4265
+ "",
4266
+ heading(2, "Translate a page"),
4267
+ "",
4268
+ "Copy a file next to the original with the language suffix, for",
4269
+ "example `hello.ja.md`. The language switcher picks it up.",
4270
+ "",
4271
+ heading(2, "Build"),
4272
+ "",
4273
+ codeBlock("sh", "npm exec riebeckite build"),
4274
+ "",
4275
+ "The static site is written to `dist/`.",
4276
+ "",
4277
+ heading(2, "Extend"),
4278
+ "",
4279
+ "Plugins and themes are registered in `riebeckite.config.ts`.",
4280
+ "Install a package, import its factory, and add it to the `plugins`",
4281
+ "array, or point `theme` at a new theme factory.",
4282
+ ""
4283
+ ].join("\n");
4284
+ }
4285
+ function guideContent(preset) {
4286
+ if (preset.name !== "showcase") return starterGuideContent();
3192
4287
  return [
3193
4288
  frontmatter(),
3194
4289
  heading(1, "Getting started"),
@@ -3213,9 +4308,9 @@ function guideContent() {
3213
4308
  codeBlock(
3214
4309
  "sh",
3215
4310
  [
3216
- "pnpm install",
3217
- "pnpm exec riebeckite dev",
3218
- "pnpm exec riebeckite build"
4311
+ "npm install",
4312
+ "npm exec riebeckite dev",
4313
+ "npm exec riebeckite build"
3219
4314
  ].join("\n")
3220
4315
  ),
3221
4316
  "",
@@ -3253,7 +4348,77 @@ var SHOWCASE_GUIDE_DEMOS = [
3253
4348
  "@riebeckite/plugin-canvas",
3254
4349
  "@riebeckite/plugin-flashcards"
3255
4350
  ];
4351
+ var STARTER_EXAMPLES = {
4352
+ heading: {
4353
+ en: "Example",
4354
+ ja: "サンプル"
4355
+ },
4356
+ intro: {
4357
+ en: "A small page showing what Riebeckite Markdown can do. Copy any section into your own files.",
4358
+ ja: "Riebeckite の Markdown でできることをまとめた小さなページです。必要なセクションを自分のファイルにコピーできます。"
4359
+ },
4360
+ linksHeading: {
4361
+ en: "Links",
4362
+ ja: "リンク"
4363
+ },
4364
+ linksBody: {
4365
+ en: "WikiLinks connect pages: [[guide]] opens Getting started. Regular Markdown links work as well — [Home](/) returns to the home page.",
4366
+ ja: "WikiLink でページをつなげます:[[guide]] は Getting started を開きます。通常の Markdown リンクも使えます — [Home](/) でホームに戻れます。"
4367
+ },
4368
+ codeHeading: {
4369
+ en: "Code",
4370
+ ja: "コード"
4371
+ },
4372
+ codeIntro: {
4373
+ en: "A fenced code block keeps its formatting and gains a toolbar from the code plugins:",
4374
+ ja: "フェンス付きコードブロックは書式を保ったまま表示され、コード系プラグインでツールバーが付きます:"
4375
+ },
4376
+ frontmatterHeading: {
4377
+ en: "Frontmatter",
4378
+ ja: "フロントマター"
4379
+ },
4380
+ frontmatterBody: {
4381
+ en: "The block between the `---` lines at the top of a file controls the page. `publish: true` keeps it in the built site:",
4382
+ ja: "ファイル先頭の `---` に挟まれた部分がページを制御します。`publish: true` を書き続けるとビルド対象に残ります:"
4383
+ }
4384
+ };
4385
+ function starterExamplesContent(language) {
4386
+ return [
4387
+ frontmatter(),
4388
+ heading(1, readSummary(STARTER_EXAMPLES.heading, language)),
4389
+ "",
4390
+ readSummary(STARTER_EXAMPLES.intro, language),
4391
+ "",
4392
+ heading(2, readSummary(STARTER_EXAMPLES.linksHeading, language)),
4393
+ "",
4394
+ readSummary(STARTER_EXAMPLES.linksBody, language),
4395
+ "",
4396
+ heading(2, readSummary(STARTER_EXAMPLES.codeHeading, language)),
4397
+ "",
4398
+ readSummary(STARTER_EXAMPLES.codeIntro, language),
4399
+ "",
4400
+ codeBlock(
4401
+ "ts",
4402
+ [
4403
+ "export function hello(): string {",
4404
+ ' return "Hello from Riebeckite";',
4405
+ "}"
4406
+ ].join("\n")
4407
+ ),
4408
+ "",
4409
+ heading(2, readSummary(STARTER_EXAMPLES.frontmatterHeading, language)),
4410
+ "",
4411
+ readSummary(STARTER_EXAMPLES.frontmatterBody, language),
4412
+ "",
4413
+ codeBlock(
4414
+ "yaml",
4415
+ ["---", "publish: true", "title: Example", "---"].join("\n")
4416
+ ),
4417
+ ""
4418
+ ].join("\n");
4419
+ }
3256
4420
  function examplesContent(preset, language) {
4421
+ if (preset.name === "starter") return starterExamplesContent(language);
3257
4422
  const lines = [
3258
4423
  frontmatter(),
3259
4424
  heading(1, readSummary(EXAMPLES_COPY.heading, language)),
@@ -3308,7 +4473,7 @@ function referencePluginsContent(preset, variables) {
3308
4473
  "Every registered plugin is listed below with the Markdown that triggers it,",
3309
4474
  "or with the Markdown/frontmatter/content shape it reads when the plugin is",
3310
4475
  "site-wide rather than block-based. Paste the examples into files under",
3311
- "`content/` and run `pnpm exec riebeckite dev` to inspect the rendered result.",
4476
+ "`content/` and run `npm exec riebeckite dev` to inspect the rendered result.",
3312
4477
  ""
3313
4478
  );
3314
4479
  for (const plugin of preset.plugins) {
@@ -3373,7 +4538,7 @@ function referenceThemesContent(preset) {
3373
4538
  }
3374
4539
 
3375
4540
  // src/scaffold/version.ts
3376
- var RIEBECKITE_VERSION = "^0.0.13";
4541
+ var RIEBECKITE_VERSION = "^0.0.15";
3377
4542
 
3378
4543
  // src/scaffold/templates.ts
3379
4544
  function siteTemplateFiles(preset, variables) {
@@ -3403,7 +4568,7 @@ function readPackageAsset(fileName) {
3403
4568
  new URL(`../assets/${fileName}`, import.meta.url)
3404
4569
  ];
3405
4570
  for (const candidate of candidates) {
3406
- if (fs6.existsSync(candidate)) return fs6.readFileSync(candidate);
4571
+ if (fs7.existsSync(candidate)) return fs7.readFileSync(candidate);
3407
4572
  }
3408
4573
  throw new Error(`Missing Riebeckite scaffold asset: ${fileName}`);
3409
4574
  }
@@ -3432,7 +4597,7 @@ function appFiles(preset) {
3432
4597
  style: () => style(preset),
3433
4598
  renderer: () => renderer(preset),
3434
4599
  index: () => indexRoute(preset),
3435
- slug: () => slugRoute(),
4600
+ slug: () => slugRoute(preset),
3436
4601
  header: () => siteHeader(),
3437
4602
  article: () => article()
3438
4603
  };
@@ -3688,7 +4853,7 @@ var README_WORDS = {
3688
4853
  demoPages: "Try the demos",
3689
4854
  demoPagesBody: "The generated site ships content pages that exercise these features:",
3690
4855
  copyDemos: "Copy-paste demos",
3691
- copyDemosBody: "Paste any of these snippets into a Markdown file under `content/` and run `pnpm exec riebeckite dev`. Each one renders through a plugin this preset registers."
4856
+ copyDemosBody: "Paste any of these snippets into a Markdown file under `content/` and run `npm exec riebeckite dev`. Each one renders through a plugin this preset registers."
3692
4857
  },
3693
4858
  ja: {
3694
4859
  included: "含まれているもの",
@@ -3712,7 +4877,7 @@ var README_WORDS = {
3712
4877
  demoPages: "デモを試す",
3713
4878
  demoPagesBody: "生成されたサイトには、各機能を体験できるコンテンツページが含まれています:",
3714
4879
  copyDemos: "コピーして使えるデモ",
3715
- copyDemosBody: "以下のスニペットを `content/` 配下の Markdown ファイルに貼り付けて `pnpm exec riebeckite dev` を実行してください。それぞれ、このプリセットが登録しているプラグインでレンダリングされます。"
4880
+ copyDemosBody: "以下のスニペットを `content/` 配下の Markdown ファイルに貼り付けて `npm exec riebeckite dev` を実行してください。それぞれ、このプリセットが登録しているプラグインでレンダリングされます。"
3716
4881
  }
3717
4882
  };
3718
4883
  function readme(preset, variables) {
@@ -3745,10 +4910,10 @@ function readmeEn(preset, variables) {
3745
4910
  lines.push(`## ${words.commands}`, "");
3746
4911
  lines.push(
3747
4912
  "```sh",
3748
- "pnpm install",
3749
- "pnpm exec riebeckite check",
3750
- "pnpm exec riebeckite dev",
3751
- "pnpm exec riebeckite build",
4913
+ "npm install",
4914
+ "npm exec riebeckite check",
4915
+ "npm exec riebeckite dev",
4916
+ "npm exec riebeckite build",
3752
4917
  "```",
3753
4918
  ""
3754
4919
  );
@@ -3793,10 +4958,10 @@ function readmeJa(preset, variables) {
3793
4958
  lines.push(`## ${words.commands}`, "");
3794
4959
  lines.push(
3795
4960
  "```sh",
3796
- "pnpm install",
3797
- "pnpm exec riebeckite check",
3798
- "pnpm exec riebeckite dev",
3799
- "pnpm exec riebeckite build",
4961
+ "npm install",
4962
+ "npm exec riebeckite check",
4963
+ "npm exec riebeckite dev",
4964
+ "npm exec riebeckite build",
3800
4965
  "```",
3801
4966
  ""
3802
4967
  );
@@ -4263,7 +5428,7 @@ var ScaffoldSiteError = class extends Error {
4263
5428
  }
4264
5429
  };
4265
5430
  async function scaffoldRiebeckiteSite(options) {
4266
- const targetDirectory = path7.resolve(options.targetDirectory);
5431
+ const targetDirectory = path9.resolve(options.targetDirectory);
4267
5432
  const preset = resolveScaffoldPreset(options.preset);
4268
5433
  validateDeploymentOptions(options);
4269
5434
  const files = [
@@ -4276,9 +5441,9 @@ async function scaffoldRiebeckiteSite(options) {
4276
5441
  options.overwrite ?? false
4277
5442
  );
4278
5443
  for (const file of files) {
4279
- const filePath = path7.join(targetDirectory, file.path);
4280
- await fs7.mkdir(path7.dirname(filePath), { recursive: true });
4281
- await fs7.writeFile(filePath, file.content);
5444
+ const filePath = path9.join(targetDirectory, file.path);
5445
+ await fs8.mkdir(path9.dirname(filePath), { recursive: true });
5446
+ await fs8.writeFile(filePath, file.content);
4282
5447
  }
4283
5448
  return { targetDirectory, files: files.map((file) => file.path) };
4284
5449
  }
@@ -4314,7 +5479,7 @@ function validateDeploymentOptions(options) {
4314
5479
  }
4315
5480
  function templateVariables(options, targetDirectory) {
4316
5481
  const name = normalizePackageName(
4317
- options.name ?? path7.basename(targetDirectory)
5482
+ options.name ?? path9.basename(targetDirectory)
4318
5483
  );
4319
5484
  return {
4320
5485
  name,
@@ -4327,9 +5492,9 @@ function templateVariables(options, targetDirectory) {
4327
5492
  async function assertTargetWritable(targetDirectory, files, overwrite) {
4328
5493
  let entries;
4329
5494
  try {
4330
- entries = await fs7.readdir(targetDirectory);
5495
+ entries = await fs8.readdir(targetDirectory);
4331
5496
  } catch (error) {
4332
- if (isNotFoundError2(error)) return;
5497
+ if (isNotFoundError3(error)) return;
4333
5498
  throw error;
4334
5499
  }
4335
5500
  const conflicts = entries.filter(
@@ -4347,20 +5512,31 @@ function normalizePackageName(value) {
4347
5512
  const normalized = value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[-_.]+/, "").replace(/[-_.]+$/, "");
4348
5513
  return normalized || "riebeckite-site";
4349
5514
  }
4350
- function isNotFoundError2(error) {
5515
+ function isNotFoundError3(error) {
4351
5516
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
4352
5517
  }
4353
5518
 
4354
5519
  // src/scaffold/next-steps.ts
4355
- function formatScaffoldNextSteps(relativeDirectory) {
5520
+ function formatScaffoldNextSteps(relativeDirectory, options = {}) {
4356
5521
  const lines = ["Next steps:"];
4357
5522
  if (relativeDirectory !== ".") {
4358
5523
  lines.push(` cd ${relativeDirectory}`);
4359
5524
  }
4360
- lines.push(" pnpm install");
4361
- lines.push(" pnpm exec riebeckite check");
4362
- lines.push(" pnpm exec riebeckite dev");
4363
- lines.push(" pnpm exec riebeckite build");
5525
+ lines.push(" npm install");
5526
+ lines.push(" npm exec riebeckite check");
5527
+ lines.push(" npm exec riebeckite dev");
5528
+ lines.push(" npm exec riebeckite build");
5529
+ if (options.editFile !== void 0) {
5530
+ lines.push("");
5531
+ lines.push("Then edit:");
5532
+ lines.push(` ${options.editFile}`);
5533
+ }
5534
+ if (options.externalContent === true) {
5535
+ lines.push("");
5536
+ lines.push(
5537
+ "Copy github/notify-site.yml to the content repository as .github/workflows/notify-site.yml."
5538
+ );
5539
+ }
4364
5540
  return lines.join("\n");
4365
5541
  }
4366
5542
  export {