@riebeckite/plugin-seo 0.0.16 → 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
@@ -20,8 +20,6 @@ export default defineConfig({
20
20
  // ...
21
21
  plugins: [
22
22
  seo({
23
- siteName: "Riebeckite Blog",
24
- defaultImage: "/ogp.png",
25
23
  feed: { rss: true, atom: true, json: true, limit: 30 },
26
24
  sitemap: true,
27
25
  robots: true,
@@ -34,8 +32,6 @@ export default defineConfig({
34
32
 
35
33
  | Option | Type | Description |
36
34
  | ------ | ---- | ----------- |
37
- | `siteName` | `string` | Site name used in page titles. Defaults to `site.title`. |
38
- | `defaultImage` | `string` | Default OG image. Falls back to `site.defaultOgImage`. |
39
35
  | `feed` | `{ rss?: boolean; atom?: boolean; json?: boolean; limit?: number }` | Feed output settings. `limit` defaults to 30 and applies to every format. |
40
36
  | `sitemap` | `boolean` | Sitemap output settings. |
41
37
  | `robots` | `boolean` | robots.txt output settings. |
@@ -44,22 +40,27 @@ export default defineConfig({
44
40
 
45
41
  ### Articles (`buildArticleSeo`)
46
42
 
47
- - `title`: `"{title} | {siteName}"`
43
+ - `title`: `"{title} | {site.title}"`
48
44
  - `description`: `frontmatter.description`, otherwise the first 160 characters
49
45
  of the post text
50
46
  - `canonicalUrl`: `frontmatter.canonical`, otherwise the entry's resolved canonical permalink
51
47
  - `imageUrl`: `frontmatter.ogImage` / `frontmatter.image`, otherwise
52
- `defaultImage` / `site.defaultOgImage`
48
+ `site.defaultOgImage`
53
49
  - `noindex`: `frontmatter.noindex === true`
54
50
  - `publishedTime`: `published` / `date` / `created`
55
51
  - `modifiedTime`: `updated`, falling back to the publish time
56
52
  - `tags`, `readingTimeMinutes`
57
53
  - JSON-LD: `BlogPosting` and `BreadcrumbList`
58
54
 
55
+ Pass the page's `headTags` when they already include a BreadcrumbList (for
56
+ example contributed by the breadcrumbs plugin) and this plugin omits its own
57
+ list, so the page keeps a single BreadcrumbList entity.
58
+
59
59
  ### Websites (`buildWebsiteSeo`)
60
60
 
61
61
  Title, description, canonical URL, default image, and JSON-LD `WebSite` +
62
- `BreadcrumbList` for index, tag, and other non-article pages.
62
+ `BreadcrumbList` for index, tag, and other non-article pages. The
63
+ BreadcrumbList is omitted under the same `headTags` condition as articles.
63
64
 
64
65
  ## Feeds, sitemap, and robots
65
66
 
package/README_ja.md CHANGED
@@ -14,8 +14,6 @@ export default defineConfig({
14
14
  // ...
15
15
  plugins: [
16
16
  seo({
17
- siteName: "Riebeckite Blog",
18
- defaultImage: "/ogp.png",
19
17
  feed: { rss: true, atom: true, json: true, limit: 30 },
20
18
  sitemap: true,
21
19
  robots: true,
@@ -26,15 +24,13 @@ export default defineConfig({
26
24
 
27
25
  | 項目 | 説明 |
28
26
  | --- | --- |
29
- | `siteName` | ページタイトルに使うサイト名。省略時は `site.title` |
30
- | `defaultImage` | 既定の OGP 画像。省略時は `site.defaultOgImage` |
31
27
  | `feed` | RSS、Atom、JSON Feed の出力を個別に有効化する。`limit` は全形式共通の件数上限で、既定値は 30 件 |
32
28
  | `sitemap` | サイトマップを出力する |
33
29
  | `robots` | robots.txt を出力する |
34
30
 
35
31
  ## 記事の frontmatter が出力を決める
36
32
 
37
- `buildArticleSeo()` は記事タイトル、説明、canonical URL、OGP 画像、公開・更新日時、タグ、読了時間を組み立て、`BlogPosting` と `BreadcrumbList` の JSON-LD を作ります。値は次の順で補完されます。
33
+ `buildArticleSeo()` は記事タイトル、説明、canonical URL、OGP 画像、公開・更新日時、タグ、読了時間を組み立て、`BlogPosting` と `BreadcrumbList` の JSON-LD を作ります。呼び出し側がページの `headTags` を渡し、そこに BreadcrumbList(breadcrumbs プラグインが提供するものなど)が既にある場合、重複を避けるためこのプラグイン側の BreadcrumbList は省略します。値は次の順で補完されます。
38
34
 
39
35
  | フィールド | 用途 |
40
36
  | --- | --- |
package/dist/index.js CHANGED
@@ -125,16 +125,16 @@ function renderJsonFeed(config, entries) {
125
125
  }
126
126
  function getResolvedFeedMetadata(config) {
127
127
  return {
128
- title: config.site.feed.title ?? config.site.title,
129
- description: config.site.feed.description ?? config.site.description,
130
- language: config.site.feed.language ?? getHtmlLanguage(config)
128
+ title: config.site.feed.title,
129
+ description: config.site.feed.description,
130
+ language: config.site.feed.language
131
131
  };
132
132
  }
133
133
 
134
134
  // src/metadata.ts
135
135
  import { calculateReadingTime } from "@riebeckite/core";
136
- function buildArticleSeo(config, options, permalink, post) {
137
- const siteName = getSiteName(config, options);
136
+ function buildArticleSeo(config, permalink, post, headTags) {
137
+ const siteName = config.site.title;
138
138
  const title = getArticleTitle(config, permalink, post.frontmatter.title);
139
139
  const canonicalUrl = buildCanonicalUrl(
140
140
  config,
@@ -144,7 +144,6 @@ function buildArticleSeo(config, options, permalink, post) {
144
144
  const description = getDescription(post) || config.site.description;
145
145
  const imageUrl = buildImageUrl(
146
146
  config,
147
- options,
148
147
  post.frontmatter.ogImage ?? post.frontmatter.image
149
148
  );
150
149
  const publishedTime = getIsoDate2(
@@ -153,10 +152,31 @@ function buildArticleSeo(config, options, permalink, post) {
153
152
  const modifiedTime = getIsoDate2(post.frontmatter.updated) ?? publishedTime;
154
153
  const tags = normalizeTags(post.frontmatter.tags);
155
154
  const readingTimeMinutes = calculateReadingTime(post.html);
156
- const breadcrumb = buildBreadcrumbSchema(config, [
157
- { name: siteName, url: buildAbsoluteUrl(config, "/") },
158
- { name: title, url: canonicalUrl }
159
- ]);
155
+ const jsonLd = [
156
+ removeUndefined({
157
+ "@context": "https://schema.org",
158
+ "@type": "BlogPosting",
159
+ headline: title,
160
+ description,
161
+ url: canonicalUrl,
162
+ image: imageUrl ? [imageUrl] : void 0,
163
+ datePublished: publishedTime,
164
+ dateModified: modifiedTime,
165
+ author: config.site.author ? { "@type": "Person", name: config.site.author } : void 0,
166
+ publisher: { "@type": "Organization", name: siteName },
167
+ keywords: tags.length > 0 ? tags.join(", ") : void 0,
168
+ timeRequired: readingTimeMinutes ? `PT${readingTimeMinutes}M` : void 0,
169
+ inLanguage: getHtmlLanguage(config)
170
+ })
171
+ ];
172
+ if (!hasBreadcrumbListHeadTag(headTags)) {
173
+ jsonLd.push(
174
+ buildBreadcrumbSchema(config, [
175
+ { name: siteName, url: buildAbsoluteUrl(config, "/") },
176
+ { name: title, url: canonicalUrl }
177
+ ])
178
+ );
179
+ }
160
180
  return {
161
181
  title: buildPageTitle(siteName, title),
162
182
  description,
@@ -168,32 +188,32 @@ function buildArticleSeo(config, options, permalink, post) {
168
188
  modifiedTime,
169
189
  tags,
170
190
  readingTimeMinutes,
171
- jsonLd: [
172
- removeUndefined({
173
- "@context": "https://schema.org",
174
- "@type": "BlogPosting",
175
- headline: title,
176
- description,
177
- url: canonicalUrl,
178
- image: imageUrl ? [imageUrl] : void 0,
179
- datePublished: publishedTime,
180
- dateModified: modifiedTime,
181
- author: config.site.author ? { "@type": "Person", name: config.site.author } : void 0,
182
- publisher: { "@type": "Organization", name: siteName },
183
- keywords: tags.length > 0 ? tags.join(", ") : void 0,
184
- timeRequired: readingTimeMinutes ? `PT${readingTimeMinutes}M` : void 0,
185
- inLanguage: getHtmlLanguage(config)
186
- }),
187
- breadcrumb
188
- ]
191
+ jsonLd
189
192
  };
190
193
  }
191
- function buildWebsiteSeo(config, options, input) {
192
- const siteName = getSiteName(config, options);
194
+ function buildWebsiteSeo(config, input, headTags) {
195
+ const siteName = config.site.title;
193
196
  const canonicalUrl = buildAbsoluteUrl(config, input.path);
194
197
  const description = input.description || config.site.description;
195
- const imageUrl = buildImageUrl(config, options);
198
+ const imageUrl = buildImageUrl(config);
196
199
  const title = input.kind === "tag" ? input.title : buildPageTitle(siteName, input.title);
200
+ const jsonLd = [
201
+ removeUndefined({
202
+ "@context": "https://schema.org",
203
+ "@type": "WebSite",
204
+ name: siteName,
205
+ description,
206
+ url: canonicalUrl,
207
+ inLanguage: getHtmlLanguage(config)
208
+ })
209
+ ];
210
+ if (!hasBreadcrumbListHeadTag(headTags)) {
211
+ jsonLd.push(
212
+ buildBreadcrumbSchema(config, [
213
+ { name: siteName, url: buildAbsoluteUrl(config, "/") }
214
+ ])
215
+ );
216
+ }
197
217
  return {
198
218
  title,
199
219
  description,
@@ -202,19 +222,7 @@ function buildWebsiteSeo(config, options, input) {
202
222
  type: "website",
203
223
  noindex: false,
204
224
  tags: [],
205
- jsonLd: [
206
- removeUndefined({
207
- "@context": "https://schema.org",
208
- "@type": "WebSite",
209
- name: siteName,
210
- description,
211
- url: canonicalUrl,
212
- inLanguage: getHtmlLanguage(config)
213
- }),
214
- buildBreadcrumbSchema(config, [
215
- { name: siteName, url: buildAbsoluteUrl(config, "/") }
216
- ])
217
- ]
225
+ jsonLd
218
226
  };
219
227
  }
220
228
  function buildCanonicalUrl(config, canonical, permalink) {
@@ -223,13 +231,10 @@ function buildCanonicalUrl(config, canonical, permalink) {
223
231
  }
224
232
  return buildPostUrl(config, permalink);
225
233
  }
226
- function buildImageUrl(config, options, value) {
227
- const image = value || options.defaultImage || config.site.defaultOgImage;
234
+ function buildImageUrl(config, value) {
235
+ const image = value || config.site.defaultOgImage;
228
236
  return image ? buildAbsoluteUrl(config, image) : "";
229
237
  }
230
- function getSiteName(config, options) {
231
- return options.siteName || config.site.title;
232
- }
233
238
  function getArticleTitle(config, slug, value) {
234
239
  if (typeof value === "string" && value.trim()) return value.trim();
235
240
  const lastSegment = slug.split("/").filter(Boolean).at(-1);
@@ -247,6 +252,25 @@ function getIsoDate2(value) {
247
252
  const date = new Date(value);
248
253
  return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
249
254
  }
255
+ function hasBreadcrumbListHeadTag(headTags) {
256
+ return headTags?.some(
257
+ (tag) => tag.tag === "script" && typeof tag.children === "string" && hasBreadcrumbListJsonLd(tag.children)
258
+ ) ?? false;
259
+ }
260
+ function hasBreadcrumbListJsonLd(value) {
261
+ try {
262
+ return hasBreadcrumbListType(JSON.parse(value));
263
+ } catch {
264
+ return false;
265
+ }
266
+ }
267
+ function hasBreadcrumbListType(value) {
268
+ if (Array.isArray(value)) return value.some(hasBreadcrumbListType);
269
+ if (!value || typeof value !== "object") return false;
270
+ const record = value;
271
+ if (record["@type"] === "BreadcrumbList") return true;
272
+ return hasBreadcrumbListType(record["@graph"]);
273
+ }
250
274
 
251
275
  // src/plugin.ts
252
276
  import { definePlugin } from "@riebeckite/core";
@@ -353,8 +377,8 @@ function seo(options = {}) {
353
377
  name: "seo",
354
378
  options,
355
379
  seo: {
356
- buildArticleSeo: (config, permalink, post) => buildArticleSeo(config, options, permalink, post),
357
- buildWebsiteSeo: (config, input) => buildWebsiteSeo(config, options, input),
380
+ buildArticleSeo: (config, permalink, post, headTags) => buildArticleSeo(config, permalink, post, headTags),
381
+ buildWebsiteSeo: (config, input, headTags) => buildWebsiteSeo(config, input, headTags),
358
382
  buildAbsoluteUrl,
359
383
  buildPostUrl,
360
384
  getDescription,
@@ -1,4 +1,3 @@
1
- import type { PostContent, ResolvedRiebeckiteConfig, SeoMetadata, WebsiteSeoInput } from "@riebeckite/core";
2
- import type { SeoPluginOptions } from "./types.js";
3
- export declare function buildArticleSeo(config: ResolvedRiebeckiteConfig, options: SeoPluginOptions, permalink: string, post: PostContent): SeoMetadata;
4
- export declare function buildWebsiteSeo(config: ResolvedRiebeckiteConfig, options: SeoPluginOptions, input: WebsiteSeoInput): SeoMetadata;
1
+ import type { PluginHeadTag, PostContent, ResolvedRiebeckiteConfig, SeoMetadata, WebsiteSeoInput } from "@riebeckite/core";
2
+ export declare function buildArticleSeo(config: ResolvedRiebeckiteConfig, permalink: string, post: PostContent, headTags?: readonly PluginHeadTag[]): SeoMetadata;
3
+ export declare function buildWebsiteSeo(config: ResolvedRiebeckiteConfig, input: WebsiteSeoInput, headTags?: readonly PluginHeadTag[]): SeoMetadata;
@@ -6,8 +6,6 @@ export type FeedOptions = {
6
6
  limit?: number;
7
7
  };
8
8
  export type SeoPluginOptions = {
9
- siteName?: string;
10
- defaultImage?: string;
11
9
  feed?: FeedOptions;
12
10
  sitemap?: boolean;
13
11
  robots?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riebeckite/plugin-seo",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -14,11 +14,11 @@
14
14
  }
15
15
  },
16
16
  "dependencies": {
17
- "@riebeckite/core": "0.0.16"
17
+ "@riebeckite/core": "0.0.17"
18
18
  },
19
19
  "devDependencies": {
20
20
  "tsx": "^4.22.4",
21
- "@riebeckite/test": "0.0.16"
21
+ "@riebeckite/test": "0.0.17"
22
22
  },
23
23
  "publishConfig": {
24
24
  "access": "public"