@moku-labs/web 0.5.5 → 0.5.6

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.cjs CHANGED
@@ -1335,6 +1335,23 @@ function calculateReadingTime(text) {
1335
1335
  function articleToUrl(locale, slug) {
1336
1336
  return `/${locale}/${slug}/`;
1337
1337
  }
1338
+ /** Matches an `<img>` `src` that points at the co-located `images/` dir (relative or root-relative). */
1339
+ const RELATIVE_IMAGE_SRC = /(<img\b[^>]*?\bsrc=")(?:\.?\/)?images\//g;
1340
+ /**
1341
+ * Rewrite relative co-located image URLs (`./images/x.webp`) in rendered article HTML to the shared
1342
+ * absolute path the build copies them to (`/<slug>/images/...`), so they resolve from any locale page.
1343
+ *
1344
+ * @param html - The rendered article HTML.
1345
+ * @param slug - Article directory name.
1346
+ * @returns The HTML with image `src`s rewritten to absolute paths.
1347
+ * @example
1348
+ * ```ts
1349
+ * rewriteImageUrls('<img src="./images/a.webp">', "post"); // '<img src="/post/images/a.webp">'
1350
+ * ```
1351
+ */
1352
+ function rewriteImageUrls(html, slug) {
1353
+ return html.replaceAll(RELATIVE_IMAGE_SRC, `$1/${slug}/images/`);
1354
+ }
1338
1355
  /**
1339
1356
  * Build the canonical "article not found" error for {@link createContentApi.load}.
1340
1357
  * Centralised so the null-resolve path and the production draft-suppression path
@@ -1450,7 +1467,7 @@ async function readArticle(ctx, slug, fileLocale, outLocale, isFallback) {
1450
1467
  ctx.state.dirtyPaths.delete(filePath);
1451
1468
  const { frontmatter, body } = parseFrontmatter(raw, ctx.config);
1452
1469
  const processor = ensureProcessor(ctx.state, ctx.config);
1453
- const html = String(await processor.process(body));
1470
+ const html = rewriteImageUrls(String(await processor.process(body)), slug);
1454
1471
  const { readingTime, wordCount } = calculateReadingTime(body);
1455
1472
  return {
1456
1473
  frontmatter,
@@ -1662,6 +1679,18 @@ function createContentApi(ctx) {
1662
1679
  */
1663
1680
  articleToCard(article) {
1664
1681
  return toCard(article);
1682
+ },
1683
+ /**
1684
+ * The configured content source directory (e.g. `"./content"`).
1685
+ *
1686
+ * @returns The content directory path from config.
1687
+ * @example
1688
+ * ```ts
1689
+ * api.contentDir(); // "./content"
1690
+ * ```
1691
+ */
1692
+ contentDir() {
1693
+ return ctx.config.contentDir;
1665
1694
  }
1666
1695
  };
1667
1696
  }
@@ -3445,6 +3474,52 @@ function readCachedContent(ctx) {
3445
3474
  return cached instanceof Map ? cached : /* @__PURE__ */ new Map();
3446
3475
  }
3447
3476
  //#endregion
3477
+ //#region src/plugins/build/phases/content-images.ts
3478
+ /**
3479
+ * @file build phase — content-images. Copies each article's co-located image directory
3480
+ * (`<contentDir>/<slug>/images/`) to a single shared output dir (`<outDir>/<slug>/images/`) reused by
3481
+ * every locale, matching the absolute `/<slug>/images/...` URLs the content renderer emits. Gated by
3482
+ * `config.images`.
3483
+ */
3484
+ /** Conventional per-article image subdirectory name (alongside `<slug>/<locale>.md`). */
3485
+ const ARTICLE_IMAGE_DIR = "images";
3486
+ /**
3487
+ * Copy every article's co-located `images/` directory to `<outDir>/<slug>/images/`. No-op when
3488
+ * `config.images` is false or the content directory does not exist.
3489
+ *
3490
+ * @param ctx - Plugin context (provides `config`, `log`, `require`).
3491
+ * @returns The number of directories copied (one per article that has an `images/` dir).
3492
+ * @example
3493
+ * ```ts
3494
+ * const copied = await copyContentImages(ctx);
3495
+ * ```
3496
+ */
3497
+ async function copyContentImages(ctx) {
3498
+ if (!ctx.config.images) {
3499
+ ctx.log.debug("build:content-images", { skipped: true });
3500
+ return 0;
3501
+ }
3502
+ const contentDir = ctx.require(contentPlugin).contentDir();
3503
+ if (!(0, node_fs.existsSync)(contentDir)) {
3504
+ ctx.log.debug("build:content-images", {
3505
+ skipped: true,
3506
+ reason: "no content dir"
3507
+ });
3508
+ return 0;
3509
+ }
3510
+ const entries = await (0, node_fs_promises.readdir)(contentDir, { withFileTypes: true });
3511
+ let copied = 0;
3512
+ for (const entry of entries) {
3513
+ if (!entry.isDirectory()) continue;
3514
+ const source = node_path$1.default.join(contentDir, entry.name, ARTICLE_IMAGE_DIR);
3515
+ if (!(0, node_fs.existsSync)(source)) continue;
3516
+ await (0, node_fs_promises.cp)(source, node_path$1.default.join(ctx.config.outDir, entry.name, ARTICLE_IMAGE_DIR), { recursive: true });
3517
+ copied += 1;
3518
+ }
3519
+ ctx.log.debug("build:content-images", { copied });
3520
+ return copied;
3521
+ }
3522
+ //#endregion
3448
3523
  //#region src/plugins/build/phases/feeds.ts
3449
3524
  /**
3450
3525
  * @file build phase 4 — feeds. Generates RSS/Atom/JSON from cached content plus
@@ -4690,6 +4765,7 @@ const PHASE_ORDER = [
4690
4765
  "content",
4691
4766
  "images",
4692
4767
  "pages",
4768
+ "content-images",
4693
4769
  "feeds",
4694
4770
  "sitemap",
4695
4771
  "og-images",
@@ -4796,6 +4872,7 @@ async function runPipeline(ctx, options) {
4796
4872
  await withPhase(phaseContext, "bundle", () => bundle(phaseContext));
4797
4873
  await Promise.all([withPhase(phaseContext, "content", () => loadContent(phaseContext)), withPhase(phaseContext, "images", () => processImages(phaseContext))]);
4798
4874
  const pages = await withPhase(phaseContext, "pages", () => renderPages(phaseContext));
4875
+ await withPhase(phaseContext, "content-images", () => copyContentImages(phaseContext));
4799
4876
  await runOutputs(phaseContext);
4800
4877
  await withPhase(phaseContext, "root-index", async () => {
4801
4878
  if (pages.rootHtml !== null) await (0, node_fs_promises.writeFile)(node_path$1.default.join(outDir, "index.html"), pages.rootHtml, "utf8");
package/dist/index.d.cts CHANGED
@@ -1575,7 +1575,7 @@ interface State$2 {
1575
1575
  * const phase: PhaseName = "bundle";
1576
1576
  * ```
1577
1577
  */
1578
- type PhaseName = "bundle" | "content" | "images" | "pages" | "feeds" | "sitemap" | "og-images" | "public" | "not-found" | "locale-redirects" | "root-index";
1578
+ type PhaseName = "bundle" | "content" | "images" | "pages" | "content-images" | "feeds" | "sitemap" | "og-images" | "public" | "not-found" | "locale-redirects" | "root-index";
1579
1579
  /**
1580
1580
  * Result of a completed build run.
1581
1581
  *
@@ -1849,6 +1849,12 @@ type Api$1 = {
1849
1849
  * @param article - The source article.
1850
1850
  */
1851
1851
  articleToCard(article: Article): ArticleCard;
1852
+ /**
1853
+ * The configured content source directory (e.g. `"./content"`). Lets the build copy each
1854
+ * article's co-located assets (`<contentDir>/<slug>/images/`) into the output so the absolute
1855
+ * image URLs the renderer emits resolve.
1856
+ */
1857
+ contentDir(): string;
1852
1858
  };
1853
1859
  //#endregion
1854
1860
  //#region src/plugins/content/index.d.ts
package/dist/index.d.mts CHANGED
@@ -1575,7 +1575,7 @@ interface State$2 {
1575
1575
  * const phase: PhaseName = "bundle";
1576
1576
  * ```
1577
1577
  */
1578
- type PhaseName = "bundle" | "content" | "images" | "pages" | "feeds" | "sitemap" | "og-images" | "public" | "not-found" | "locale-redirects" | "root-index";
1578
+ type PhaseName = "bundle" | "content" | "images" | "pages" | "content-images" | "feeds" | "sitemap" | "og-images" | "public" | "not-found" | "locale-redirects" | "root-index";
1579
1579
  /**
1580
1580
  * Result of a completed build run.
1581
1581
  *
@@ -1849,6 +1849,12 @@ type Api$1 = {
1849
1849
  * @param article - The source article.
1850
1850
  */
1851
1851
  articleToCard(article: Article): ArticleCard;
1852
+ /**
1853
+ * The configured content source directory (e.g. `"./content"`). Lets the build copy each
1854
+ * article's co-located assets (`<contentDir>/<slug>/images/`) into the output so the absolute
1855
+ * image URLs the renderer emits resolve.
1856
+ */
1857
+ contentDir(): string;
1852
1858
  };
1853
1859
  //#endregion
1854
1860
  //#region src/plugins/content/index.d.ts
package/dist/index.mjs CHANGED
@@ -1322,6 +1322,23 @@ function calculateReadingTime(text) {
1322
1322
  function articleToUrl(locale, slug) {
1323
1323
  return `/${locale}/${slug}/`;
1324
1324
  }
1325
+ /** Matches an `<img>` `src` that points at the co-located `images/` dir (relative or root-relative). */
1326
+ const RELATIVE_IMAGE_SRC = /(<img\b[^>]*?\bsrc=")(?:\.?\/)?images\//g;
1327
+ /**
1328
+ * Rewrite relative co-located image URLs (`./images/x.webp`) in rendered article HTML to the shared
1329
+ * absolute path the build copies them to (`/<slug>/images/...`), so they resolve from any locale page.
1330
+ *
1331
+ * @param html - The rendered article HTML.
1332
+ * @param slug - Article directory name.
1333
+ * @returns The HTML with image `src`s rewritten to absolute paths.
1334
+ * @example
1335
+ * ```ts
1336
+ * rewriteImageUrls('<img src="./images/a.webp">', "post"); // '<img src="/post/images/a.webp">'
1337
+ * ```
1338
+ */
1339
+ function rewriteImageUrls(html, slug) {
1340
+ return html.replaceAll(RELATIVE_IMAGE_SRC, `$1/${slug}/images/`);
1341
+ }
1325
1342
  /**
1326
1343
  * Build the canonical "article not found" error for {@link createContentApi.load}.
1327
1344
  * Centralised so the null-resolve path and the production draft-suppression path
@@ -1437,7 +1454,7 @@ async function readArticle(ctx, slug, fileLocale, outLocale, isFallback) {
1437
1454
  ctx.state.dirtyPaths.delete(filePath);
1438
1455
  const { frontmatter, body } = parseFrontmatter(raw, ctx.config);
1439
1456
  const processor = ensureProcessor(ctx.state, ctx.config);
1440
- const html = String(await processor.process(body));
1457
+ const html = rewriteImageUrls(String(await processor.process(body)), slug);
1441
1458
  const { readingTime, wordCount } = calculateReadingTime(body);
1442
1459
  return {
1443
1460
  frontmatter,
@@ -1649,6 +1666,18 @@ function createContentApi(ctx) {
1649
1666
  */
1650
1667
  articleToCard(article) {
1651
1668
  return toCard(article);
1669
+ },
1670
+ /**
1671
+ * The configured content source directory (e.g. `"./content"`).
1672
+ *
1673
+ * @returns The content directory path from config.
1674
+ * @example
1675
+ * ```ts
1676
+ * api.contentDir(); // "./content"
1677
+ * ```
1678
+ */
1679
+ contentDir() {
1680
+ return ctx.config.contentDir;
1652
1681
  }
1653
1682
  };
1654
1683
  }
@@ -3432,6 +3461,52 @@ function readCachedContent(ctx) {
3432
3461
  return cached instanceof Map ? cached : /* @__PURE__ */ new Map();
3433
3462
  }
3434
3463
  //#endregion
3464
+ //#region src/plugins/build/phases/content-images.ts
3465
+ /**
3466
+ * @file build phase — content-images. Copies each article's co-located image directory
3467
+ * (`<contentDir>/<slug>/images/`) to a single shared output dir (`<outDir>/<slug>/images/`) reused by
3468
+ * every locale, matching the absolute `/<slug>/images/...` URLs the content renderer emits. Gated by
3469
+ * `config.images`.
3470
+ */
3471
+ /** Conventional per-article image subdirectory name (alongside `<slug>/<locale>.md`). */
3472
+ const ARTICLE_IMAGE_DIR = "images";
3473
+ /**
3474
+ * Copy every article's co-located `images/` directory to `<outDir>/<slug>/images/`. No-op when
3475
+ * `config.images` is false or the content directory does not exist.
3476
+ *
3477
+ * @param ctx - Plugin context (provides `config`, `log`, `require`).
3478
+ * @returns The number of directories copied (one per article that has an `images/` dir).
3479
+ * @example
3480
+ * ```ts
3481
+ * const copied = await copyContentImages(ctx);
3482
+ * ```
3483
+ */
3484
+ async function copyContentImages(ctx) {
3485
+ if (!ctx.config.images) {
3486
+ ctx.log.debug("build:content-images", { skipped: true });
3487
+ return 0;
3488
+ }
3489
+ const contentDir = ctx.require(contentPlugin).contentDir();
3490
+ if (!existsSync(contentDir)) {
3491
+ ctx.log.debug("build:content-images", {
3492
+ skipped: true,
3493
+ reason: "no content dir"
3494
+ });
3495
+ return 0;
3496
+ }
3497
+ const entries = await readdir(contentDir, { withFileTypes: true });
3498
+ let copied = 0;
3499
+ for (const entry of entries) {
3500
+ if (!entry.isDirectory()) continue;
3501
+ const source = path.join(contentDir, entry.name, ARTICLE_IMAGE_DIR);
3502
+ if (!existsSync(source)) continue;
3503
+ await cp(source, path.join(ctx.config.outDir, entry.name, ARTICLE_IMAGE_DIR), { recursive: true });
3504
+ copied += 1;
3505
+ }
3506
+ ctx.log.debug("build:content-images", { copied });
3507
+ return copied;
3508
+ }
3509
+ //#endregion
3435
3510
  //#region src/plugins/build/phases/feeds.ts
3436
3511
  /**
3437
3512
  * @file build phase 4 — feeds. Generates RSS/Atom/JSON from cached content plus
@@ -4677,6 +4752,7 @@ const PHASE_ORDER = [
4677
4752
  "content",
4678
4753
  "images",
4679
4754
  "pages",
4755
+ "content-images",
4680
4756
  "feeds",
4681
4757
  "sitemap",
4682
4758
  "og-images",
@@ -4783,6 +4859,7 @@ async function runPipeline(ctx, options) {
4783
4859
  await withPhase(phaseContext, "bundle", () => bundle(phaseContext));
4784
4860
  await Promise.all([withPhase(phaseContext, "content", () => loadContent(phaseContext)), withPhase(phaseContext, "images", () => processImages(phaseContext))]);
4785
4861
  const pages = await withPhase(phaseContext, "pages", () => renderPages(phaseContext));
4862
+ await withPhase(phaseContext, "content-images", () => copyContentImages(phaseContext));
4786
4863
  await runOutputs(phaseContext);
4787
4864
  await withPhase(phaseContext, "root-index", async () => {
4788
4865
  if (pages.rootHtml !== null) await writeFile(path.join(outDir, "index.html"), pages.rootHtml, "utf8");
package/package.json CHANGED
@@ -112,5 +112,5 @@
112
112
  "test:integration": "vitest run --project integration",
113
113
  "test:coverage": "vitest run --project unit --project integration --coverage"
114
114
  },
115
- "version": "0.5.5"
115
+ "version": "0.5.6"
116
116
  }