@riebeckite/honox 0.0.14 → 0.0.16

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
@@ -254,12 +254,51 @@ function isJavaScriptModule(filePath) {
254
254
  return /\.[cm]?[jt]sx?$/.test(filePath);
255
255
  }
256
256
 
257
- // src/scaffold/index.ts
258
- import fs7 from "node:fs/promises";
259
- import path7 from "node:path";
257
+ // src/scaffold/wrangler-defaults.ts
258
+ var WRANGLER_DEFAULTS = {
259
+ /** JSON schema for IDE support and validation */
260
+ $schema: "node_modules/wrangler/config-schema.json",
261
+ /** Worker name - becomes <name>.<account>.workers.dev */
262
+ name: "riebeckite-site",
263
+ /** Compatibility date - update when Workers runtime changes require it */
264
+ compatibility_date: "2026-06-09",
265
+ /** Required for Node.js APIs used by Riebeckite */
266
+ compatibility_flags: ["nodejs_compat"],
267
+ /** Static assets directory - Riebeckite builds to ./dist */
268
+ assets: { directory: "./dist" }
269
+ };
270
+ var WRANGLER_VERSION = "^4.83.0";
271
+ var DEFAULT_WORKER_NAME = "riebeckite-site";
272
+ var WORKER_NAME_MAX_LENGTH = 63;
273
+ function workerNameFromDirectory(directoryName) {
274
+ const normalized = directoryName.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
275
+ const name = normalized.slice(0, WORKER_NAME_MAX_LENGTH).replace(/-+$/g, "");
276
+ return name.length > 0 ? name : DEFAULT_WORKER_NAME;
277
+ }
278
+ function buildDefaultWranglerConfig(workerName) {
279
+ return `${JSON.stringify({ ...WRANGLER_DEFAULTS, name: workerName }, null, 2)}
280
+ `;
281
+ }
282
+ var GITHUB_ACTIONS_SECRETS = {
283
+ /** Cloudflare API token with Workers Scripts: Edit permission */
284
+ CLOUDFLARE_API_TOKEN: "CLOUDFLARE_API_TOKEN",
285
+ /** Cloudflare Account ID (found in Workers & Pages dashboard URL) */
286
+ CLOUDFLARE_ACCOUNT_ID: "CLOUDFLARE_ACCOUNT_ID",
287
+ /** Optional: Token for content repo to dispatch to site repo */
288
+ SITE_DISPATCH_TOKEN: "SITE_DISPATCH_TOKEN",
289
+ /** Optional: Token for site repo to read content repo */
290
+ RIEBECKITE_CONTENT_READ_TOKEN: "RIEBECKITE_CONTENT_READ_TOKEN"
291
+ };
292
+ var PACKAGE_MANAGER = "npm";
260
293
 
261
294
  // src/scaffold/deployment.ts
262
295
  var GITHUB_REPOSITORY_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
296
+ var BUILD_STATE_CACHE_KEY_PREFIX = "riebeckite-build-v1";
297
+ var LEGACY_BUILD_CACHE_KEY_PREFIX = "riebeckite-content-v3";
298
+ var BUILD_STATE_CACHE_PATHS = [
299
+ ".riebeckite/cache",
300
+ ".riebeckite/build/content-state.json"
301
+ ];
263
302
  function assertGitHubRepository(value, option) {
264
303
  if (!GITHUB_REPOSITORY_PATTERN.test(value)) {
265
304
  throw new Error(
@@ -294,18 +333,14 @@ function deploymentTemplateFiles(options) {
294
333
  ] : []
295
334
  ];
296
335
  }
336
+ function wranglerJsoncFile(workerName) {
337
+ return {
338
+ path: "wrangler.jsonc",
339
+ content: buildDefaultWranglerConfig(workerName)
340
+ };
341
+ }
297
342
  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
- )}
308
- `;
343
+ return buildDefaultWranglerConfig(WRANGLER_DEFAULTS.name);
309
344
  }
310
345
  function deployWorkflow(contentRepository) {
311
346
  const contentCheckout = contentRepository === void 0 ? "" : `
@@ -347,6 +382,16 @@ ${contentCheckout}
347
382
  - name: Install dependencies
348
383
  run: npm ci
349
384
 
385
+ - name: Restore Riebeckite build state
386
+ uses: actions/cache@v4
387
+ with:
388
+ path: |
389
+ ${BUILD_STATE_CACHE_PATHS.map((entry) => ` ${entry}`).join("\n")}
390
+ key: ${BUILD_STATE_CACHE_KEY_PREFIX}-\${{ runner.os }}-\${{ hashFiles('package-lock.json') }}-\${{ github.run_id }}-\${{ github.run_attempt }}
391
+ restore-keys: |
392
+ ${BUILD_STATE_CACHE_KEY_PREFIX}-\${{ runner.os }}-\${{ hashFiles('package-lock.json') }}-
393
+ ${LEGACY_BUILD_CACHE_KEY_PREFIX}-\${{ runner.os }}-\${{ hashFiles('package-lock.json') }}
394
+
350
395
  - name: Validate configuration and plugins
351
396
  run: npm exec riebeckite check
352
397
 
@@ -356,8 +401,8 @@ ${contentCheckout}
356
401
  - name: Deploy the built assets
357
402
  uses: cloudflare/wrangler-action@v3
358
403
  with:
359
- apiToken: \${{ secrets.CLOUDFLARE_API_TOKEN }}
360
- accountId: \${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
404
+ apiToken: \${{ secrets.${GITHUB_ACTIONS_SECRETS.CLOUDFLARE_API_TOKEN} }}
405
+ accountId: \${{ secrets.${GITHUB_ACTIONS_SECRETS.CLOUDFLARE_ACCOUNT_ID} }}
361
406
  command: deploy
362
407
  `;
363
408
  }
@@ -377,7 +422,7 @@ jobs:
377
422
  steps:
378
423
  - name: Validate dispatch configuration
379
424
  env:
380
- SITE_DISPATCH_TOKEN: \${{ secrets.SITE_DISPATCH_TOKEN }}
425
+ SITE_DISPATCH_TOKEN: \${{ secrets.${GITHUB_ACTIONS_SECRETS.SITE_DISPATCH_TOKEN} }}
381
426
  run: |
382
427
  if [ -z "$SITE_DISPATCH_TOKEN" ]; then
383
428
  echo "::error::SITE_DISPATCH_TOKEN is not configured. Add a token that can dispatch to ${siteRepository}."
@@ -387,7 +432,7 @@ jobs:
387
432
  - name: Notify the site repository
388
433
  uses: actions/github-script@v7
389
434
  with:
390
- github-token: \${{ secrets.SITE_DISPATCH_TOKEN }}
435
+ github-token: \${{ secrets.${GITHUB_ACTIONS_SECRETS.SITE_DISPATCH_TOKEN} }}
391
436
  script: |
392
437
  await github.rest.repos.createDispatchEvent({
393
438
  owner: ${JSON.stringify(owner)},
@@ -397,6 +442,10 @@ jobs:
397
442
  `;
398
443
  }
399
444
 
445
+ // src/scaffold/index.ts
446
+ import fs8 from "node:fs/promises";
447
+ import path9 from "node:path";
448
+
400
449
  // src/scaffold/presets.ts
401
450
  var SCAFFOLD_LANGUAGES = [
402
451
  "en",
@@ -924,10 +973,10 @@ function resolveScaffoldPreset(value) {
924
973
  }
925
974
 
926
975
  // src/scaffold/templates.ts
927
- import fs6 from "node:fs";
976
+ import fs7 from "node:fs";
928
977
 
929
978
  // src/vite_plugin.ts
930
- import path6 from "node:path";
979
+ import path8 from "node:path";
931
980
 
932
981
  // src/asset_entries.ts
933
982
  import fs4 from "node:fs";
@@ -1037,11 +1086,288 @@ ${initializers.map((initializer) => ` ${initializer};`).join("\n")}
1037
1086
  `;
1038
1087
  }
1039
1088
 
1089
+ // src/content_assets.ts
1090
+ import fs5 from "node:fs/promises";
1091
+ import path6 from "node:path";
1092
+ import {
1093
+ isIgnoredContentPath as isIgnoredContentPath2,
1094
+ isImagePath as isImagePath2,
1095
+ normalizeContentPath as normalizeContentPath2
1096
+ } from "@riebeckite/core";
1097
+
1098
+ // src/content_watch.ts
1099
+ import path5 from "node:path";
1100
+ import {
1101
+ isAttachmentPath,
1102
+ isIgnoredContentPath,
1103
+ isImagePath,
1104
+ normalizeContentPath
1105
+ } from "@riebeckite/core";
1106
+ var CONTENT_RELOAD_DEBOUNCE_MS = 60;
1107
+ var configuredWatchers = /* @__PURE__ */ new WeakSet();
1108
+ function riebeckiteContentWatch(options) {
1109
+ return {
1110
+ name: "riebeckite-content-watch",
1111
+ apply: "serve",
1112
+ enforce: "post",
1113
+ configureServer(server2) {
1114
+ if (configuredWatchers.has(server2.watcher)) return;
1115
+ configuredWatchers.add(server2.watcher);
1116
+ const appRoot = options.appRoot();
1117
+ const contentRoot = options.contentRoot();
1118
+ const exclude = options.exclude();
1119
+ const isContentFile = (file) => isPathInsideDirectory(contentRoot, file);
1120
+ server2.watcher.add(contentRoot);
1121
+ suppressRestartOnContentEvents(server2, isContentFile);
1122
+ let timer;
1123
+ const invalidate = () => {
1124
+ if (timer) clearTimeout(timer);
1125
+ timer = setTimeout(() => {
1126
+ timer = void 0;
1127
+ invalidateApplicationRuntime(server2, appRoot);
1128
+ server2.hot.send({ type: "full-reload" });
1129
+ }, CONTENT_RELOAD_DEBOUNCE_MS);
1130
+ };
1131
+ server2.httpServer?.once("close", () => {
1132
+ if (timer) clearTimeout(timer);
1133
+ });
1134
+ const handle = (event) => (file) => {
1135
+ if (!isContentFile(file)) return;
1136
+ const logicalPath = toLogicalPath(contentRoot, file);
1137
+ if (isIgnoredContentPath(logicalPath, exclude)) return;
1138
+ if (event === "change" && isSiteAssetPath(logicalPath)) {
1139
+ server2.hot.send({ type: "full-reload" });
1140
+ return;
1141
+ }
1142
+ invalidate();
1143
+ };
1144
+ server2.watcher.on("add", handle("add"));
1145
+ server2.watcher.on("change", handle("change"));
1146
+ server2.watcher.on("unlink", handle("unlink"));
1147
+ }
1148
+ };
1149
+ }
1150
+ function isSiteAssetPath(logicalPath) {
1151
+ return isImagePath(logicalPath) || isAttachmentPath(logicalPath);
1152
+ }
1153
+ function isPathInsideDirectory(directory, file) {
1154
+ const root = normalizeForCompare(directory).replace(/\/+$/, "");
1155
+ if (!root) return false;
1156
+ const target = normalizeForCompare(file);
1157
+ return target === root || target.startsWith(`${root}/`);
1158
+ }
1159
+ function toLogicalPath(directory, file) {
1160
+ return normalizeContentPath(
1161
+ path5.relative(normalizeContentPath(directory), normalizeContentPath(file))
1162
+ );
1163
+ }
1164
+ function invalidateApplicationRuntime(server2, appRoot) {
1165
+ const moduleGraph = server2.environments?.ssr?.moduleGraph;
1166
+ if (!moduleGraph) return 0;
1167
+ const appDirectory = path5.join(appRoot, "app") + path5.sep;
1168
+ const seen = /* @__PURE__ */ new Set();
1169
+ const timestamp = Date.now();
1170
+ let invalidated = 0;
1171
+ for (const mod of moduleGraph.idToModuleMap.values()) {
1172
+ const file = mod.file;
1173
+ if (!file || !isPathInsideDirectory(appDirectory, file)) continue;
1174
+ moduleGraph.invalidateModule(mod, seen, timestamp);
1175
+ invalidated += 1;
1176
+ }
1177
+ return invalidated;
1178
+ }
1179
+ function suppressRestartOnContentEvents(server2, isContentFile) {
1180
+ for (const event of ["add", "unlink"]) {
1181
+ const listeners = server2.watcher.rawListeners(event).slice();
1182
+ server2.watcher.removeAllListeners(event);
1183
+ for (const listener of listeners) {
1184
+ server2.watcher.on(event, (file) => {
1185
+ if (isContentFile(file)) return;
1186
+ listener(file);
1187
+ });
1188
+ }
1189
+ }
1190
+ }
1191
+ function normalizeForCompare(value) {
1192
+ const normalized = normalizeContentPath(value);
1193
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
1194
+ }
1195
+
1196
+ // src/content_assets.ts
1197
+ var IMAGE_CONTENT_TYPES = {
1198
+ png: "image/png",
1199
+ jpg: "image/jpeg",
1200
+ jpeg: "image/jpeg",
1201
+ gif: "image/gif",
1202
+ svg: "image/svg+xml",
1203
+ webp: "image/webp",
1204
+ bmp: "image/bmp"
1205
+ };
1206
+ function riebeckiteContentAssets(options) {
1207
+ return {
1208
+ name: "riebeckite-content-assets",
1209
+ apply: "serve",
1210
+ enforce: "pre",
1211
+ configureServer(server2) {
1212
+ const contentRoot = options.contentRoot();
1213
+ const entry = path6.join(options.appRoot(), "app", "server.ts");
1214
+ const base = server2.config.base;
1215
+ const publicDir = server2.config.publicDir;
1216
+ let cachedManifest;
1217
+ let cachedPaths = /* @__PURE__ */ new Set();
1218
+ const resolvePublicImagePaths = async () => {
1219
+ const module = await server2.ssrLoadModule(entry);
1220
+ const manifest = await module.content?.getManifest();
1221
+ if (!manifest) return /* @__PURE__ */ new Set();
1222
+ if (manifest !== cachedManifest) {
1223
+ cachedManifest = manifest;
1224
+ cachedPaths = collectPublicImagePaths(manifest);
1225
+ }
1226
+ return cachedPaths;
1227
+ };
1228
+ const middleware = async (request, response, next) => {
1229
+ try {
1230
+ if (request.method !== "GET" && request.method !== "HEAD") {
1231
+ next();
1232
+ return;
1233
+ }
1234
+ const logicalPath = toContentPath(request.url, base);
1235
+ if (!logicalPath || !isImagePath2(logicalPath)) {
1236
+ next();
1237
+ return;
1238
+ }
1239
+ if (isIgnoredContentPath2(logicalPath)) {
1240
+ next();
1241
+ return;
1242
+ }
1243
+ const contentType = imageContentType(logicalPath);
1244
+ if (!contentType) {
1245
+ next();
1246
+ return;
1247
+ }
1248
+ const available = await resolvePublicImagePaths();
1249
+ if (!available.has(logicalPath)) {
1250
+ next();
1251
+ return;
1252
+ }
1253
+ const file = path6.join(contentRoot, logicalPath);
1254
+ if (!isPathInsideDirectory(contentRoot, file)) {
1255
+ next();
1256
+ return;
1257
+ }
1258
+ if (await hasSiteOwnedFile(publicDir, logicalPath)) {
1259
+ next();
1260
+ return;
1261
+ }
1262
+ const content = await fs5.readFile(file);
1263
+ response.statusCode = 200;
1264
+ response.setHeader("Content-Type", contentType);
1265
+ response.setHeader("Content-Length", String(content.byteLength));
1266
+ response.setHeader("Cache-Control", "no-cache");
1267
+ if (request.method === "HEAD") {
1268
+ response.end();
1269
+ return;
1270
+ }
1271
+ response.end(content);
1272
+ return;
1273
+ } catch {
1274
+ next();
1275
+ }
1276
+ };
1277
+ server2.middlewares.use((request, response, next) => {
1278
+ void middleware(
1279
+ request,
1280
+ response,
1281
+ next
1282
+ );
1283
+ });
1284
+ }
1285
+ };
1286
+ }
1287
+ function collectPublicImagePaths(manifest) {
1288
+ const paths2 = /* @__PURE__ */ new Set();
1289
+ for (const entry of manifest.publicEntries ?? []) {
1290
+ for (const asset of entry.assets ?? []) {
1291
+ const normalized = normalizeContentPath2(asset.path);
1292
+ if (isImagePath2(normalized)) paths2.add(normalized);
1293
+ }
1294
+ }
1295
+ return paths2;
1296
+ }
1297
+ async function collectSiteOwnedOutputPaths(publicDir) {
1298
+ const paths2 = /* @__PURE__ */ new Set();
1299
+ if (!publicDir) return paths2;
1300
+ const root = path6.resolve(publicDir);
1301
+ const walk = async (directory) => {
1302
+ const entries = await fs5.readdir(directory, { withFileTypes: true }).catch(() => void 0);
1303
+ if (!entries) return;
1304
+ for (const entry of entries) {
1305
+ const full = path6.join(directory, entry.name);
1306
+ if (entry.isDirectory()) {
1307
+ await walk(full);
1308
+ } else {
1309
+ paths2.add(path6.relative(root, full).replaceAll("\\", "/"));
1310
+ }
1311
+ }
1312
+ };
1313
+ await walk(root);
1314
+ return paths2;
1315
+ }
1316
+ function toContentPath(url, base) {
1317
+ if (!url) return void 0;
1318
+ const withoutQuery = url.split("?")[0]?.split("#")[0];
1319
+ if (!withoutQuery) return void 0;
1320
+ let decoded = withoutQuery;
1321
+ try {
1322
+ decoded = decodeURIComponent(withoutQuery);
1323
+ } catch {
1324
+ return void 0;
1325
+ }
1326
+ let pathname = normalizeContentPath2(decoded);
1327
+ if (base && base !== "/" && pathname.startsWith(base)) {
1328
+ pathname = normalizeContentPath2(pathname.slice(base.length));
1329
+ }
1330
+ const logicalPath = pathname.replace(/^\/+/, "");
1331
+ if (!logicalPath) return void 0;
1332
+ if (logicalPath.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
1333
+ return void 0;
1334
+ }
1335
+ return logicalPath;
1336
+ }
1337
+ function imageContentType(logicalPath) {
1338
+ const extension = logicalPath.split("/").at(-1)?.split(".").at(-1);
1339
+ if (!extension) return void 0;
1340
+ return IMAGE_CONTENT_TYPES[extension.toLowerCase()];
1341
+ }
1342
+ async function hasSiteOwnedFile(publicDir, logicalPath) {
1343
+ if (!publicDir) return false;
1344
+ const file = path6.join(publicDir, logicalPath);
1345
+ try {
1346
+ return (await fs5.stat(file)).isFile();
1347
+ } catch {
1348
+ return false;
1349
+ }
1350
+ }
1351
+
1040
1352
  // src/ssg_plugin.ts
1041
- import { relative } from "node:path";
1353
+ import { createHash, randomUUID } from "node:crypto";
1354
+ import {
1355
+ mkdir,
1356
+ readdir,
1357
+ readFile,
1358
+ rename as rename2,
1359
+ rm,
1360
+ stat,
1361
+ writeFile
1362
+ } from "node:fs/promises";
1363
+ import { dirname, join, relative, resolve } from "node:path";
1364
+ import { performance } from "node:perf_hooks";
1042
1365
  import { defaultExtensionMap, toSSG } from "hono/ssg";
1043
- import { createServer } from "vite";
1366
+ import {
1367
+ createServer
1368
+ } from "vite";
1044
1369
  var defaultEntry = "./src/index.tsx";
1370
+ var cloudflareWorkerAssetLimit = 25 * 1024 * 1024;
1045
1371
  function riebeckiteSsg(options = {}) {
1046
1372
  const virtualId = "virtual:riebeckite-ssg-void-entry";
1047
1373
  const resolvedVirtualId = `\0${virtualId}`;
@@ -1049,7 +1375,7 @@ function riebeckiteSsg(options = {}) {
1049
1375
  let resolvedConfig;
1050
1376
  return {
1051
1377
  name: "riebeckite-ssg",
1052
- apply: "build",
1378
+ apply: shouldApplyRiebeckiteSsg,
1053
1379
  enforce: "post",
1054
1380
  config() {
1055
1381
  return {
@@ -1076,53 +1402,224 @@ function riebeckiteSsg(options = {}) {
1076
1402
  "Riebeckite SSG could not resolve the Vite configuration."
1077
1403
  );
1078
1404
  }
1405
+ const timingsMs = {};
1079
1406
  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
- });
1407
+ const server2 = await measure(
1408
+ timingsMs,
1409
+ "ssgSetup",
1410
+ () => createServer({
1411
+ root: config.root,
1412
+ define: config.define,
1413
+ resolve: {
1414
+ ...config.resolve,
1415
+ builtins: [...config.resolve.builtins, /^node:/]
1416
+ },
1417
+ plugins: [],
1418
+ build: { ssr: true },
1419
+ mode: config.mode,
1420
+ server: { watch: null }
1421
+ })
1422
+ );
1091
1423
  try {
1092
- const module = await server2.ssrLoadModule(entry);
1424
+ const module = await measure(
1425
+ timingsMs,
1426
+ "ssrModuleLoad",
1427
+ () => server2.ssrLoadModule(entry)
1428
+ );
1093
1429
  const app = module.default;
1094
1430
  if (!app) {
1095
1431
  throw new Error(`Failed to find a default export from ${entry}.`);
1096
1432
  }
1433
+ const outputChangeSet = await measure(
1434
+ timingsMs,
1435
+ "outputDependencyResolution",
1436
+ async () => module.content?.getOutputChangeSet()
1437
+ );
1438
+ const appFingerprint = await measure(
1439
+ timingsMs,
1440
+ "appFingerprint",
1441
+ () => computeAppFingerprint(config.root, config.configFile)
1442
+ );
1443
+ const outputCachePath = join(
1444
+ config.root,
1445
+ ".riebeckite",
1446
+ "ssg-output-cache.json"
1447
+ );
1448
+ const previousOutputCache = await loadOutputCache(
1449
+ outputCachePath,
1450
+ timingsMs,
1451
+ (message) => this.warn(message)
1452
+ );
1453
+ const siteOwnedOutputPaths = await measure(
1454
+ timingsMs,
1455
+ "siteOwnedOutputs",
1456
+ () => collectSiteOwnedOutputPaths(config.publicDir)
1457
+ );
1458
+ const siteOutputCollisions = findBuildOutputSiteCollisions(
1459
+ Object.keys(bundle),
1460
+ siteOwnedOutputPaths
1461
+ );
1462
+ if (siteOutputCollisions.length > 0) {
1463
+ this.warn(
1464
+ `Build outputs take precedence over site public assets: ${siteOutputCollisions.join(", ")}`
1465
+ );
1466
+ }
1467
+ const canUseIncremental = outputChangeSet && !outputChangeSet.fullRegenerationRequired && previousOutputCache && previousOutputCache.appFingerprint === appFingerprint && outputChangeSet.unchanged.every(
1468
+ (output) => siteOwnedOutputPaths.has(output.path) || previousOutputCache.outputs[output.path]
1469
+ );
1470
+ const reusablePaths = new Set(
1471
+ canUseIncremental ? outputChangeSet.unchanged.map((output) => output.path) : []
1472
+ );
1473
+ const shadowedOutputs = /* @__PURE__ */ new Set();
1474
+ const skippedRouteCount = canUseIncremental ? outputChangeSet.unchanged.filter(
1475
+ (output) => output.kind !== "generated"
1476
+ ).length : 0;
1097
1477
  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 });
1478
+ const nextOutputCache = {
1479
+ version: 1,
1480
+ appFingerprint,
1481
+ outputs: {}
1482
+ };
1483
+ const metrics = {
1484
+ candidateOutputCount: outputChangeSet?.candidateOutputCount ?? 0,
1485
+ affectedOutputCount: outputChangeSet?.affectedOutputCount ?? 0,
1486
+ removedOutputCount: outputChangeSet?.removedOutputCount ?? 0,
1487
+ unchangedOutputCount: outputChangeSet?.unchangedOutputCount ?? 0,
1488
+ renderedOutputCount: 0,
1489
+ emittedOutputCount: 0,
1490
+ reusedOutputCount: 0,
1491
+ deletedOutputCount: 0,
1492
+ shadowedOutputCount: 0,
1493
+ fullRegenerationRequired: !canUseIncremental,
1494
+ htmlOutputCount: 0,
1495
+ assetOutputCount: 0,
1496
+ cacheFileSizeBytes: previousOutputCache?.fileSizeBytes ?? null,
1497
+ cacheEntriesRead: previousOutputCache ? Object.keys(previousOutputCache.outputs).length : 0,
1498
+ cacheEntriesSerialized: 0,
1499
+ enumeratedRouteCount: 0,
1500
+ renderedRouteCount: 0,
1501
+ skippedRouteCount,
1502
+ timingsMs
1503
+ };
1504
+ process.env.RIEBECKITE_SSG_FULL_REGENERATION = canUseIncremental ? "" : "1";
1505
+ const outDir = resolve(config.root, config.build.outDir);
1506
+ const result = await measure(
1507
+ timingsMs,
1508
+ "toSSG",
1509
+ () => toSSG(
1510
+ app,
1511
+ {
1512
+ writeFile: async (filePath, data) => {
1513
+ const fileName = relative(outDir, filePath).replaceAll(
1514
+ "\\",
1515
+ "/"
1516
+ );
1517
+ metrics.enumeratedRouteCount += 1;
1518
+ if (siteOwnedOutputPaths.has(fileName)) {
1519
+ shadowedOutputs.add(fileName);
1520
+ return;
1521
+ }
1522
+ if (canUseIncremental && reusablePaths.has(fileName)) {
1523
+ return;
1524
+ }
1525
+ if (fileName.endsWith(".html") && typeof data === "string") {
1526
+ metrics.renderedOutputCount += 1;
1527
+ metrics.htmlOutputCount += 1;
1528
+ generatedHtml.push({ path: fileName, html: data });
1529
+ } else {
1530
+ metrics.assetOutputCount += 1;
1531
+ }
1532
+ this.emitFile({
1533
+ type: "asset",
1534
+ fileName,
1535
+ source: data
1536
+ });
1537
+ metrics.emittedOutputCount += 1;
1538
+ nextOutputCache.outputs[fileName] = encodeOutput(data);
1539
+ },
1540
+ async mkdir() {
1541
+ }
1542
+ },
1543
+ {
1544
+ dir: outDir,
1545
+ plugins: [...options.plugins ?? []],
1546
+ extensionMap: options.extensionMap ?? defaultExtensionMap
1547
+ }
1548
+ )
1549
+ );
1550
+ delete process.env.RIEBECKITE_SSG_FULL_REGENERATION;
1551
+ metrics.renderedRouteCount = result.files.length;
1552
+ if (!result.success) throw result.error;
1553
+ const emittedGeneratedOutputCount = await measure(
1554
+ timingsMs,
1555
+ "generatedOutputs",
1556
+ () => emitGeneratedOutputs(
1557
+ module,
1558
+ (asset) => {
1559
+ this.emitFile(asset);
1560
+ metrics.emittedOutputCount += 1;
1561
+ nextOutputCache.outputs[asset.fileName] = encodeOutput(
1562
+ asset.source
1563
+ );
1564
+ },
1565
+ canUseIncremental ? new Set(
1566
+ outputChangeSet.affected.filter((output) => output.kind === "generated").map((output) => output.path)
1567
+ ) : void 0,
1568
+ siteOwnedOutputPaths,
1569
+ (path10) => shadowedOutputs.add(path10)
1570
+ )
1571
+ );
1572
+ if (!outputChangeSet)
1573
+ metrics.candidateOutputCount = result.files.length + emittedGeneratedOutputCount;
1574
+ if (canUseIncremental) {
1575
+ await measure(timingsMs, "unchangedOutputReuse", async () => {
1576
+ for (const output of outputChangeSet.unchanged) {
1577
+ const path10 = output.path;
1578
+ if (siteOwnedOutputPaths.has(path10)) {
1579
+ shadowedOutputs.add(path10);
1580
+ continue;
1108
1581
  }
1582
+ const cached = previousOutputCache.outputs[path10];
1583
+ if (!cached) continue;
1109
1584
  this.emitFile({
1110
1585
  type: "asset",
1111
- fileName,
1112
- source: data
1586
+ fileName: path10,
1587
+ source: decodeOutput(cached)
1113
1588
  });
1114
- },
1115
- async mkdir() {
1589
+ metrics.emittedOutputCount += 1;
1590
+ metrics.reusedOutputCount += 1;
1591
+ nextOutputCache.outputs[path10] = cached;
1116
1592
  }
1117
- },
1118
- {
1119
- dir: config.build.outDir,
1120
- plugins: options.plugins ?? [],
1121
- extensionMap: options.extensionMap ?? defaultExtensionMap
1593
+ });
1594
+ }
1595
+ if (outputChangeSet) {
1596
+ for (const output of outputChangeSet.removed) {
1597
+ if (siteOwnedOutputPaths.has(output.path)) continue;
1598
+ await rm(join(outDir, output.path), {
1599
+ force: true
1600
+ });
1601
+ metrics.deletedOutputCount += 1;
1122
1602
  }
1123
- );
1124
- if (!result.success) throw result.error;
1125
- await emitGeneratedOutputs(module, (asset) => this.emitFile(asset));
1603
+ }
1604
+ metrics.shadowedOutputCount = shadowedOutputs.size;
1605
+ if (shadowedOutputs.size > 0) {
1606
+ this.warn(
1607
+ `Site public assets take precedence over generated outputs: ${[
1608
+ ...shadowedOutputs
1609
+ ].sort().join(", ")}`
1610
+ );
1611
+ }
1612
+ metrics.cacheEntriesSerialized = Object.keys(
1613
+ nextOutputCache.outputs
1614
+ ).length;
1615
+ try {
1616
+ await saveOutputCache(outputCachePath, nextOutputCache, timingsMs);
1617
+ } catch (error) {
1618
+ this.warn(
1619
+ `SSG output cache could not be saved; the next build regenerates every output. ${error instanceof Error ? error.message : String(error)}`
1620
+ );
1621
+ }
1622
+ await writeSsgOutputMetrics(metrics);
1126
1623
  inspectGeneratedHtmlPages(module, generatedHtml, {
1127
1624
  warn: (message) => this.warn(message),
1128
1625
  info: (message) => this.info(message)
@@ -1130,19 +1627,247 @@ function riebeckiteSsg(options = {}) {
1130
1627
  } finally {
1131
1628
  await server2.close();
1132
1629
  }
1630
+ },
1631
+ async closeBundle() {
1632
+ if (!resolvedConfig) return;
1633
+ const outDir = resolve(resolvedConfig.root, resolvedConfig.build.outDir);
1634
+ const oversizedAssets = await findOversizedAssets(outDir);
1635
+ for (const asset of oversizedAssets) {
1636
+ this.warn(
1637
+ `Generated asset ${asset.path} is ${formatBytes(asset.size)}, exceeding Cloudflare Workers' ${formatBytes(cloudflareWorkerAssetLimit)} asset limit.`
1638
+ );
1639
+ }
1133
1640
  }
1134
1641
  };
1135
1642
  }
1136
- async function emitGeneratedOutputs(module, emit) {
1643
+ async function findOversizedAssets(directory, relativeDirectory = "") {
1644
+ const entries = await readdir(directory, { withFileTypes: true }).catch(
1645
+ (error) => {
1646
+ if (isMissingDirectoryError(error)) return [];
1647
+ throw error;
1648
+ }
1649
+ );
1650
+ const nestedAssets = await Promise.all(
1651
+ entries.map(async (entry) => {
1652
+ const path10 = join(directory, entry.name);
1653
+ const relativePath = join(relativeDirectory, entry.name).replaceAll(
1654
+ "\\",
1655
+ "/"
1656
+ );
1657
+ if (entry.isDirectory()) {
1658
+ return findOversizedAssets(path10, relativePath);
1659
+ }
1660
+ if (!entry.isFile()) return [];
1661
+ const size = (await stat(path10)).size;
1662
+ return size > cloudflareWorkerAssetLimit ? [{ path: relativePath, size }] : [];
1663
+ })
1664
+ );
1665
+ return nestedAssets.flat();
1666
+ }
1667
+ function isMissingDirectoryError(error) {
1668
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
1669
+ }
1670
+ function formatBytes(bytes) {
1671
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
1672
+ }
1673
+ function shouldApplyRiebeckiteSsg(_config, env) {
1674
+ return env.command === "build" && env.mode !== "client";
1675
+ }
1676
+ function findBuildOutputSiteCollisions(buildOutputPaths, siteOwnedOutputPaths) {
1677
+ const collisions = /* @__PURE__ */ new Set();
1678
+ for (const path10 of buildOutputPaths) {
1679
+ if (siteOwnedOutputPaths.has(path10)) collisions.add(path10);
1680
+ }
1681
+ return [...collisions].sort();
1682
+ }
1683
+ async function emitGeneratedOutputs(module, emit, affectedPaths, shadowedPaths, onShadowed) {
1137
1684
  const content = module.content;
1138
- if (!content) return;
1685
+ if (!content) return 0;
1139
1686
  const manifest = await content.getManifest();
1687
+ let count = 0;
1140
1688
  for (const output of manifest.generatedOutputs ?? []) {
1689
+ if (affectedPaths && !affectedPaths.has(output.path)) continue;
1690
+ if (shadowedPaths?.has(output.path)) {
1691
+ onShadowed?.(output.path);
1692
+ continue;
1693
+ }
1141
1694
  emit({
1142
1695
  type: "asset",
1143
1696
  fileName: output.path,
1144
1697
  source: output.content
1145
1698
  });
1699
+ count += 1;
1700
+ }
1701
+ return count;
1702
+ }
1703
+ var APP_FINGERPRINT_IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
1704
+ "node_modules",
1705
+ ".riebeckite",
1706
+ "dist"
1707
+ ]);
1708
+ var APP_FINGERPRINT_CONFIG_FILES = [
1709
+ "riebeckite.config.ts",
1710
+ "riebeckite.config.mts",
1711
+ "riebeckite.config.cts",
1712
+ "riebeckite.config.js",
1713
+ "riebeckite.config.mjs",
1714
+ "riebeckite.config.cjs"
1715
+ ];
1716
+ async function computeAppFingerprint(root, configFile) {
1717
+ const files = [];
1718
+ await collectAppFiles(join(root, "app"), files);
1719
+ if (configFile) files.push(configFile);
1720
+ for (const name of APP_FINGERPRINT_CONFIG_FILES) {
1721
+ files.push(join(root, name));
1722
+ }
1723
+ files.sort();
1724
+ const hash = createHash("sha256");
1725
+ for (const file of files) {
1726
+ let content;
1727
+ try {
1728
+ content = await readFile(file);
1729
+ } catch {
1730
+ continue;
1731
+ }
1732
+ hash.update(relative(root, file).replaceAll("\\", "/"));
1733
+ hash.update("\0");
1734
+ hash.update(content);
1735
+ hash.update("\0");
1736
+ }
1737
+ return hash.digest("hex");
1738
+ }
1739
+ async function collectAppFiles(directory, files) {
1740
+ const entries = await readdir(directory, { withFileTypes: true }).catch(
1741
+ () => void 0
1742
+ );
1743
+ if (!entries) return;
1744
+ for (const entry of entries) {
1745
+ const full = join(directory, entry.name);
1746
+ if (entry.isDirectory()) {
1747
+ if (APP_FINGERPRINT_IGNORED_DIRECTORIES.has(entry.name)) continue;
1748
+ await collectAppFiles(full, files);
1749
+ } else if (entry.isFile()) {
1750
+ files.push(full);
1751
+ }
1752
+ }
1753
+ }
1754
+ async function loadOutputCache(path10, timingsMs, log = () => {
1755
+ }) {
1756
+ let raw;
1757
+ try {
1758
+ raw = await measure(
1759
+ timingsMs,
1760
+ "outputCacheRead",
1761
+ () => readFile(path10, "utf8")
1762
+ );
1763
+ } catch (error) {
1764
+ if (isNotFoundError2(error)) return void 0;
1765
+ log(
1766
+ `SSG output cache could not be read; the build regenerates every output. ${error instanceof Error ? error.message : String(error)}`
1767
+ );
1768
+ return void 0;
1769
+ }
1770
+ let parsed;
1771
+ try {
1772
+ parsed = measureSync(timingsMs, "outputCacheParse", () => JSON.parse(raw));
1773
+ } catch (error) {
1774
+ log(
1775
+ `SSG output cache is corrupted and will be rebuilt. ${error instanceof Error ? error.message : String(error)}`
1776
+ );
1777
+ return void 0;
1778
+ }
1779
+ if (!isOutputCacheState(parsed)) {
1780
+ log("SSG output cache uses an unsupported format and will be rebuilt.");
1781
+ return void 0;
1782
+ }
1783
+ return timingsMs ? { ...parsed, fileSizeBytes: Buffer.byteLength(raw) } : parsed;
1784
+ }
1785
+ function isOutputCacheState(value) {
1786
+ if (typeof value !== "object" || value === null) return false;
1787
+ const candidate = value;
1788
+ return candidate.version === 1 && (candidate.appFingerprint === void 0 || typeof candidate.appFingerprint === "string") && isOutputCacheEntries(candidate.outputs);
1789
+ }
1790
+ function isOutputCacheEntries(value) {
1791
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1792
+ return false;
1793
+ }
1794
+ return Object.values(value).every(
1795
+ (entry) => typeof entry === "object" && entry !== null && typeof entry.source === "string" && (entry.encoding === "utf8" || entry.encoding === "base64")
1796
+ );
1797
+ }
1798
+ async function saveOutputCache(path10, state, timingsMs) {
1799
+ const directory = dirname(path10);
1800
+ await mkdir(directory, { recursive: true });
1801
+ const temporaryPath = `${path10}.${randomUUID()}.tmp`;
1802
+ const serialized = measureSync(
1803
+ timingsMs,
1804
+ "outputCacheSerialize",
1805
+ () => `${JSON.stringify(state)}
1806
+ `
1807
+ );
1808
+ try {
1809
+ await measure(
1810
+ timingsMs,
1811
+ "outputCacheWrite",
1812
+ () => writeFile(temporaryPath, serialized, "utf8")
1813
+ );
1814
+ await measure(
1815
+ timingsMs,
1816
+ "outputCacheRename",
1817
+ () => replaceFile(temporaryPath, path10)
1818
+ );
1819
+ } catch (error) {
1820
+ await rm(temporaryPath, { force: true }).catch(() => void 0);
1821
+ throw error;
1822
+ }
1823
+ }
1824
+ async function replaceFile(temporaryPath, targetPath) {
1825
+ try {
1826
+ await rename2(temporaryPath, targetPath);
1827
+ } catch (error) {
1828
+ if (!isReplacementFailure(error)) throw error;
1829
+ await rm(targetPath, { force: true });
1830
+ await rename2(temporaryPath, targetPath);
1831
+ }
1832
+ }
1833
+ function isReplacementFailure(error) {
1834
+ if (typeof error !== "object" || error === null || !("code" in error)) {
1835
+ return false;
1836
+ }
1837
+ const code = String(error.code);
1838
+ return code === "EPERM" || code === "EEXIST" || code === "EACCES";
1839
+ }
1840
+ function isNotFoundError2(error) {
1841
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
1842
+ }
1843
+ function encodeOutput(source) {
1844
+ return typeof source === "string" ? { source, encoding: "utf8" } : { source: Buffer.from(source).toString("base64"), encoding: "base64" };
1845
+ }
1846
+ function decodeOutput(entry) {
1847
+ return entry.encoding === "utf8" ? entry.source : Buffer.from(entry.source, "base64");
1848
+ }
1849
+ async function writeSsgOutputMetrics(metrics) {
1850
+ const file = process.env.RIEBECKITE_SSG_METRICS_FILE;
1851
+ if (!file) return;
1852
+ await writeFile(file, `${JSON.stringify(metrics)}
1853
+ `, "utf8");
1854
+ }
1855
+ async function measure(timingsMs, name, fn) {
1856
+ const start = performance.now();
1857
+ try {
1858
+ return await fn();
1859
+ } finally {
1860
+ if (timingsMs)
1861
+ timingsMs[name] = (timingsMs[name] ?? 0) + performance.now() - start;
1862
+ }
1863
+ }
1864
+ function measureSync(timingsMs, name, fn) {
1865
+ const start = performance.now();
1866
+ try {
1867
+ return fn();
1868
+ } finally {
1869
+ if (timingsMs)
1870
+ timingsMs[name] = (timingsMs[name] ?? 0) + performance.now() - start;
1146
1871
  }
1147
1872
  }
1148
1873
  function inspectGeneratedHtmlPages(module, pages, log) {
@@ -1192,8 +1917,8 @@ function createRiebeckiteSsg(options = {}) {
1192
1917
  }
1193
1918
 
1194
1919
  // src/vite_runner.ts
1195
- import fs5 from "node:fs/promises";
1196
- import path5 from "node:path";
1920
+ import fs6 from "node:fs/promises";
1921
+ import path7 from "node:path";
1197
1922
  import { build, createServer as createServer2 } from "vite";
1198
1923
  var viteConfigFileNames = [
1199
1924
  "vite.config.ts",
@@ -1219,8 +1944,8 @@ var HonoxApplicationRootError = class extends Error {
1219
1944
  }
1220
1945
  };
1221
1946
  async function resolveHonoxApplication(options) {
1222
- const configRoot = path5.resolve(options.configRoot);
1223
- const appRoot = options.appRoot ? path5.resolve(options.appRoot) : await resolveHonoxApplicationRoot(configRoot);
1947
+ const configRoot = path7.resolve(options.configRoot);
1948
+ const appRoot = options.appRoot ? path7.resolve(options.appRoot) : await resolveHonoxApplicationRoot(configRoot);
1224
1949
  const workspaceRoot = options.workspaceRoot ?? await findWorkspaceRoot(configRoot);
1225
1950
  const rawConfig = await loadRiebeckiteConfig({
1226
1951
  configRoot,
@@ -1237,37 +1962,53 @@ async function resolveHonoxApplication(options) {
1237
1962
  }
1238
1963
  async function findWorkspaceRoot(configRoot) {
1239
1964
  try {
1240
- const workspaceConfig = path5.join(configRoot, "pnpm-workspace.yaml");
1241
- return (await fs5.stat(workspaceConfig)).isFile() ? configRoot : void 0;
1965
+ const workspaceConfig = path7.join(configRoot, "pnpm-workspace.yaml");
1966
+ return (await fs6.stat(workspaceConfig)).isFile() ? configRoot : void 0;
1242
1967
  } catch {
1243
1968
  return void 0;
1244
1969
  }
1245
1970
  }
1246
1971
  async function startHonoxDevServer(options) {
1247
- const server2 = await createServer2({ root: options.root });
1248
- await server2.listen();
1249
- server2.printUrls();
1250
- await waitForShutdown(server2);
1972
+ const root = path7.resolve(options.root);
1973
+ await withWorkingDirectory(root, async () => {
1974
+ const server2 = await createServer2({ root });
1975
+ await server2.listen();
1976
+ server2.printUrls();
1977
+ await waitForShutdown(server2);
1978
+ });
1251
1979
  }
1252
1980
  async function buildHonoxApplication(options) {
1253
- await runTraced(
1254
- options.tracer,
1255
- "integration.honox.client_build",
1256
- () => build({ root: options.root, mode: "client" })
1257
- );
1258
- await runTraced(
1259
- options.tracer,
1260
- "integration.honox.server_build",
1261
- () => build({ root: options.root })
1262
- );
1981
+ const root = path7.resolve(options.root);
1982
+ await withWorkingDirectory(root, async () => {
1983
+ await runTraced(
1984
+ options.tracer,
1985
+ "integration.honox.client_build",
1986
+ () => build({ root, mode: "client" })
1987
+ );
1988
+ await runTraced(
1989
+ options.tracer,
1990
+ "integration.honox.server_build",
1991
+ () => build({ root })
1992
+ );
1993
+ });
1994
+ }
1995
+ async function withWorkingDirectory(root, task) {
1996
+ const previousDirectory = process.cwd();
1997
+ if (previousDirectory === root) return await task();
1998
+ process.chdir(root);
1999
+ try {
2000
+ return await task();
2001
+ } finally {
2002
+ process.chdir(previousDirectory);
2003
+ }
1263
2004
  }
1264
2005
  async function runTraced(tracer, name, buildTask) {
1265
2006
  return tracer ? await tracer.span(name, {}, buildTask) : await buildTask();
1266
2007
  }
1267
2008
  async function waitForShutdown(server2) {
1268
- await new Promise((resolve) => {
2009
+ await new Promise((resolve2) => {
1269
2010
  const close = () => {
1270
- void server2.close().finally(resolve);
2011
+ void server2.close().finally(resolve2);
1271
2012
  };
1272
2013
  process.once("SIGINT", close);
1273
2014
  process.once("SIGTERM", close);
@@ -1280,12 +2021,12 @@ async function findViteApplicationRoots(root) {
1280
2021
  const directory = directories.pop();
1281
2022
  if (!directory) continue;
1282
2023
  if (await hasViteConfig(directory)) applicationRoots.push(directory);
1283
- const entries = await fs5.readdir(directory, { withFileTypes: true });
2024
+ const entries = await fs6.readdir(directory, { withFileTypes: true });
1284
2025
  for (const entry of entries) {
1285
2026
  if (!entry.isDirectory() || entry.name === "node_modules" || entry.name === ".git" || entry.name === "tests") {
1286
2027
  continue;
1287
2028
  }
1288
- directories.push(path5.join(directory, entry.name));
2029
+ directories.push(path7.join(directory, entry.name));
1289
2030
  }
1290
2031
  }
1291
2032
  return applicationRoots;
@@ -1293,7 +2034,7 @@ async function findViteApplicationRoots(root) {
1293
2034
  async function hasViteConfig(directory) {
1294
2035
  for (const fileName of viteConfigFileNames) {
1295
2036
  try {
1296
- if ((await fs5.stat(path5.join(directory, fileName))).isFile()) return true;
2037
+ if ((await fs6.stat(path7.join(directory, fileName))).isFile()) return true;
1297
2038
  } catch {
1298
2039
  }
1299
2040
  }
@@ -1313,6 +2054,7 @@ var defaultSsrExternals = [
1313
2054
  ];
1314
2055
  function riebeckite(options = {}) {
1315
2056
  let resolvedConfig;
2057
+ let contentWatchRoots;
1316
2058
  const getConfig = () => {
1317
2059
  if (!resolvedConfig) {
1318
2060
  throw new Error("Riebeckite config has not been loaded yet.");
@@ -1323,7 +2065,7 @@ function riebeckite(options = {}) {
1323
2065
  {
1324
2066
  name: "riebeckite-host-integration",
1325
2067
  async config(userConfig) {
1326
- const root = userConfig.root ? path6.resolve(userConfig.root) : process.cwd();
2068
+ const root = userConfig.root ? path8.resolve(userConfig.root) : process.cwd();
1327
2069
  const appRoot = options.appRoot ?? root;
1328
2070
  const application = await resolveHonoxApplication({
1329
2071
  configRoot: options.configRoot ?? appRoot,
@@ -1332,9 +2074,10 @@ function riebeckite(options = {}) {
1332
2074
  workspaceRoot: options.workspaceRoot
1333
2075
  });
1334
2076
  resolvedConfig = application.config;
2077
+ contentWatchRoots = application;
1335
2078
  writeRiebeckiteAssetEntries(resolvedConfig, {
1336
- pluginStyles: path6.join(appRoot, "app/.riebeckite/plugin-styles.css"),
1337
- themeStyles: path6.join(appRoot, "app/.riebeckite/theme-styles.css")
2079
+ pluginStyles: path8.join(appRoot, "app/.riebeckite/plugin-styles.css"),
2080
+ themeStyles: path8.join(appRoot, "app/.riebeckite/theme-styles.css")
1338
2081
  });
1339
2082
  return {
1340
2083
  define: {
@@ -1351,9 +2094,24 @@ function riebeckite(options = {}) {
1351
2094
  };
1352
2095
  }
1353
2096
  },
1354
- riebeckiteClientModule(getConfig)
2097
+ riebeckiteClientModule(getConfig),
2098
+ riebeckiteContentAssets({
2099
+ appRoot: () => requireContentWatchRoots(contentWatchRoots).appRoot,
2100
+ contentRoot: () => requireContentWatchRoots(contentWatchRoots).contentRoot
2101
+ }),
2102
+ riebeckiteContentWatch({
2103
+ appRoot: () => requireContentWatchRoots(contentWatchRoots).appRoot,
2104
+ contentRoot: () => requireContentWatchRoots(contentWatchRoots).contentRoot,
2105
+ exclude: () => getConfig().content.exclude ?? []
2106
+ })
1355
2107
  ];
1356
2108
  }
2109
+ function requireContentWatchRoots(roots) {
2110
+ if (!roots) {
2111
+ throw new Error("Riebeckite application has not been resolved yet.");
2112
+ }
2113
+ return roots;
2114
+ }
1357
2115
  function riebeckiteVite(options = {}) {
1358
2116
  const { ssg, ssrExternals, ...integration } = options;
1359
2117
  return [
@@ -1380,6 +2138,17 @@ function riebeckiteSsrExternals(externals) {
1380
2138
  }
1381
2139
 
1382
2140
  // src/scaffold/app-templates.ts
2141
+ function hasScaffoldPlugin(preset, packageName) {
2142
+ return preset.plugins.some((plugin) => plugin.package === packageName);
2143
+ }
2144
+ function articleTitleHelper() {
2145
+ return `
2146
+ function getArticleTitle(slug: string, title: unknown): string {
2147
+ if (typeof title === "string" && title.trim().length > 0) return title;
2148
+ return slug.split("/").at(-1) ?? slug;
2149
+ }
2150
+ `;
2151
+ }
1383
2152
  function server() {
1384
2153
  return `import { mountRiebeckiteEndpoints } from "@riebeckite/honox/server";
1385
2154
  import { createApp } from "honox/server";
@@ -1504,18 +2273,112 @@ function style(preset) {
1504
2273
  " justify-content: space-between;",
1505
2274
  " gap: 1rem;",
1506
2275
  " padding: 1rem;",
1507
- " border-bottom: 1px solid #d1d5db;",
2276
+ " border-bottom: 1px solid var(--rb-color-border);",
1508
2277
  "}",
1509
2278
  "",
1510
2279
  ".site-header__home {",
1511
2280
  " display: inline-flex;",
1512
2281
  " align-items: center;",
1513
2282
  " gap: 0.5rem;",
1514
- " color: #111827;",
2283
+ " color: var(--rb-color-ink);",
1515
2284
  " font-weight: 700;",
1516
2285
  " text-decoration: none;",
1517
2286
  "}",
1518
2287
  "",
2288
+ ".rb-nav {",
2289
+ " color: var(--rb-color-ink);",
2290
+ "}",
2291
+ "",
2292
+ ".rb-nav__list {",
2293
+ " display: flex;",
2294
+ " align-items: center;",
2295
+ " gap: 0.75rem;",
2296
+ " margin: 0;",
2297
+ " padding: 0;",
2298
+ " list-style: none;",
2299
+ "}",
2300
+ "",
2301
+ ".rb-nav__item {",
2302
+ " position: relative;",
2303
+ "}",
2304
+ "",
2305
+ ".rb-nav__link {",
2306
+ " color: inherit;",
2307
+ " text-decoration: none;",
2308
+ "}",
2309
+ "",
2310
+ ".rb-nav__link:hover,",
2311
+ ".rb-nav__link:focus-visible,",
2312
+ ".rb-nav__link--active {",
2313
+ " color: var(--rb-color-accent);",
2314
+ "}",
2315
+ "",
2316
+ ".rb-nav__link:focus-visible,",
2317
+ ".rb-nav__toggle:focus-visible {",
2318
+ " outline: 2px solid var(--rb-color-accent);",
2319
+ " outline-offset: 2px;",
2320
+ "}",
2321
+ "",
2322
+ ".rb-nav__children {",
2323
+ " display: none;",
2324
+ " position: absolute;",
2325
+ " top: 100%;",
2326
+ " left: 0;",
2327
+ " min-width: max-content;",
2328
+ " padding: 0.5rem;",
2329
+ " background: var(--rb-color-surface);",
2330
+ " border: 1px solid var(--rb-color-border);",
2331
+ "}",
2332
+ "",
2333
+ ".rb-nav__item:hover > .rb-nav__children,",
2334
+ ".rb-nav__item:focus-within > .rb-nav__children {",
2335
+ " display: flex;",
2336
+ " flex-direction: column;",
2337
+ " align-items: stretch;",
2338
+ "}",
2339
+ "",
2340
+ ".rb-nav__mobile {",
2341
+ " display: none;",
2342
+ "}",
2343
+ "",
2344
+ ".rb-site-footer {",
2345
+ " width: min(100% - 2rem, 48rem);",
2346
+ " margin: 1rem auto 0;",
2347
+ " padding-block: 1rem;",
2348
+ " border-top: 1px solid var(--rb-color-border);",
2349
+ "}",
2350
+ "",
2351
+ "@media (max-width: 48rem) {",
2352
+ " .rb-site-header {",
2353
+ " flex-wrap: wrap;",
2354
+ " }",
2355
+ "",
2356
+ " .rb-site-header > .rb-nav {",
2357
+ " display: none;",
2358
+ " }",
2359
+ "",
2360
+ " .rb-nav__mobile {",
2361
+ " display: block;",
2362
+ " }",
2363
+ "",
2364
+ " .rb-nav__mobile .rb-nav__list {",
2365
+ " flex-direction: column;",
2366
+ " align-items: flex-start;",
2367
+ " padding-top: 0.75rem;",
2368
+ " }",
2369
+ "",
2370
+ " .rb-nav__mobile .rb-nav__children {",
2371
+ " display: flex;",
2372
+ " position: static;",
2373
+ " flex-direction: column;",
2374
+ " align-items: flex-start;",
2375
+ " gap: 0.5rem;",
2376
+ " margin-top: 0.5rem;",
2377
+ " background: transparent;",
2378
+ " border: 0;",
2379
+ " }",
2380
+ "}",
2381
+ "",
1519
2382
  ".site-header__logo {",
1520
2383
  " width: 1.75rem;",
1521
2384
  " height: 1.75rem;",
@@ -1532,6 +2395,56 @@ function style(preset) {
1532
2395
  " padding: 2rem 1rem;",
1533
2396
  " margin: 0 auto;",
1534
2397
  "}",
2398
+ "",
2399
+ ".article-shell__layout {",
2400
+ " display: grid;",
2401
+ " grid-template-columns: minmax(0, 1fr);",
2402
+ " justify-content: center;",
2403
+ " gap: var(--rb-layout-gap, 2rem);",
2404
+ "}",
2405
+ "",
2406
+ ".site-article__aside {",
2407
+ " display: none;",
2408
+ "}",
2409
+ "",
2410
+ ".site-article__footer {",
2411
+ " min-width: 0;",
2412
+ "}",
2413
+ "",
2414
+ "@media (min-width: 88rem) {",
2415
+ ' :is(:root, .rb-theme-root)[data-article-layout="article"] .article-shell__layout {',
2416
+ " grid-template-columns: minmax(0, var(--rb-layout-article-max, 48rem));",
2417
+ " position: relative;",
2418
+ " }",
2419
+ "",
2420
+ ' :is(:root, .rb-theme-root)[data-article-layout="article"] .article-shell__body {',
2421
+ " grid-column: 1;",
2422
+ " grid-row: 1;",
2423
+ " }",
2424
+ "",
2425
+ ' :is(:root, .rb-theme-root)[data-article-layout="article"] .site-article__footer {',
2426
+ " grid-column: 1;",
2427
+ " }",
2428
+ "",
2429
+ ' :is(:root, .rb-theme-root)[data-article-layout="article"] .site-article__aside {',
2430
+ " display: block;",
2431
+ " grid-column: 1;",
2432
+ " grid-row: 1;",
2433
+ " position: sticky;",
2434
+ " top: 4rem;",
2435
+ " align-self: start;",
2436
+ " width: var(--rb-layout-sidebar, 14rem);",
2437
+ " max-height: calc(100dvh - 8rem);",
2438
+ " margin-inline-start: calc(",
2439
+ " -1 * var(--rb-layout-sidebar, 14rem) -",
2440
+ " var(--rb-layout-gap, 2rem)",
2441
+ " );",
2442
+ " overflow: auto;",
2443
+ " padding-inline-end: 1rem;",
2444
+ " border-inline-end: 1px solid",
2445
+ " color-mix(in oklab, currentColor 18%, transparent);",
2446
+ " }",
2447
+ "}",
1535
2448
  ""
1536
2449
  );
1537
2450
  }
@@ -1548,13 +2461,16 @@ function style(preset) {
1548
2461
  return [...imports, ...blocks].join("\n");
1549
2462
  }
1550
2463
  function siteHeader() {
1551
- return `import { ColorModeToggle } from "@riebeckite/plugin-color-mode";
2464
+ return `import type { NavigationItem } from "@riebeckite/core";
2465
+ import { ColorModeToggle } from "@riebeckite/plugin-color-mode";
1552
2466
  import { config } from "../config";
1553
2467
 
1554
- export function SiteHeader() {
2468
+ export function SiteHeader({ path }: { path: string }) {
2469
+ const navigation = (config.navigation?.header ?? []).filter((item) => item.href !== "/");
2470
+
1555
2471
  return (
1556
- <header class="site-header">
1557
- <a href="/" class="site-header__home">
2472
+ <header class="site-header rb-site-header">
2473
+ <a href="/" class="site-header__home rb-site-header__home">
1558
2474
  <img
1559
2475
  src="/riebeckite-logo.png"
1560
2476
  alt=""
@@ -1564,10 +2480,85 @@ export function SiteHeader() {
1564
2480
  />
1565
2481
  {config.site.title}
1566
2482
  </a>
2483
+ {navigation.length > 0 ? (
2484
+ <>
2485
+ <SiteNavigation items={navigation} path={path} />
2486
+ <details class="site-navigation__mobile rb-nav__mobile">
2487
+ <summary class="rb-nav__toggle">Menu</summary>
2488
+ <SiteNavigation items={navigation} path={path} />
2489
+ </details>
2490
+ </>
2491
+ ) : null}
1567
2492
  <ColorModeToggle />
1568
2493
  </header>
1569
2494
  );
1570
2495
  }
2496
+
2497
+ export function SiteFooter({ path }: { path: string }) {
2498
+ const footerNavigation = config.navigation?.footer ?? [];
2499
+ if (footerNavigation.length === 0) return null;
2500
+
2501
+ return (
2502
+ <footer class="site-footer rb-site-footer">
2503
+ <SiteNavigation items={footerNavigation} path={path} />
2504
+ </footer>
2505
+ );
2506
+ }
2507
+
2508
+ function SiteNavigation({
2509
+ items,
2510
+ path,
2511
+ }: {
2512
+ items: readonly NavigationItem[];
2513
+ path: string;
2514
+ }) {
2515
+ return (
2516
+ <nav class="site-navigation rb-nav" aria-label="Site navigation">
2517
+ <NavigationItems items={items} path={path} />
2518
+ </nav>
2519
+ );
2520
+ }
2521
+
2522
+ function NavigationItems({
2523
+ items,
2524
+ path,
2525
+ isChildList = false,
2526
+ }: {
2527
+ items: readonly NavigationItem[];
2528
+ path: string;
2529
+ isChildList?: boolean;
2530
+ }) {
2531
+ return (
2532
+ <ul class={isChildList ? "site-navigation__list rb-nav__list rb-nav__children" : "site-navigation__list rb-nav__list"}>
2533
+ {items.map((item) => {
2534
+ const active = isActive(item.href, path);
2535
+ return (
2536
+ <li class="site-navigation__item rb-nav__item">
2537
+ <a
2538
+ href={item.href}
2539
+ class={active ? "site-navigation__link rb-nav__link rb-nav__link--active is-active" : "site-navigation__link rb-nav__link"}
2540
+ aria-current={active ? "page" : undefined}
2541
+ target={item.external ? "_blank" : undefined}
2542
+ rel={item.external ? "noreferrer" : undefined}
2543
+ >
2544
+ {item.label}
2545
+ </a>
2546
+ {item.children && item.children.length > 0 ? (
2547
+ <NavigationItems items={item.children} path={path} isChildList />
2548
+ ) : null}
2549
+ </li>
2550
+ );
2551
+ })}
2552
+ </ul>
2553
+ );
2554
+ }
2555
+
2556
+ function isActive(href: string, path: string): boolean {
2557
+ if (!href.startsWith("/")) return false;
2558
+ const target = href.replace(/\\/+$/, "") || "/";
2559
+ const current = path.replace(/^\\/[a-z]{2}(?:-[A-Z]{2})?(?=\\/|$)/, "").replace(/\\/+$/, "") || "/";
2560
+ return target === "/" ? current === target : current === target || current.startsWith(\`\${target}/\`);
2561
+ }
1571
2562
  `;
1572
2563
  }
1573
2564
  function article() {
@@ -1576,23 +2567,81 @@ import { Article, ArticleContent, ArticleLayout } from "@riebeckite/honox/ui";
1576
2567
 
1577
2568
  export function SiteArticle({
1578
2569
  post,
1579
- afterMeta,
2570
+ propertiesHtml,
2571
+ afterHeaderHtml,
2572
+ afterMetaHtml,
2573
+ beforeContentHtml,
2574
+ afterContentHtml,
2575
+ asideHtml,
2576
+ footerHtml,
2577
+ asideContent,
2578
+ afterContent,
2579
+ footerContent,
1580
2580
  }: {
1581
2581
  post: PostContent;
1582
- afterMeta?: string;
2582
+ propertiesHtml?: string;
2583
+ afterHeaderHtml?: string;
2584
+ afterMetaHtml?: string;
2585
+ beforeContentHtml?: string;
2586
+ afterContentHtml?: string;
2587
+ asideHtml?: string;
2588
+ footerHtml?: string;
2589
+ asideContent?: unknown;
2590
+ afterContent?: unknown;
2591
+ footerContent?: unknown;
1583
2592
  }) {
1584
2593
  return (
1585
2594
  <Article class="site-article">
1586
2595
  <ArticleLayout>
2596
+ {asideContent}
2597
+ {asideHtml ? (
2598
+ <div
2599
+ class="site-article__aside"
2600
+ dangerouslySetInnerHTML={{ __html: asideHtml }}
2601
+ />
2602
+ ) : null}
1587
2603
  <ArticleContent>
1588
- {afterMeta ? (
2604
+ {afterHeaderHtml ? (
2605
+ <div
2606
+ class="site-article__after-header"
2607
+ dangerouslySetInnerHTML={{ __html: afterHeaderHtml }}
2608
+ />
2609
+ ) : null}
2610
+ {propertiesHtml ? (
2611
+ <div
2612
+ class="article-properties"
2613
+ dangerouslySetInnerHTML={{ __html: propertiesHtml }}
2614
+ />
2615
+ ) : null}
2616
+ {afterMetaHtml ? (
1589
2617
  <div
1590
2618
  class="site-article__after-meta"
1591
- dangerouslySetInnerHTML={{ __html: afterMeta }}
2619
+ dangerouslySetInnerHTML={{ __html: afterMetaHtml }}
2620
+ />
2621
+ ) : null}
2622
+ {beforeContentHtml ? (
2623
+ <div
2624
+ class="site-article__before-content"
2625
+ dangerouslySetInnerHTML={{ __html: beforeContentHtml }}
1592
2626
  />
1593
2627
  ) : null}
1594
2628
  <div dangerouslySetInnerHTML={{ __html: post.html ?? "" }} />
2629
+ {afterContent}
2630
+ {afterContentHtml ? (
2631
+ <div
2632
+ class="site-article__after-content"
2633
+ dangerouslySetInnerHTML={{ __html: afterContentHtml }}
2634
+ />
2635
+ ) : null}
1595
2636
  </ArticleContent>
2637
+ {footerHtml || footerContent ? (
2638
+ <div class="site-article__footer">
2639
+ {footerHtml ? (
2640
+ <div dangerouslySetInnerHTML={{ __html: footerHtml }} />
2641
+ ) : null}
2642
+ {footerContent}
2643
+ </div>
2644
+ ) : null}
1596
2645
  </ArticleLayout>
1597
2646
  </Article>
1598
2647
  );
@@ -1605,11 +2654,13 @@ function renderer(preset) {
1605
2654
  const hasColorMode = preset.plugins.some(
1606
2655
  (plugin) => plugin.package === "@riebeckite/plugin-color-mode"
1607
2656
  );
2657
+ const hasSearch = hasScaffoldPlugin(preset, "@riebeckite/plugin-search");
1608
2658
  return `import type { PluginHeadTag } from "@riebeckite/core";
1609
2659
  import { jsxRenderer } from "hono/jsx-renderer";
1610
2660
  import { Link, Script } from "honox/server";
1611
2661
  ${hasColorMode ? `import { ColorModeScript } from "@riebeckite/plugin-color-mode";
1612
- ` : ""}${hasHeader ? `import { SiteHeader } from "../components/site-header";
2662
+ ` : ""}${hasSearch ? `import { SearchBar } from "@riebeckite/plugin-search";
2663
+ ` : ""}${hasHeader ? `import { SiteFooter, SiteHeader } from "../components/site-header";
1613
2664
  ` : ""}import { config } from "../config";
1614
2665
 
1615
2666
  function themeAttributes() {
@@ -1646,10 +2697,12 @@ ${hasColorMode ? ` <ColorModeScript />
1646
2697
  {headTags.map(renderHeadTag)}
1647
2698
  <Script src="/app/client.ts" async />
1648
2699
  </head>
1649
- <body class="riebeckite-page">
1650
- ${hasHeader ? ` <SiteHeader />
2700
+ <body class="riebeckite-page rb-site">
2701
+ ${hasHeader ? ` <SiteHeader path={c.req.path} />
2702
+ ` : ""}${hasSearch ? ` <SearchBar />
1651
2703
  ` : ""} {children}
1652
- </body>
2704
+ ${hasHeader ? ` <SiteFooter path={c.req.path} />
2705
+ ` : ""} </body>
1653
2706
  </html>
1654
2707
  );
1655
2708
  });
@@ -1689,18 +2742,103 @@ export default jsxRenderer(({ children }, c) => (
1689
2742
  <Link href="/app/style.css" rel="stylesheet" />
1690
2743
  <Script src="/app/client.ts" async />
1691
2744
  </head>
1692
- <body class="riebeckite-page">{children}</body>
2745
+ <body class="riebeckite-page rb-site">{children}</body>
1693
2746
  </html>
1694
2747
  ));
1695
2748
  `;
1696
2749
  }
1697
2750
  function indexRoute(preset) {
1698
2751
  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";
2752
+ const hasToc = hasScaffoldPlugin(preset, "@riebeckite/plugin-toc");
2753
+ const hasBacklinks = hasScaffoldPlugin(
2754
+ preset,
2755
+ "@riebeckite/plugin-backlinks"
2756
+ );
2757
+ const hasRecentPosts = hasScaffoldPlugin(
2758
+ preset,
2759
+ "@riebeckite/plugin-recent-posts"
2760
+ );
2761
+ const hasDailyNotes = hasScaffoldPlugin(
2762
+ preset,
2763
+ "@riebeckite/plugin-daily-notes"
2764
+ );
2765
+ const needsTitle = hasBacklinks || hasRecentPosts;
2766
+ const needsConfig = hasBacklinks || hasRecentPosts || hasDailyNotes;
2767
+ const importLines = [
2768
+ 'import { createRoute } from "honox/factory";'
2769
+ ];
2770
+ if (hasBacklinks) {
2771
+ importLines.push(
2772
+ 'import { Backlinks, getPublishedBacklinks } from "@riebeckite/plugin-backlinks";'
2773
+ );
2774
+ }
2775
+ if (hasDailyNotes) {
2776
+ importLines.push(
2777
+ 'import { DailyNotes, getDailyNotes } from "@riebeckite/plugin-daily-notes";'
2778
+ );
2779
+ }
2780
+ if (hasRecentPosts) {
2781
+ importLines.push(
2782
+ 'import { RecentPosts, getRecentPosts } from "@riebeckite/plugin-recent-posts";'
2783
+ );
2784
+ }
2785
+ if (hasToc) {
2786
+ importLines.push(
2787
+ 'import { extractTableOfContents, TableOfContents } from "@riebeckite/plugin-toc";'
2788
+ );
2789
+ }
2790
+ importLines.push('import { SiteArticle } from "../components/article";');
2791
+ if (needsConfig) importLines.push('import { config } from "../config";');
2792
+ importLines.push('import { content } from "../content";');
2793
+ const dataLines = [];
2794
+ if (hasBacklinks) {
2795
+ dataLines.push(
2796
+ ' const backlinks = getPublishedBacklinks({ manifest, config, slug: "index", resolveTitle: getArticleTitle });'
2797
+ );
2798
+ }
2799
+ if (hasRecentPosts) {
2800
+ dataLines.push(
2801
+ " const recentPosts = await getRecentPosts({ posts: manifest.discoverableEntries, config, getProcessedContent: (slug) => content.getProcessedContent(slug), resolveTitle: getArticleTitle });"
2802
+ );
2803
+ }
2804
+ if (hasDailyNotes) {
2805
+ dataLines.push(" const dailyNotes = getDailyNotes({ manifest, config });");
2806
+ }
2807
+ if (hasToc) {
2808
+ dataLines.push(
2809
+ ' const tableOfContents = extractTableOfContents(post.html ?? "");'
2810
+ );
2811
+ }
2812
+ const propLines = [
2813
+ " propertiesHtml={indexEntry?.bodySlots?.properties}",
2814
+ ' afterHeaderHtml={indexEntry?.bodySlots?.["article.after-header"]}',
2815
+ ' afterMetaHtml={indexEntry?.bodySlots?.["article.after-meta"]}',
2816
+ ' beforeContentHtml={indexEntry?.bodySlots?.["article.before-content"]}',
2817
+ ' afterContentHtml={indexEntry?.bodySlots?.["article.after-content"]}',
2818
+ ' asideHtml={indexEntry?.bodySlots?.["article.aside"]}',
2819
+ ' footerHtml={indexEntry?.bodySlots?.["article.footer"]}'
2820
+ ];
2821
+ if (hasToc) {
2822
+ propLines.push(
2823
+ ' asideContent={<TableOfContents className="table-of-contents--desktop" items={tableOfContents} />}'
2824
+ );
2825
+ }
2826
+ const afterChildren = [];
2827
+ if (hasRecentPosts) afterChildren.push("<RecentPosts posts={recentPosts} />");
2828
+ if (hasDailyNotes) afterChildren.push("<DailyNotes notes={dailyNotes} />");
2829
+ if (afterChildren.length > 0) {
2830
+ propLines.push(` afterContent={<>${afterChildren.join("")}</>}`);
2831
+ }
2832
+ if (hasBacklinks) {
2833
+ propLines.push(
2834
+ " footerContent={<Backlinks backlinks={backlinks} />}"
2835
+ );
2836
+ }
2837
+ const titleHelper = needsTitle ? articleTitleHelper() : "";
2838
+ const dataBlock = dataLines.length > 0 ? `
2839
+ ${dataLines.join("\n")}
2840
+ ` : "";
2841
+ return `${importLines.join("\n")}
1704
2842
 
1705
2843
  export default createRoute(async (c) => {
1706
2844
  const manifest = await content.getManifest();
@@ -1710,10 +2848,10 @@ export default createRoute(async (c) => {
1710
2848
  }
1711
2849
 
1712
2850
  const post = await content.getProcessedContent("index");
1713
- if (!isPublished(config, post.frontmatter)) {
2851
+ if (indexEntry?.publishing?.routable === false) {
1714
2852
  return c.notFound();
1715
2853
  }
1716
-
2854
+ ${dataBlock}
1717
2855
  if (indexEntry) {
1718
2856
  c.set("htmlLanguage", indexEntry.publicLocation.metadata?.["l10n.lang"]);
1719
2857
  c.set("headTags", indexEntry.headTags ?? []);
@@ -1722,11 +2860,10 @@ export default createRoute(async (c) => {
1722
2860
  return c.render(
1723
2861
  <SiteArticle
1724
2862
  post={post}
1725
- afterMeta={indexEntry?.bodySlots?.["article.after-meta"]}
2863
+ ${propLines.join("\n")}
1726
2864
  />,
1727
2865
  );
1728
- });
1729
- `;
2866
+ });${titleHelper}`;
1730
2867
  }
1731
2868
  function staticIndexRoute() {
1732
2869
  return `import { createRoute } from "honox/factory";
@@ -1742,23 +2879,98 @@ export default createRoute((c) =>
1742
2879
  );
1743
2880
  `;
1744
2881
  }
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";
2882
+ function slugRoute(preset) {
2883
+ const hasToc = hasScaffoldPlugin(preset, "@riebeckite/plugin-toc");
2884
+ const hasBacklinks = hasScaffoldPlugin(
2885
+ preset,
2886
+ "@riebeckite/plugin-backlinks"
2887
+ );
2888
+ const hasLocalGraph = hasScaffoldPlugin(
2889
+ preset,
2890
+ "@riebeckite/plugin-local-graph"
2891
+ );
2892
+ const needsTitle = hasBacklinks || hasLocalGraph;
2893
+ const needsConfig = hasBacklinks || hasLocalGraph;
2894
+ const needsManifest = hasBacklinks || hasLocalGraph;
2895
+ const importLines = [
2896
+ "import {",
2897
+ " contentRouteSsgParams,",
2898
+ " pluginPageSsgParams,",
2899
+ " resolveRiebeckiteRoute,",
2900
+ '} from "@riebeckite/honox/server";'
2901
+ ];
2902
+ if (hasBacklinks) {
2903
+ importLines.push(
2904
+ 'import { Backlinks, getPublishedBacklinks } from "@riebeckite/plugin-backlinks";'
2905
+ );
2906
+ }
2907
+ if (hasLocalGraph) {
2908
+ importLines.push(
2909
+ 'import { LocalGraph, getLocalGraph } from "@riebeckite/plugin-local-graph";'
2910
+ );
2911
+ }
2912
+ if (hasToc) {
2913
+ importLines.push(
2914
+ 'import { extractTableOfContents, TableOfContents } from "@riebeckite/plugin-toc";'
2915
+ );
2916
+ }
2917
+ importLines.push('import { createRoute } from "honox/factory";');
2918
+ importLines.push('import { SiteArticle } from "../components/article";');
2919
+ if (needsConfig) importLines.push('import { config } from "../config";');
2920
+ importLines.push('import { content } from "../content";');
2921
+ const dataLines = [];
2922
+ if (needsManifest) {
2923
+ dataLines.push(" const manifest = await content.getManifest();");
2924
+ }
2925
+ if (hasBacklinks) {
2926
+ dataLines.push(
2927
+ " const backlinks = getPublishedBacklinks({ manifest, config, slug: route.entry.slug, resolveTitle: getArticleTitle });"
2928
+ );
2929
+ }
2930
+ if (hasLocalGraph) {
2931
+ dataLines.push(
2932
+ " const localGraph = getLocalGraph({ manifest, config, slug: route.entry.slug, resolveTitle: getArticleTitle });"
2933
+ );
2934
+ }
2935
+ if (hasToc) {
2936
+ dataLines.push(
2937
+ ' const tableOfContents = extractTableOfContents(post.html ?? "");'
2938
+ );
2939
+ }
2940
+ const propLines = [
2941
+ " propertiesHtml={route.entry.bodySlots?.properties}",
2942
+ ' afterHeaderHtml={route.entry.bodySlots?.["article.after-header"]}',
2943
+ ' afterMetaHtml={route.entry.bodySlots?.["article.after-meta"]}',
2944
+ ' beforeContentHtml={route.entry.bodySlots?.["article.before-content"]}',
2945
+ ' afterContentHtml={route.entry.bodySlots?.["article.after-content"]}',
2946
+ ' asideHtml={route.entry.bodySlots?.["article.aside"]}',
2947
+ ' footerHtml={route.entry.bodySlots?.["article.footer"]}'
2948
+ ];
2949
+ if (hasToc) {
2950
+ propLines.push(
2951
+ ' asideContent={<TableOfContents className="table-of-contents--desktop" items={tableOfContents} />}'
2952
+ );
2953
+ }
2954
+ const footerChildren = [];
2955
+ if (hasLocalGraph) {
2956
+ footerChildren.push("{localGraph && <LocalGraph graph={localGraph} />}");
2957
+ }
2958
+ if (hasBacklinks) {
2959
+ footerChildren.push("<Backlinks backlinks={backlinks} />");
2960
+ }
2961
+ if (footerChildren.length > 0) {
2962
+ propLines.push(` footerContent={<>${footerChildren.join("")}</>}`);
2963
+ }
2964
+ const titleHelper = needsTitle ? articleTitleHelper() : "";
2965
+ const dataBlock = dataLines.length > 0 ? `
2966
+ ${dataLines.join("\n")}
2967
+ ` : "";
2968
+ return `${importLines.join("\n")}
1756
2969
 
1757
2970
  export default createRoute(
1758
2971
  contentRouteSsgParams("/:slug{.+}", async () => {
1759
2972
  const manifest = await content.getManifest();
1760
- const contentPaths = manifest.entries
1761
- .filter((entry) => isPublished(config, entry.frontmatter))
2973
+ const contentPaths = manifest.publicEntries
1762
2974
  .filter((entry) => entry.permalink !== "/")
1763
2975
  .map((entry) => ({ slug: entry.permalink.replace(/^\\/+/, "") }));
1764
2976
  return [...contentPaths, ...(await pluginPageSsgParams(content))];
@@ -1780,22 +2992,18 @@ export default createRoute(
1780
2992
  }
1781
2993
 
1782
2994
  const post = await content.getProcessedContent(route.entry.slug);
1783
- if (!isPublished(config, post.frontmatter)) {
1784
- return c.notFound();
1785
- }
1786
-
2995
+ ${dataBlock}
1787
2996
  c.set("htmlLanguage", route.entry.publicLocation.metadata?.["l10n.lang"]);
1788
2997
  c.set("headTags", route.entry.headTags ?? []);
1789
2998
 
1790
2999
  return c.render(
1791
3000
  <SiteArticle
1792
3001
  post={post}
1793
- afterMeta={route.entry.bodySlots?.["article.after-meta"]}
3002
+ ${propLines.join("\n")}
1794
3003
  />,
1795
3004
  );
1796
3005
  },
1797
- );
1798
- `;
3006
+ );${titleHelper}`;
1799
3007
  }
1800
3008
 
1801
3009
  // src/scaffold/localized-content.ts
@@ -2160,7 +3368,7 @@ function localizedContentFiles(variables, preset) {
2160
3368
  (language) => themesContent(language, preset)
2161
3369
  );
2162
3370
  } else if (page === "guide") {
2163
- pushEnglishOnly("guide", guideContent);
3371
+ pushEnglishOnly("guide", () => guideContent(preset));
2164
3372
  } else if (page === "examples") {
2165
3373
  pushLocalized(
2166
3374
  "examples",
@@ -2354,10 +3562,50 @@ var INDEX = {
2354
3562
  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
3563
  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
3564
  ko: "번역 페이지는 기본 파일 옆에 `<base>.<lang>.md` 규칙으로 둡니다(예: `about.ja.md`). l10n 플러그인이 `/lang/` 경로로 서빙하며 자동으로 링크합니다."
3565
+ },
3566
+ editFileNote: {
3567
+ en: "This page is `content/index.md`. Open that file, change it, and save — the browser updates while you write.",
3568
+ ja: "このページは `content/index.md` です。ファイルを開いて書き換えると、書いている途中でもブラウザの表示が更新されます。",
3569
+ "zh-CN": "这个页面来自 `content/index.md`。打开这个文件修改并保存,浏览器会随你的输入更新。",
3570
+ es: "Esta página viene de `content/index.md`. Abre ese archivo, edítalo y guarda: el navegador se actualiza mientras escribes.",
3571
+ de: "Diese Seite stammt aus `content/index.md`. Öffne die Datei, ändere sie und speichere — der Browser aktualisiert sich beim Schreiben.",
3572
+ fr: "Cette page provient de `content/index.md`. Ouvrez ce fichier, modifiez-le et enregistrez — le navigateur se met à jour pendant que vous écrivez.",
3573
+ ko: "이 페이지는 `content/index.md`에서 만들어집니다. 그 파일을 열어 수정하고 저장하면 글을 쓰는 동안 브라우저가 갱신됩니다."
3574
+ },
3575
+ linkLine: {
3576
+ en: "Link pages with a WikiLink, for example `[[guide]]`.",
3577
+ ja: "ページ同士は WikiLink でリンクできます(例:`[[guide]]`)。",
3578
+ "zh-CN": "用 WikiLink 连接页面,例如 `[[guide]]`。",
3579
+ es: "Enlaza páginas con un WikiLink, por ejemplo `[[guide]]`.",
3580
+ de: "Verbinde Seiten mit einem WikiLink, zum Beispiel `[[guide]]`.",
3581
+ fr: "Reliez les pages avec un WikiLink, par exemple `[[guide]]`.",
3582
+ ko: "WikiLink로 페이지를 연결하세요. 예: `[[guide]]`."
2357
3583
  }
2358
3584
  };
3585
+ function starterIndexContent(title, language) {
3586
+ return [
3587
+ frontmatter(),
3588
+ heading(1, title),
3589
+ "",
3590
+ read(INDEX.lead, language),
3591
+ "",
3592
+ read(INDEX.editFileNote, language),
3593
+ "",
3594
+ heading(2, read(INDEX.editHeading, language)),
3595
+ read(INDEX.editLead, language),
3596
+ "",
3597
+ read(INDEX.linkLine, language),
3598
+ "",
3599
+ heading(2, read(INDEX.exploreHeading, language)),
3600
+ "",
3601
+ "- [Getting started](/guide)",
3602
+ "- [[examples]]",
3603
+ ""
3604
+ ].join("\n");
3605
+ }
2359
3606
  function indexContent(variables, preset, language) {
2360
3607
  const title = variables.title;
3608
+ if (preset.name === "starter") return starterIndexContent(title, language);
2361
3609
  return [
2362
3610
  frontmatter(),
2363
3611
  heading(1, title),
@@ -2371,10 +3619,6 @@ function indexContent(variables, preset, language) {
2371
3619
  `- [${read(INDEX.exploreThemes, language)}](/framework/themes)`,
2372
3620
  "- [Working examples](/examples/)",
2373
3621
  "- [Plugin reference](/reference/plugins/)"
2374
- ] : preset.name === "starter" ? [
2375
- heading(2, read(INDEX.exploreHeading, language)),
2376
- "- [Getting started](/guide/)",
2377
- "- [Example notes](/examples/)"
2378
3622
  ] : [],
2379
3623
  heading(2, read(INDEX.editHeading, language)),
2380
3624
  read(INDEX.editLead, language),
@@ -2426,7 +3670,7 @@ var THEMES_COPY = {
2426
3670
  es: "Este starter usa `@riebeckite/theme-default`. Para probar otro tema:",
2427
3671
  de: "Dieser Starter verwendet `@riebeckite/theme-default`. Um ein anderes Theme zu testen:",
2428
3672
  fr: "Ce starter utilise `@riebeckite/theme-default`. Pour essayer un autre thème :",
2429
- ko: "이 스타터는 `@riebeckite/theme-default`를 사용합니다. 다른 테마를 시도하려면:"
3673
+ ko: "이 스타터는 `@riebeckite/theme-default`를 사용합니다. 다른 테마를 시도하려면:"
2430
3674
  },
2431
3675
  switchStep1: {
2432
3676
  en: "Install the package:",
@@ -2435,7 +3679,7 @@ var THEMES_COPY = {
2435
3679
  es: "Instala el paquete:",
2436
3680
  de: "Installiere das Paket:",
2437
3681
  fr: "Installez le paquet :",
2438
- ko: "패키지를 설치합니다:"
3682
+ ko: "패키지를 설치합니다:"
2439
3683
  },
2440
3684
  switchStep2: {
2441
3685
  en: "Point `theme` at the new factory in `riebeckite.config.ts`:",
@@ -2444,7 +3688,7 @@ var THEMES_COPY = {
2444
3688
  es: "Apunta `theme` al nuevo factory en `riebeckite.config.ts`:",
2445
3689
  de: "Setze `theme` in `riebeckite.config.ts` auf den neuen Factory:",
2446
3690
  fr: "Pointez `theme` vers le nouveau factory dans `riebeckite.config.ts` :",
2447
- ko: "`riebeckite.config.ts`의 `theme`를 새 팩토리로 변경합니다:"
3691
+ ko: "`riebeckite.config.ts`의 `theme`를 새 팩토리로 변경합니다:"
2448
3692
  },
2449
3693
  tableHeading: {
2450
3694
  en: "The bundled themes",
@@ -2664,7 +3908,7 @@ var PLUGINS_COPY = {
2664
3908
  es: "Instala el paquete:",
2665
3909
  de: "Installiere das Paket:",
2666
3910
  fr: "Installez le paquet :",
2667
- ko: "패키지를 설치합니다:"
3911
+ ko: "패키지를 설치합니다:"
2668
3912
  },
2669
3913
  installStep2: {
2670
3914
  en: "Register it in the `plugins` array of `riebeckite.config.ts`:",
@@ -2673,7 +3917,7 @@ var PLUGINS_COPY = {
2673
3917
  es: "Regístralo en el array `plugins` de `riebeckite.config.ts`:",
2674
3918
  de: "Registriere es im `plugins`-Array der `riebeckite.config.ts`:",
2675
3919
  fr: "Enregistrez-le dans le tableau `plugins` de `riebeckite.config.ts` :",
2676
- ko: "`riebeckite.config.ts`의 `plugins` 배열에 등록합니다:"
3920
+ ko: "`riebeckite.config.ts`의 `plugins` 배열에 등록합니다:"
2677
3921
  },
2678
3922
  fullList: {
2679
3923
  en: "The complete plugin index lives in the repository:",
@@ -2682,7 +3926,7 @@ var PLUGINS_COPY = {
2682
3926
  es: "El índice completo de plugins vive en el repositorio:",
2683
3927
  de: "Der vollständige Plugin-Index liegt im Repository:",
2684
3928
  fr: "L'index complet des plugins se trouve dans le dépôt :",
2685
- ko: "전체 플러그인 목록은 저장소에 있습니다:"
3929
+ ko: "전체 플러그인 목록은 저장소에 있습니다:"
2686
3930
  },
2687
3931
  fullListLabel: {
2688
3932
  en: "Riebeckite plugins on GitHub",
@@ -3188,7 +4432,65 @@ function pluginsContent(language) {
3188
4432
  lines.push("");
3189
4433
  return lines.join("\n");
3190
4434
  }
3191
- function guideContent() {
4435
+ function starterGuideContent() {
4436
+ return [
4437
+ frontmatter(),
4438
+ heading(1, "Getting started"),
4439
+ "",
4440
+ "This site was generated from a Riebeckite scaffold preset.",
4441
+ "Everything below lives in this repository, ready to edit.",
4442
+ "",
4443
+ heading(2, "Run the site"),
4444
+ "",
4445
+ codeBlock("sh", ["npm install", "npm exec riebeckite dev"].join("\n")),
4446
+ "",
4447
+ "Open the URL printed in the terminal. The page reloads every time",
4448
+ "you save a Markdown file.",
4449
+ "",
4450
+ heading(2, "Add a page"),
4451
+ "",
4452
+ "Create `content/hello.md`:",
4453
+ "",
4454
+ codeBlock(
4455
+ "md",
4456
+ [
4457
+ "---",
4458
+ "publish: true",
4459
+ "---",
4460
+ "",
4461
+ "# Hello",
4462
+ "",
4463
+ "This is my second page."
4464
+ ].join("\n")
4465
+ ),
4466
+ "",
4467
+ "The dev server serves it at `/hello`.",
4468
+ "",
4469
+ heading(2, "Link the pages"),
4470
+ "",
4471
+ "Write `[[hello]]` anywhere and it becomes a link to that page.",
4472
+ "",
4473
+ heading(2, "Translate a page"),
4474
+ "",
4475
+ "Copy a file next to the original with the language suffix, for",
4476
+ "example `hello.ja.md`. The language switcher picks it up.",
4477
+ "",
4478
+ heading(2, "Build"),
4479
+ "",
4480
+ codeBlock("sh", "npm exec riebeckite build"),
4481
+ "",
4482
+ "The static site is written to `dist/`.",
4483
+ "",
4484
+ heading(2, "Extend"),
4485
+ "",
4486
+ "Plugins and themes are registered in `riebeckite.config.ts`.",
4487
+ "Install a package, import its factory, and add it to the `plugins`",
4488
+ "array, or point `theme` at a new theme factory.",
4489
+ ""
4490
+ ].join("\n");
4491
+ }
4492
+ function guideContent(preset) {
4493
+ if (preset.name !== "showcase") return starterGuideContent();
3192
4494
  return [
3193
4495
  frontmatter(),
3194
4496
  heading(1, "Getting started"),
@@ -3213,9 +4515,9 @@ function guideContent() {
3213
4515
  codeBlock(
3214
4516
  "sh",
3215
4517
  [
3216
- "pnpm install",
3217
- "pnpm exec riebeckite dev",
3218
- "pnpm exec riebeckite build"
4518
+ "npm install",
4519
+ "npm exec riebeckite dev",
4520
+ "npm exec riebeckite build"
3219
4521
  ].join("\n")
3220
4522
  ),
3221
4523
  "",
@@ -3253,7 +4555,77 @@ var SHOWCASE_GUIDE_DEMOS = [
3253
4555
  "@riebeckite/plugin-canvas",
3254
4556
  "@riebeckite/plugin-flashcards"
3255
4557
  ];
4558
+ var STARTER_EXAMPLES = {
4559
+ heading: {
4560
+ en: "Example",
4561
+ ja: "サンプル"
4562
+ },
4563
+ intro: {
4564
+ en: "A small page showing what Riebeckite Markdown can do. Copy any section into your own files.",
4565
+ ja: "Riebeckite の Markdown でできることをまとめた小さなページです。必要なセクションを自分のファイルにコピーできます。"
4566
+ },
4567
+ linksHeading: {
4568
+ en: "Links",
4569
+ ja: "リンク"
4570
+ },
4571
+ linksBody: {
4572
+ en: "WikiLinks connect pages: [[guide]] opens Getting started. Regular Markdown links work as well — [Home](/) returns to the home page.",
4573
+ ja: "WikiLink でページをつなげます:[[guide]] は Getting started を開きます。通常の Markdown リンクも使えます — [Home](/) でホームに戻れます。"
4574
+ },
4575
+ codeHeading: {
4576
+ en: "Code",
4577
+ ja: "コード"
4578
+ },
4579
+ codeIntro: {
4580
+ en: "A fenced code block keeps its formatting and gains a toolbar from the code plugins:",
4581
+ ja: "フェンス付きコードブロックは書式を保ったまま表示され、コード系プラグインでツールバーが付きます:"
4582
+ },
4583
+ frontmatterHeading: {
4584
+ en: "Frontmatter",
4585
+ ja: "フロントマター"
4586
+ },
4587
+ frontmatterBody: {
4588
+ en: "The block between the `---` lines at the top of a file controls the page. `publish: true` keeps it in the built site:",
4589
+ ja: "ファイル先頭の `---` に挟まれた部分がページを制御します。`publish: true` を書き続けるとビルド対象に残ります:"
4590
+ }
4591
+ };
4592
+ function starterExamplesContent(language) {
4593
+ return [
4594
+ frontmatter(),
4595
+ heading(1, readSummary(STARTER_EXAMPLES.heading, language)),
4596
+ "",
4597
+ readSummary(STARTER_EXAMPLES.intro, language),
4598
+ "",
4599
+ heading(2, readSummary(STARTER_EXAMPLES.linksHeading, language)),
4600
+ "",
4601
+ readSummary(STARTER_EXAMPLES.linksBody, language),
4602
+ "",
4603
+ heading(2, readSummary(STARTER_EXAMPLES.codeHeading, language)),
4604
+ "",
4605
+ readSummary(STARTER_EXAMPLES.codeIntro, language),
4606
+ "",
4607
+ codeBlock(
4608
+ "ts",
4609
+ [
4610
+ "export function hello(): string {",
4611
+ ' return "Hello from Riebeckite";',
4612
+ "}"
4613
+ ].join("\n")
4614
+ ),
4615
+ "",
4616
+ heading(2, readSummary(STARTER_EXAMPLES.frontmatterHeading, language)),
4617
+ "",
4618
+ readSummary(STARTER_EXAMPLES.frontmatterBody, language),
4619
+ "",
4620
+ codeBlock(
4621
+ "yaml",
4622
+ ["---", "publish: true", "title: Example", "---"].join("\n")
4623
+ ),
4624
+ ""
4625
+ ].join("\n");
4626
+ }
3256
4627
  function examplesContent(preset, language) {
4628
+ if (preset.name === "starter") return starterExamplesContent(language);
3257
4629
  const lines = [
3258
4630
  frontmatter(),
3259
4631
  heading(1, readSummary(EXAMPLES_COPY.heading, language)),
@@ -3308,7 +4680,7 @@ function referencePluginsContent(preset, variables) {
3308
4680
  "Every registered plugin is listed below with the Markdown that triggers it,",
3309
4681
  "or with the Markdown/frontmatter/content shape it reads when the plugin is",
3310
4682
  "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.",
4683
+ "`content/` and run `npm exec riebeckite dev` to inspect the rendered result.",
3312
4684
  ""
3313
4685
  );
3314
4686
  for (const plugin of preset.plugins) {
@@ -3373,12 +4745,12 @@ function referenceThemesContent(preset) {
3373
4745
  }
3374
4746
 
3375
4747
  // src/scaffold/version.ts
3376
- var RIEBECKITE_VERSION = "^0.0.14";
4748
+ var RIEBECKITE_VERSION = "^0.0.16";
3377
4749
 
3378
4750
  // src/scaffold/templates.ts
3379
- function siteTemplateFiles(preset, variables) {
4751
+ function siteTemplateFiles(preset, variables, options = {}) {
3380
4752
  return [
3381
- { path: "package.json", content: packageJson(preset, variables) },
4753
+ { path: "package.json", content: packageJson(preset, variables, options) },
3382
4754
  {
3383
4755
  path: "riebeckite.config.ts",
3384
4756
  content: riebeckiteConfig(preset, variables)
@@ -3403,7 +4775,7 @@ function readPackageAsset(fileName) {
3403
4775
  new URL(`../assets/${fileName}`, import.meta.url)
3404
4776
  ];
3405
4777
  for (const candidate of candidates) {
3406
- if (fs6.existsSync(candidate)) return fs6.readFileSync(candidate);
4778
+ if (fs7.existsSync(candidate)) return fs7.readFileSync(candidate);
3407
4779
  }
3408
4780
  throw new Error(`Missing Riebeckite scaffold asset: ${fileName}`);
3409
4781
  }
@@ -3432,7 +4804,7 @@ function appFiles(preset) {
3432
4804
  style: () => style(preset),
3433
4805
  renderer: () => renderer(preset),
3434
4806
  index: () => indexRoute(preset),
3435
- slug: () => slugRoute(),
4807
+ slug: () => slugRoute(preset),
3436
4808
  header: () => siteHeader(),
3437
4809
  article: () => article()
3438
4810
  };
@@ -3459,7 +4831,7 @@ function appFiles(preset) {
3459
4831
  ...has("article") ? [{ path: "app/components/article.tsx", content: get("article") }] : []
3460
4832
  ];
3461
4833
  }
3462
- function packageJson(preset, variables) {
4834
+ function packageJson(preset, variables, options) {
3463
4835
  const dependencies = {
3464
4836
  "@riebeckite/core": RIEBECKITE_VERSION,
3465
4837
  "@riebeckite/honox": RIEBECKITE_VERSION,
@@ -3491,7 +4863,8 @@ function packageJson(preset, variables) {
3491
4863
  "@riebeckite/cli": RIEBECKITE_VERSION,
3492
4864
  "@types/node": "^24.5.2",
3493
4865
  typescript: "^5.0.0",
3494
- vite: "^8.0.9"
4866
+ vite: "^8.0.9",
4867
+ ...options.cloudflareWorkers ? { wrangler: WRANGLER_VERSION } : {}
3495
4868
  }
3496
4869
  },
3497
4870
  null,
@@ -3521,6 +4894,7 @@ function riebeckiteConfig(preset, variables) {
3521
4894
  lines.push(" content: {");
3522
4895
  lines.push(' directory: "content",');
3523
4896
  lines.push(" },");
4897
+ for (const line of navigationConfig(preset)) lines.push(line);
3524
4898
  if (preset.theme) {
3525
4899
  const theme = preset.theme;
3526
4900
  lines.push(
@@ -3536,6 +4910,25 @@ function riebeckiteConfig(preset, variables) {
3536
4910
  return `${lines.join("\n")}
3537
4911
  `;
3538
4912
  }
4913
+ function navigationConfig(preset) {
4914
+ if (preset.name === "starter") {
4915
+ return [
4916
+ " navigation: {",
4917
+ ' header: [{ label: "Guide", href: "/guide" }, { label: "Examples", href: "/examples" }, { label: "Notes", href: "/notes/planning", children: [{ label: "Planning", href: "/notes/planning" }, { label: "Writing", href: "/notes/writing" }] }],',
4918
+ ' footer: [{ label: "Guide", href: "/guide" }, { label: "Examples", href: "/examples" }],',
4919
+ " },"
4920
+ ];
4921
+ }
4922
+ if (preset.name === "showcase") {
4923
+ return [
4924
+ " navigation: {",
4925
+ ' header: [{ label: "Guide", href: "/guide" }, { label: "Examples", href: "/examples" }, { label: "Framework", href: "/framework/plugins", children: [{ label: "Plugins", href: "/framework/plugins" }, { label: "Themes", href: "/framework/themes" }] }],',
4926
+ ' footer: [{ label: "Guide", href: "/guide" }, { label: "Examples", href: "/examples" }],',
4927
+ " },"
4928
+ ];
4929
+ }
4930
+ return [];
4931
+ }
3539
4932
  var OPTION_DEPTH = {
3540
4933
  empty: 0,
3541
4934
  minimal: 0,
@@ -3688,7 +5081,7 @@ var README_WORDS = {
3688
5081
  demoPages: "Try the demos",
3689
5082
  demoPagesBody: "The generated site ships content pages that exercise these features:",
3690
5083
  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."
5084
+ 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
5085
  },
3693
5086
  ja: {
3694
5087
  included: "含まれているもの",
@@ -3712,7 +5105,7 @@ var README_WORDS = {
3712
5105
  demoPages: "デモを試す",
3713
5106
  demoPagesBody: "生成されたサイトには、各機能を体験できるコンテンツページが含まれています:",
3714
5107
  copyDemos: "コピーして使えるデモ",
3715
- copyDemosBody: "以下のスニペットを `content/` 配下の Markdown ファイルに貼り付けて `pnpm exec riebeckite dev` を実行してください。それぞれ、このプリセットが登録しているプラグインでレンダリングされます。"
5108
+ copyDemosBody: "以下のスニペットを `content/` 配下の Markdown ファイルに貼り付けて `npm exec riebeckite dev` を実行してください。それぞれ、このプリセットが登録しているプラグインでレンダリングされます。"
3716
5109
  }
3717
5110
  };
3718
5111
  function readme(preset, variables) {
@@ -3745,13 +5138,23 @@ function readmeEn(preset, variables) {
3745
5138
  lines.push(`## ${words.commands}`, "");
3746
5139
  lines.push(
3747
5140
  "```sh",
3748
- "pnpm install",
3749
- "pnpm exec riebeckite check",
3750
- "pnpm exec riebeckite dev",
3751
- "pnpm exec riebeckite build",
5141
+ "npm install",
5142
+ "npm exec riebeckite check",
5143
+ "npm exec riebeckite dev",
5144
+ "npm exec riebeckite build",
3752
5145
  "```",
3753
5146
  ""
3754
5147
  );
5148
+ if (preset.name !== "empty") {
5149
+ lines.push(
5150
+ "## First edits",
5151
+ "",
5152
+ "- `content/index.md`: the first published page.",
5153
+ "- `riebeckite.config.ts`: set `site.title`, `site.baseUrl`, and `content.directory`.",
5154
+ "- `public/favicon.ico`: replace the site icon when you are ready.",
5155
+ ""
5156
+ );
5157
+ }
3755
5158
  if (preset.readme === "rich") {
3756
5159
  lines.push(...demoPagesLines(preset, variables, words, "en"));
3757
5160
  lines.push(...copyPasteDemoLines(preset, words, "en"));
@@ -3793,13 +5196,23 @@ function readmeJa(preset, variables) {
3793
5196
  lines.push(`## ${words.commands}`, "");
3794
5197
  lines.push(
3795
5198
  "```sh",
3796
- "pnpm install",
3797
- "pnpm exec riebeckite check",
3798
- "pnpm exec riebeckite dev",
3799
- "pnpm exec riebeckite build",
5199
+ "npm install",
5200
+ "npm exec riebeckite check",
5201
+ "npm exec riebeckite dev",
5202
+ "npm exec riebeckite build",
3800
5203
  "```",
3801
5204
  ""
3802
5205
  );
5206
+ if (preset.name !== "empty") {
5207
+ lines.push(
5208
+ "## 最初に編集する場所",
5209
+ "",
5210
+ "- `content/index.md`: 最初に公開されるページです。",
5211
+ "- `riebeckite.config.ts`: `site.title`、`site.baseUrl`、`content.directory` を設定します。",
5212
+ "- `public/favicon.ico`: 必要になったらサイトアイコンを置き換えます。",
5213
+ ""
5214
+ );
5215
+ }
3803
5216
  if (preset.readme === "rich") {
3804
5217
  lines.push(...demoPagesLines(preset, variables, words, "ja"));
3805
5218
  lines.push(...copyPasteDemoLines(preset, words, "ja"));
@@ -4263,12 +5676,18 @@ var ScaffoldSiteError = class extends Error {
4263
5676
  }
4264
5677
  };
4265
5678
  async function scaffoldRiebeckiteSite(options) {
4266
- const targetDirectory = path7.resolve(options.targetDirectory);
5679
+ const targetDirectory = path9.resolve(options.targetDirectory);
4267
5680
  const preset = resolveScaffoldPreset(options.preset);
4268
5681
  validateDeploymentOptions(options);
4269
5682
  const files = [
4270
- ...siteTemplateFiles(preset, templateVariables(options, targetDirectory)),
4271
- ...options.githubActions ? deploymentTemplateFiles(deploymentOptions(options)) : []
5683
+ ...siteTemplateFiles(preset, templateVariables(options, targetDirectory), {
5684
+ cloudflareWorkers: options.cloudflareWorkers
5685
+ }),
5686
+ ...options.githubActions ? deploymentTemplateFiles(deploymentOptions(options)) : options.cloudflareWorkers ? [
5687
+ wranglerJsoncFile(
5688
+ workerNameFromDirectory(path9.basename(targetDirectory))
5689
+ )
5690
+ ] : []
4272
5691
  ];
4273
5692
  await assertTargetWritable(
4274
5693
  targetDirectory,
@@ -4276,9 +5695,9 @@ async function scaffoldRiebeckiteSite(options) {
4276
5695
  options.overwrite ?? false
4277
5696
  );
4278
5697
  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);
5698
+ const filePath = path9.join(targetDirectory, file.path);
5699
+ await fs8.mkdir(path9.dirname(filePath), { recursive: true });
5700
+ await fs8.writeFile(filePath, file.content);
4282
5701
  }
4283
5702
  return { targetDirectory, files: files.map((file) => file.path) };
4284
5703
  }
@@ -4314,7 +5733,7 @@ function validateDeploymentOptions(options) {
4314
5733
  }
4315
5734
  function templateVariables(options, targetDirectory) {
4316
5735
  const name = normalizePackageName(
4317
- options.name ?? path7.basename(targetDirectory)
5736
+ options.name ?? path9.basename(targetDirectory)
4318
5737
  );
4319
5738
  return {
4320
5739
  name,
@@ -4327,9 +5746,9 @@ function templateVariables(options, targetDirectory) {
4327
5746
  async function assertTargetWritable(targetDirectory, files, overwrite) {
4328
5747
  let entries;
4329
5748
  try {
4330
- entries = await fs7.readdir(targetDirectory);
5749
+ entries = await fs8.readdir(targetDirectory);
4331
5750
  } catch (error) {
4332
- if (isNotFoundError2(error)) return;
5751
+ if (isNotFoundError3(error)) return;
4333
5752
  throw error;
4334
5753
  }
4335
5754
  const conflicts = entries.filter(
@@ -4347,30 +5766,54 @@ function normalizePackageName(value) {
4347
5766
  const normalized = value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[-_.]+/, "").replace(/[-_.]+$/, "");
4348
5767
  return normalized || "riebeckite-site";
4349
5768
  }
4350
- function isNotFoundError2(error) {
5769
+ function isNotFoundError3(error) {
4351
5770
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
4352
5771
  }
4353
5772
 
4354
5773
  // src/scaffold/next-steps.ts
4355
- function formatScaffoldNextSteps(relativeDirectory) {
5774
+ function formatScaffoldNextSteps(relativeDirectory, options = {}) {
4356
5775
  const lines = ["Next steps:"];
4357
5776
  if (relativeDirectory !== ".") {
4358
5777
  lines.push(` cd ${relativeDirectory}`);
4359
5778
  }
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");
5779
+ lines.push(" npm install");
5780
+ lines.push(" npm exec riebeckite check");
5781
+ lines.push(" npm exec riebeckite dev");
5782
+ lines.push(" npm exec riebeckite build");
5783
+ if (options.editFile !== void 0) {
5784
+ lines.push("");
5785
+ lines.push("Then edit:");
5786
+ lines.push(` ${options.editFile}`);
5787
+ lines.push("");
5788
+ lines.push("Common first settings:");
5789
+ lines.push(" riebeckite.config.ts: site.title, site.baseUrl, content.directory");
5790
+ }
5791
+ if (options.externalContent === true) {
5792
+ lines.push("");
5793
+ lines.push("Operational content lives in the content repository. CI checks it out to content/.");
5794
+ lines.push(
5795
+ "Copy github/notify-site.yml to the content repository as .github/workflows/notify-site.yml."
5796
+ );
5797
+ }
4364
5798
  return lines.join("\n");
4365
5799
  }
4366
5800
  export {
5801
+ DEFAULT_WORKER_NAME,
5802
+ GITHUB_ACTIONS_SECRETS,
5803
+ PACKAGE_MANAGER,
4367
5804
  SCAFFOLD_DEFAULT_PRESET,
4368
5805
  SCAFFOLD_PRESET_NAMES,
4369
5806
  ScaffoldSiteError,
5807
+ WORKER_NAME_MAX_LENGTH,
5808
+ WRANGLER_DEFAULTS,
5809
+ WRANGLER_VERSION,
5810
+ assertGitHubRepository,
5811
+ buildDefaultWranglerConfig,
4370
5812
  buildHonoxApplication,
4371
5813
  createRiebeckiteSsg,
4372
5814
  defaultSsgEntry,
4373
5815
  defaultSsrExternals,
5816
+ deploymentTemplateFiles,
4374
5817
  empty,
4375
5818
  formatScaffoldNextSteps,
4376
5819
  isScaffoldPresetName,
@@ -4389,5 +5832,6 @@ export {
4389
5832
  scaffoldRiebeckiteSite,
4390
5833
  showcase,
4391
5834
  startHonoxDevServer,
4392
- starter
5835
+ starter,
5836
+ workerNameFromDirectory
4393
5837
  };