@riebeckite/plugin-breadcrumbs 0.0.15 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -32,11 +32,12 @@ render it in the document `<head>` (the reference Riebeckite app renders
32
32
  `entry.headTags` in `_renderer.tsx`). Item URLs are made absolute against the
33
33
  configured site `baseUrl`.
34
34
 
35
- When the `seo` plugin is also enabled it emits a two-level BreadcrumbList
36
- (`Home / Note`) inside its own article JSON-LD. The two blocks coexist; to
37
- avoid duplicate BreadcrumbList entities a Site can drop the seo plugin's
38
- placeholder for article pages (shown in the reference app's content route) or
39
- disable `jsonLd` on this plugin (`jsonLd: false`).
35
+ When the `seo` plugin is also enabled it would emit its own two-level
36
+ BreadcrumbList (`Home / Note`) inside the article JSON-LD. Pass the page's
37
+ `headTags` to the seo extension and it detects the BreadcrumbList this plugin
38
+ contributed and omits its own placeholder, so the page carries a single
39
+ BreadcrumbList entity without any Site-side coupling. Set `jsonLd: false` to
40
+ stop this plugin from emitting one instead.
40
41
 
41
42
  ## Usage
42
43
 
@@ -58,7 +59,7 @@ export default defineConfig({
58
59
  | `className` | `string` | `"rb-breadcrumbs"` | Root CSS class |
59
60
  | `ariaLabel` | `string` | `"Breadcrumbs"` | Accessible nav name |
60
61
  | `separator` | `string` | `"/"` | Text between crumbs |
61
- | `jsonLd` | `boolean` | `true` | Emit/replace the BreadcrumbList script |
62
+ | `jsonLd` | `boolean` | `true` | Emit the BreadcrumbList script |
62
63
 
63
64
  ```ts
64
65
  breadcrumbs({
@@ -103,7 +104,6 @@ import "@riebeckite/plugin-breadcrumbs/style.css";
103
104
  - `buildBreadcrumbItems({ manifest, entry, config, homeLabel })` — build the trail
104
105
  - `renderBreadcrumbNav(items, options)` — render the navigation HTML
105
106
  - `buildBreadcrumbJsonLd(config, items)` — build the JSON-LD object
106
- - `injectBreadcrumbNav(html, nav)` / `injectBreadcrumbJsonLd(html, schema)` — HTML injection helpers
107
107
  - Types: `BreadcrumbsOptions`, `ResolvedBreadcrumbsOptions`, `BreadcrumbItem`
108
108
 
109
109
  ## Limitations
@@ -115,4 +115,3 @@ import "@riebeckite/plugin-breadcrumbs/style.css";
115
115
  ## See also
116
116
 
117
117
  - [Plugin guide](../../../docs/en/docs/reference/plugin-api.md)
118
-
package/README_ja.md CHANGED
@@ -32,11 +32,11 @@ JavaScript は不要です。
32
32
  `entry.headTags` を描画します)。アイテムの URL は設定の `baseUrl` に基づき
33
33
  絶対 URL に変換されます。
34
34
 
35
- `seo` プラグインも有効な場合、seo 側の記事 JSON-LD に 2 階層の
36
- BreadcrumbList(`ホーム / ノート`)が含まれます。この 2 つは共存するため、
37
- BreadcrumbList の重複を避けたい Site は記事ページで seo の暫定版を除去して
38
- ください(リファレンスアプリのコンテンツルートで実施例を確認できます)。
39
- 無効にする場合は本プラグインの `jsonLd: false` も利用できます。
35
+ `seo` プラグインも有効な場合、seo 側は独自の 2 階層の BreadcrumbList
36
+ (`ホーム / ノート`)を記事 JSON-LD に含めようとします。ページの `headTags`
37
+ を seo 拡張へ渡すと、本プラグインが提供した BreadcrumbList を検出して seo 側の
38
+ 暫定版を省略するため、Site 側で調整しなくても BreadcrumbList は 1 つだけに
39
+ なります。本プラグインからの出力自体を止めたい場合は `jsonLd: false` を使います。
40
40
 
41
41
  ## 使い方
42
42
 
@@ -58,7 +58,7 @@ export default defineConfig({
58
58
  | `className` | `string` | `"rb-breadcrumbs"` | ルート要素の CSS クラス |
59
59
  | `ariaLabel` | `string` | `"Breadcrumbs"` | ナビゲーションのアクセシブル名 |
60
60
  | `separator` | `string` | `"/"` | パンくず間の文字 |
61
- | `jsonLd` | `boolean` | `true` | BreadcrumbList スクリプトを出力・置換する |
61
+ | `jsonLd` | `boolean` | `true` | BreadcrumbList スクリプトを出力する |
62
62
 
63
63
  ```ts
64
64
  breadcrumbs({
@@ -103,7 +103,6 @@ import "@riebeckite/plugin-breadcrumbs/style.css";
103
103
  - `buildBreadcrumbItems({ manifest, entry, config, homeLabel })` — パンくず列を組み立てる
104
104
  - `renderBreadcrumbNav(items, options)` — ナビゲーション HTML を生成する
105
105
  - `buildBreadcrumbJsonLd(config, items)` — JSON-LD オブジェクトを生成する
106
- - `injectBreadcrumbNav(html, nav)` / `injectBreadcrumbJsonLd(html, schema)` — HTML 注入ヘルパー
107
106
  - 型: `BreadcrumbsOptions`, `ResolvedBreadcrumbsOptions`, `BreadcrumbItem`
108
107
 
109
108
  ## 制約
@@ -112,7 +111,6 @@ import "@riebeckite/plugin-breadcrumbs/style.css";
112
111
  - スラッグ階層のみを参照します。frontmatter の並び順や series プラグインの
113
112
  順序は意図的に考慮しません。
114
113
 
115
- ## ????
116
-
117
- - [?????????](../../../docs/ja/docs/reference/plugin-api.md)
114
+ ## 関連リンク
118
115
 
116
+ - [プラグイン API](../../../docs/ja/docs/reference/plugin-api.md)
package/dist/index.d.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  import type { BreadcrumbsOptions } from "./src/types.js";
2
2
  export { buildBreadcrumbItems } from "./src/breadcrumbs.js";
3
3
  export { resolveBreadcrumbsOptions } from "./src/options.js";
4
- export { BREADCRUMBS_ATTRIBUTE, buildBreadcrumbHeadTag, buildBreadcrumbJsonLd, hasBreadcrumbHeadTag, injectBreadcrumbNav, renderBreadcrumbNav, } from "./src/render.js";
4
+ export { BREADCRUMBS_ATTRIBUTE, buildBreadcrumbHeadTag, buildBreadcrumbJsonLd, hasBreadcrumbHeadTag, renderBreadcrumbNav, } from "./src/render.js";
5
5
  export type { BreadcrumbItem, BreadcrumbsOptions, ResolvedBreadcrumbsOptions, } from "./src/types.js";
6
6
  export declare const BREADCRUMBS_PLUGIN_NAME = "breadcrumbs";
7
7
  /**
8
- * Slug-hierarchy breadcrumbs for Riebeckite notes.
8
+ * Public-location breadcrumbs for Riebeckite notes.
9
9
  *
10
- * For every published entry it derives a trail from the note's slug and
11
- * inserts a `<nav>` at the top of the manifest entry HTML, which is the final
12
- * rendering source. The hierarchical BreadcrumbList JSON-LD is contributed
10
+ * For every published entry it derives a trail from the note's canonical public location and
11
+ * contributes a `<nav>` to the article header. The hierarchical BreadcrumbList
12
+ * JSON-LD is contributed
13
13
  * through `entry.headTags` so the Site shell can render it in the document
14
14
  * `<head>`. No client runtime is required.
15
15
  */
package/dist/index.js CHANGED
@@ -1,29 +1,45 @@
1
1
  // index.ts
2
2
  import {
3
+ appendContentBodySlot,
3
4
  createStyleAsset,
4
5
  definePlugin
5
6
  } from "@riebeckite/core";
6
7
 
7
8
  // src/breadcrumbs.ts
8
- import { resolveFolderLocation } from "@riebeckite/core";
9
+ import { resolvePublicFolderLocation } from "@riebeckite/core";
9
10
  function buildBreadcrumbItems(args) {
10
11
  const { manifest, entry, config, homeLabel } = args;
11
- const segments = entry.slug.split("/").filter(Boolean);
12
- if (segments.length === 0) return [];
12
+ if (!entry.slug) return [];
13
+ const segments = publicPathSegments(entry.publicLocation.permalink);
13
14
  const items = [];
14
15
  const homeName = homeLabel || config.site.title;
15
- if (homeName) items.push({ name: homeName, url: "/" });
16
+ if (homeName) pushBreadcrumbItem(items, { name: homeName, url: "/" });
17
+ if (segments.length === 0) return items;
16
18
  for (let index = 0; index < segments.length - 1; index++) {
17
- const folderSlug = segments.slice(0, index + 1).join("/");
18
- const location = resolveFolderLocation(manifest, folderSlug);
19
- items.push({
19
+ const pathname = `/${segments.slice(0, index + 1).join("/")}`;
20
+ const location = resolvePublicFolderLocation(manifest, pathname);
21
+ pushBreadcrumbItem(items, {
20
22
  name: location.type === "content" ? location.entry.title : titleCaseSegment(segments[index]),
21
23
  ...location.type === "content" ? { url: location.entry.publicLocation.permalink } : location.type === "generated" ? { url: location.pathname } : {}
22
24
  });
23
25
  }
24
- items.push({ name: entry.title, url: entry.publicLocation.permalink });
26
+ pushBreadcrumbItem(items, {
27
+ name: entry.title,
28
+ url: entry.publicLocation.permalink
29
+ });
25
30
  return items;
26
31
  }
32
+ function pushBreadcrumbItem(items, item) {
33
+ const previous = items.at(-1);
34
+ if (previous?.name === item.name) {
35
+ items[items.length - 1] = item;
36
+ return;
37
+ }
38
+ items.push(item);
39
+ }
40
+ function publicPathSegments(permalink) {
41
+ return permalink.split("/").filter(Boolean);
42
+ }
27
43
  function titleCaseSegment(segment) {
28
44
  return segment.length === 0 ? segment : segment.charAt(0).toUpperCase() + segment.slice(1);
29
45
  }
@@ -96,23 +112,22 @@ function buildBreadcrumbHeadTag(schema) {
96
112
  }
97
113
  function hasBreadcrumbHeadTag(headTags) {
98
114
  return headTags?.some(
99
- (tag) => tag.tag === "script" && typeof tag.children === "string" && tag.children.includes('"@type":"BreadcrumbList"')
115
+ (tag) => tag.tag === "script" && typeof tag.children === "string" && hasBreadcrumbListJsonLd(tag.children)
100
116
  ) ?? false;
101
117
  }
102
- function injectBreadcrumbNav(html, nav) {
103
- if (nav === "" || html.includes(BREADCRUMBS_ATTRIBUTE)) return html;
104
- const article = /<article\b[^>]*>/i.exec(html);
105
- if (article) {
106
- const end = article.index + article[0].length;
107
- return `${html.slice(0, end)}${nav}${html.slice(end)}`;
118
+ function hasBreadcrumbListJsonLd(value) {
119
+ try {
120
+ return hasBreadcrumbListType(JSON.parse(value));
121
+ } catch {
122
+ return false;
108
123
  }
109
- const body = /<body\b[^>]*>/i.exec(html);
110
- if (body) {
111
- const end = body.index + body[0].length;
112
- return `${html.slice(0, end)}${nav}${html.slice(end)}`;
113
- }
114
- return `${nav}
115
- ${html}`;
124
+ }
125
+ function hasBreadcrumbListType(value) {
126
+ if (Array.isArray(value)) return value.some(hasBreadcrumbListType);
127
+ if (!value || typeof value !== "object") return false;
128
+ const record = value;
129
+ if (record["@type"] === "BreadcrumbList") return true;
130
+ return hasBreadcrumbListType(record["@graph"]);
116
131
  }
117
132
 
118
133
  // index.ts
@@ -133,7 +148,6 @@ function breadcrumbs(options = {}) {
133
148
  const { config, manifest } = context;
134
149
  if (!config) return;
135
150
  for (const entry of manifest.publicEntries) {
136
- if (entry.html.includes(BREADCRUMBS_ATTRIBUTE)) continue;
137
151
  const items = buildBreadcrumbItems({
138
152
  manifest,
139
153
  entry,
@@ -141,9 +155,10 @@ function breadcrumbs(options = {}) {
141
155
  homeLabel: resolved.homeLabel
142
156
  });
143
157
  if (items.length === 0) continue;
144
- let html = entry.html;
145
158
  const nav = renderBreadcrumbNav(items, resolved);
146
- html = injectBreadcrumbNav(html, nav);
159
+ if (!hasSlotFragment(entry.bodySlots?.["article.header"], nav)) {
160
+ appendContentBodySlot(entry, "article.header", nav);
161
+ }
147
162
  if (resolved.jsonLd) {
148
163
  const schema = buildBreadcrumbJsonLd(config, items);
149
164
  if (!hasBreadcrumbHeadTag(entry.headTags)) {
@@ -153,13 +168,19 @@ function breadcrumbs(options = {}) {
153
168
  ];
154
169
  }
155
170
  }
156
- entry.html = html;
157
171
  }
158
172
  },
159
173
  assets: [createStyleAsset(BREADCRUMBS_PLUGIN_NAME)]
160
174
  });
161
175
  }
162
176
  var breadcrumbsPlugin = breadcrumbs;
177
+ function hasSlotFragment(slot, fragment) {
178
+ return slot === fragment || slot?.startsWith(`${fragment}
179
+ `) || slot?.endsWith(`
180
+ ${fragment}`) || slot?.includes(`
181
+ ${fragment}
182
+ `) || false;
183
+ }
163
184
  function validateBreadcrumbsOptions(options) {
164
185
  if (!options) return [];
165
186
  const issues = [];
@@ -187,7 +208,6 @@ export {
187
208
  buildBreadcrumbItems,
188
209
  buildBreadcrumbJsonLd,
189
210
  hasBreadcrumbHeadTag,
190
- injectBreadcrumbNav,
191
211
  renderBreadcrumbNav,
192
212
  resolveBreadcrumbsOptions
193
213
  };
@@ -22,13 +22,3 @@ export declare function buildBreadcrumbHeadTag(schema: Record<string, unknown>):
22
22
  * repeated plugin passes do not emit duplicates.
23
23
  */
24
24
  export declare function hasBreadcrumbHeadTag(headTags: readonly PluginHeadTag[] | undefined): boolean;
25
- /**
26
- * Places the completed navigation at the top of the note.
27
- *
28
- * When the HTML is a full page (it contains an `<article>` or `<body>` tag)
29
- * the nav is inserted right after that opening tag. The manifest `html` we
30
- * operate on is normally a content fragment the Site wraps in its own
31
- * `<article>`, in which case the nav is simply prepended so it renders above
32
- * the note's title.
33
- */
34
- export declare function injectBreadcrumbNav(html: string, nav: string): string;
@@ -16,9 +16,7 @@ export type BreadcrumbsOptions = {
16
16
  /** Text placed between siblings (defaults to "/"). */
17
17
  separator?: string;
18
18
  /**
19
- * Emit a BreadcrumbList JSON-LD script. When the seo plugin already
20
- * rendered one, its list is replaced with the hierarchical trail
21
- * (defaults to `true`).
19
+ * Emit a hierarchical BreadcrumbList JSON-LD script (defaults to `true`).
22
20
  */
23
21
  jsonLd?: boolean;
24
22
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riebeckite/plugin-breadcrumbs",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -15,11 +15,11 @@
15
15
  "./style.css": "./style.css"
16
16
  },
17
17
  "dependencies": {
18
- "@riebeckite/core": "0.0.15"
18
+ "@riebeckite/core": "0.0.17"
19
19
  },
20
20
  "devDependencies": {
21
21
  "tsx": "^4.22.4",
22
- "@riebeckite/test": "0.0.15"
22
+ "@riebeckite/test": "0.0.17"
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "public"