wpheadless-lib 1.1.7 → 1.1.10

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.
@@ -1,5 +1,5 @@
1
1
  import { createPageEndpoints } from "wpjsapi-lib";
2
- import { getAbsoluteUrl, getFeaturedMedia, getPlainTextExcerpt, getYoastHead, } from "../../utils";
2
+ import { getAbsoluteUrl, getFeaturedMedia, getPlainTextExcerpt, getYoastHead, buildYoastRobotsMeta, } from "../../utils";
3
3
  import { buildLanguageAlternatesPerLanguage, buildAbsoluteLangUrl, getEntityLanguageId, withXDefault, getTranslationKey, } from "../../utils/hreflang";
4
4
  import { mapPageEntity } from "../../api/mappers";
5
5
  async function getLegalParentId(baseApiUrl, lang, langId) {
@@ -120,9 +120,11 @@ export function buildLegalMetadata({ page, slug, canonicalBase, useYoast = true,
120
120
  return baseMetadata;
121
121
  const yoast = getYoastHead(page, { siteUrl, wpApiUrl });
122
122
  const yoastImageUrl = yoast?.og_image?.[0]?.url || imageUrl;
123
+ const robots = buildYoastRobotsMeta(yoast?.robots);
123
124
  return {
124
125
  title: yoast?.title || page.title.rendered,
125
126
  description: yoast?.description || fallbackDescription,
127
+ ...(robots ? { robots } : {}),
126
128
  alternates: {
127
129
  canonical: url,
128
130
  ...(languageAlternates ? { languages: languageAlternates } : {}),
@@ -1,4 +1,4 @@
1
- import { getFeaturedMedia, getOgImageUrl, getPlainTextExcerpt, getYoastHead, } from "../../utils";
1
+ import { getFeaturedMedia, getOgImageUrl, getPlainTextExcerpt, getYoastHead, buildYoastRobotsMeta, } from "../../utils";
2
2
  import { getPostBySlug as getPostBySlugApi, listPostStaticParams, listPostsByTranslationKey, } from "../../api/posts";
3
3
  import { withXDefault } from "../../utils/hreflang";
4
4
  export async function getPostStaticParams({ baseApiUrl, lang, langId, }) {
@@ -72,9 +72,11 @@ export function buildPostMetadata({ post, canonical, useYoast = true, siteName,
72
72
  const yoast = getYoastHead(post, { siteUrl, wpApiUrl });
73
73
  if (!yoast)
74
74
  return fallbackMeta;
75
+ const robots = buildYoastRobotsMeta(yoast.robots);
75
76
  return {
76
77
  title: yoast.title || post.title.rendered,
77
78
  description: yoast.description || fallbackDescription,
79
+ ...(robots ? { robots } : {}),
78
80
  alternates: {
79
81
  canonical,
80
82
  ...(languageAlternates ? { languages: languageAlternates } : {}),
@@ -1,4 +1,4 @@
1
- import { rewriteYoastUrls } from "../../utils/seo";
1
+ import { buildYoastRobotsMeta, rewriteYoastUrls } from "../../utils/seo";
2
2
  const hasYoast = (entity) => {
3
3
  if (!entity || typeof entity !== "object")
4
4
  return false;
@@ -16,6 +16,7 @@ export const extractYoastMetadata = (entity, fallback = {}, opts = {}) => {
16
16
  const yoast = rewriteYoastUrls(yoastEntity?.yoast_head_json, opts);
17
17
  if (!yoast)
18
18
  return null;
19
+ const robots = buildYoastRobotsMeta(yoast.robots);
19
20
  const imageUrl = yoast.og_image?.[0]?.url || fallback.image;
20
21
  const title = yoast.title || fallback.title;
21
22
  const description = yoast.description || fallback.description;
@@ -23,6 +24,7 @@ export const extractYoastMetadata = (entity, fallback = {}, opts = {}) => {
23
24
  return {
24
25
  title,
25
26
  description,
27
+ ...(robots ? { robots } : {}),
26
28
  alternates: {
27
29
  canonical,
28
30
  },
@@ -1,3 +1,4 @@
1
+ import type { Metadata } from "next";
1
2
  import type { WPYoastHeadJson } from "wpjsapi-lib";
2
3
  type OgImage = {
3
4
  url?: string;
@@ -43,7 +44,9 @@ type RewriteOpts = {
43
44
  */
44
45
  export declare function rewriteYoastUrls<T extends WPYoastHeadJson | undefined>(yoast: T, opts?: RewriteOpts): T;
45
46
  export declare function getYoastHead(entity?: YoastHolder, opts?: RewriteOpts): WPYoastHeadJson | undefined;
46
- export declare function getYoastSchema(entity?: YoastSchemaHolder, opts?: RewriteOpts): import("wpjsapi-lib").WPYoastSchema;
47
+ export declare function buildYoastRobotsMeta(robots?: WPYoastHeadJson["robots"]): Metadata["robots"] | undefined;
48
+ export declare function isYoastNoindex(yoast?: WPYoastHeadJson): boolean;
49
+ export declare function getYoastSchema(entity?: YoastSchemaHolder, opts?: RewriteOpts): unknown;
47
50
  export declare function stripBreadcrumbsFromSchema(schema: unknown): unknown;
48
51
  export declare function getFeaturedMedia(entity?: WithEmbeddedMedia): FeaturedMedia | undefined;
49
52
  export declare function getPlainTextExcerpt(html?: string, maxLength?: number): string | undefined;
package/dist/utils/seo.js CHANGED
@@ -19,6 +19,64 @@ const normalizeWpApiOrigin = (wpApiUrl) => {
19
19
  const cleaned = wpApiUrl.replace(/\/wp-json\/?$/, "");
20
20
  return getOrigin(cleaned);
21
21
  };
22
+ const resolveRewriteOrigins = (opts, canonical) => {
23
+ const targetOrigin = getOrigin(opts.siteUrl);
24
+ const sourceOrigin = normalizeWpApiOrigin(opts.wpApiUrl) || getOrigin(canonical);
25
+ if (!targetOrigin || !sourceOrigin || targetOrigin === sourceOrigin) {
26
+ return null;
27
+ }
28
+ return { targetOrigin, sourceOrigin };
29
+ };
30
+ const IMAGE_EXTENSIONS = [
31
+ ".jpg",
32
+ ".jpeg",
33
+ ".png",
34
+ ".webp",
35
+ ".gif",
36
+ ".svg",
37
+ ".avif",
38
+ ".bmp",
39
+ ".tiff",
40
+ ];
41
+ const isImageUrl = (url) => {
42
+ const pathname = url.pathname.toLowerCase();
43
+ return (pathname.includes("/wp-content/uploads/") ||
44
+ IMAGE_EXTENSIONS.some((ext) => pathname.endsWith(ext)));
45
+ };
46
+ const rewriteSchemaValue = (value, { targetOrigin, sourceOrigin }) => {
47
+ if (Array.isArray(value)) {
48
+ return value.map((item) => rewriteSchemaValue(item, { targetOrigin, sourceOrigin }));
49
+ }
50
+ if (value && typeof value === "object") {
51
+ const obj = value;
52
+ const rewritten = {};
53
+ for (const [key, val] of Object.entries(obj)) {
54
+ rewritten[key] = rewriteSchemaValue(val, { targetOrigin, sourceOrigin });
55
+ }
56
+ return rewritten;
57
+ }
58
+ if (typeof value === "string") {
59
+ let parsed;
60
+ try {
61
+ parsed = new URL(value);
62
+ }
63
+ catch {
64
+ return value;
65
+ }
66
+ if (parsed.origin !== sourceOrigin)
67
+ return value;
68
+ if (isImageUrl(parsed))
69
+ return value;
70
+ return value.split(sourceOrigin).join(targetOrigin);
71
+ }
72
+ return value;
73
+ };
74
+ const rewriteYoastSchemaUrls = (schema, opts, canonical) => {
75
+ const origins = resolveRewriteOrigins(opts, canonical);
76
+ if (!origins)
77
+ return schema;
78
+ return rewriteSchemaValue(schema, origins);
79
+ };
22
80
  /**
23
81
  * Replace Yoast URLs so they match the frontend domain instead of the WP API domain.
24
82
  * It stringifies the object to replace origins and keeps structure intact.
@@ -26,14 +84,14 @@ const normalizeWpApiOrigin = (wpApiUrl) => {
26
84
  export function rewriteYoastUrls(yoast, opts = {}) {
27
85
  if (!yoast)
28
86
  return yoast;
29
- const targetOrigin = getOrigin(opts.siteUrl);
30
- const sourceOrigin = normalizeWpApiOrigin(opts.wpApiUrl) || getOrigin(yoast.canonical);
31
- if (!targetOrigin || !sourceOrigin || targetOrigin === sourceOrigin) {
87
+ const origins = resolveRewriteOrigins(opts, yoast.canonical);
88
+ if (!origins)
32
89
  return yoast;
33
- }
34
90
  try {
35
91
  const serialized = JSON.stringify(yoast);
36
- const replaced = serialized.split(sourceOrigin).join(targetOrigin);
92
+ const replaced = serialized
93
+ .split(origins.sourceOrigin)
94
+ .join(origins.targetOrigin);
37
95
  return JSON.parse(replaced);
38
96
  }
39
97
  catch {
@@ -44,9 +102,78 @@ export function getYoastHead(entity, opts) {
44
102
  const yoast = entity?.yoast_head_json;
45
103
  return rewriteYoastUrls(yoast, opts);
46
104
  }
105
+ const normalizeRobotsValue = (value) => {
106
+ if (!value)
107
+ return undefined;
108
+ const trimmed = value.trim().toLowerCase();
109
+ if (!trimmed)
110
+ return undefined;
111
+ const token = trimmed.includes(":") ? trimmed.split(":").pop() : trimmed;
112
+ return token?.trim() || undefined;
113
+ };
114
+ const parseRobotsFlag = (value, directive) => {
115
+ const token = normalizeRobotsValue(value);
116
+ if (!token)
117
+ return undefined;
118
+ const parts = token.split(/[,\s]+/).filter(Boolean);
119
+ if (parts.includes(`no${directive}`))
120
+ return false;
121
+ if (parts.includes(directive))
122
+ return true;
123
+ if (parts.includes("1"))
124
+ return true;
125
+ if (parts.includes("0"))
126
+ return false;
127
+ return undefined;
128
+ };
129
+ const parseRobotsNumber = (value) => {
130
+ const token = normalizeRobotsValue(value);
131
+ if (!token)
132
+ return undefined;
133
+ const parsed = Number(token);
134
+ return Number.isFinite(parsed) ? parsed : undefined;
135
+ };
136
+ const parseMaxImagePreview = (value) => {
137
+ const token = normalizeRobotsValue(value);
138
+ if (!token)
139
+ return undefined;
140
+ if (token === "none" || token === "standard" || token === "large") {
141
+ return token;
142
+ }
143
+ return undefined;
144
+ };
145
+ export function buildYoastRobotsMeta(robots) {
146
+ if (!robots)
147
+ return undefined;
148
+ const meta = {};
149
+ const index = parseRobotsFlag(robots.index, "index");
150
+ if (index !== undefined)
151
+ meta.index = index;
152
+ const follow = parseRobotsFlag(robots.follow, "follow");
153
+ if (follow !== undefined)
154
+ meta.follow = follow;
155
+ const maxSnippet = parseRobotsNumber(robots["max-snippet"]);
156
+ if (maxSnippet !== undefined)
157
+ meta.maxSnippet = maxSnippet;
158
+ const maxImagePreview = parseMaxImagePreview(robots["max-image-preview"]);
159
+ if (maxImagePreview)
160
+ meta.maxImagePreview = maxImagePreview;
161
+ const maxVideoPreview = parseRobotsNumber(robots["max-video-preview"]);
162
+ if (maxVideoPreview !== undefined)
163
+ meta.maxVideoPreview = maxVideoPreview;
164
+ return Object.keys(meta).length ? meta : undefined;
165
+ }
166
+ export function isYoastNoindex(yoast) {
167
+ const robots = buildYoastRobotsMeta(yoast?.robots);
168
+ if (!robots || typeof robots === "string")
169
+ return false;
170
+ return robots.index === false;
171
+ }
47
172
  export function getYoastSchema(entity, opts) {
48
- const yoast = getYoastHead(entity, opts);
49
- return yoast?.schema || null;
173
+ const yoast = entity?.yoast_head_json;
174
+ if (!yoast?.schema)
175
+ return null;
176
+ return rewriteYoastSchemaUrls(yoast.schema, opts ?? {}, yoast.canonical) || null;
50
177
  }
51
178
  export function stripBreadcrumbsFromSchema(schema) {
52
179
  if (!schema || typeof schema !== "object")
@@ -4,6 +4,7 @@ import { listPostsWithEmbeds } from "../api/posts";
4
4
  import { getCategoryPaginationStaticParams } from "../base";
5
5
  import { getHomePaginationStaticParams } from "../base/home/pagination";
6
6
  import { getAbsoluteUrl, ensureTrailingSlash } from "./routing";
7
+ import { isYoastNoindex } from "./seo";
7
8
  import { getTranslationKey, getEntityLanguageId, withXDefault, } from "./hreflang";
8
9
  const resolveWpApiUrl = (cfg) => cfg.wpApiUrl ?? process.env.WP_API_URL ?? "";
9
10
  const resolveSiteUrl = (cfg) => cfg.siteUrl ?? process.env.NEXT_PUBLIC_SITE_URL;
@@ -12,6 +13,8 @@ const buildHomeAlternates = (cfg) => withXDefault(cfg.languages.reduce((acc, lan
12
13
  acc[langCfg.code] = getAbsoluteUrl(basePath, resolveSiteUrl(cfg));
13
14
  return acc;
14
15
  }, {}));
16
+ const getYoastHeadJson = (entity) => entity.yoast_head_json;
17
+ const isNoindexEntity = (entity) => isYoastNoindex(getYoastHeadJson(entity));
15
18
  export function renderSitemap(entries) {
16
19
  const items = entries
17
20
  .map((entry) => {
@@ -108,11 +111,12 @@ export async function getCategoryEntries(cfg) {
108
111
  const { categoriesApi } = createWpClient({ baseUrl: wpApiUrl });
109
112
  const categories = await categoriesApi.listAll({
110
113
  ...(langCfg.langId ? { lang: langCfg.code } : {}),
111
- _fields: ["id", "slug", "name", "description", "meta"],
114
+ _fields: ["id", "slug", "name", "description", "meta", "yoast_head_json"],
112
115
  });
113
- categoriesPerLang[langCfg.code] = categories;
116
+ const indexableCategories = categories.filter((cat) => !isNoindexEntity(cat));
117
+ categoriesPerLang[langCfg.code] = indexableCategories;
114
118
  const basePath = ensureTrailingSlash(langCfg.basePath || "/");
115
- for (const cat of categories) {
119
+ for (const cat of indexableCategories) {
116
120
  const translationKey = getTranslationKey(cat);
117
121
  if (!translationKey)
118
122
  continue;
@@ -166,9 +170,10 @@ export async function getCategoryPaginationEntries(cfg) {
166
170
  const { categoriesApi } = createWpClient({ baseUrl: wpApiUrl });
167
171
  const categories = await categoriesApi.listAll({
168
172
  lang: langCfg.code,
169
- _fields: ["id", "slug"],
173
+ _fields: ["id", "slug", "yoast_head_json"],
170
174
  });
171
- catIdsBySlugByLang[langCfg.code] = Object.fromEntries(categories.map((c) => [c.slug, c.id]));
175
+ const indexableCategories = categories.filter((cat) => !isNoindexEntity(cat));
176
+ catIdsBySlugByLang[langCfg.code] = Object.fromEntries(indexableCategories.map((c) => [c.slug, c.id]));
172
177
  }
173
178
  for (const langCfg of languages) {
174
179
  const catPages = await getCategoryPaginationStaticParams({
@@ -181,22 +186,22 @@ export async function getCategoryPaginationEntries(cfg) {
181
186
  for (const page of catPages) {
182
187
  const url = getAbsoluteUrl(`${basePath}${page.category}/page/${page.page}/`, siteUrl);
183
188
  const catId = catIdsBySlugByLang[langCfg.code]?.[page.category];
189
+ if (!catId)
190
+ continue;
184
191
  let lastmod;
185
- if (catId) {
186
- const pagePosts = await listPostsWithEmbeds({
187
- baseApiUrl: wpApiUrl,
188
- categories: [catId],
189
- perPage: runtimeConfig.postsPerPagePagination,
190
- page: Number(page.page),
191
- langId: langCfg.langId,
192
- orderby: "date",
193
- order: "desc",
194
- });
195
- const latest = pagePosts.ok ? pagePosts.data?.items[0] : undefined;
196
- lastmod =
197
- latest?.modified ||
198
- latest?.date;
199
- }
192
+ const pagePosts = await listPostsWithEmbeds({
193
+ baseApiUrl: wpApiUrl,
194
+ categories: [catId],
195
+ perPage: runtimeConfig.postsPerPagePagination,
196
+ page: Number(page.page),
197
+ langId: langCfg.langId,
198
+ orderby: "date",
199
+ order: "desc",
200
+ });
201
+ const latest = pagePosts.ok ? pagePosts.data?.items[0] : undefined;
202
+ lastmod =
203
+ latest?.modified ||
204
+ latest?.date;
200
205
  entries.push({
201
206
  url,
202
207
  lastmod: lastmod ? new Date(lastmod).toISOString() : undefined,
@@ -221,17 +226,21 @@ export async function getPostEntriesForLang(langCode, cfg) {
221
226
  _embed: true,
222
227
  ...languageFilter,
223
228
  });
229
+ const indexablePosts = posts.filter((post) => !isNoindexEntity(post));
224
230
  const postAlternates = {};
225
231
  for (const l of languages) {
226
232
  const base = ensureTrailingSlash(l.basePath || "/");
227
233
  const langPosts = l.code === langCfg.code
228
- ? posts
234
+ ? indexablePosts
229
235
  : await postsApi.listAll({
230
236
  per_page: 100,
231
237
  _embed: true,
232
238
  ...(l.langId ? { taxonomies: { language: [l.langId] } } : {}),
233
239
  });
234
- for (const p of langPosts) {
240
+ const filteredLangPosts = l.code === langCfg.code
241
+ ? langPosts
242
+ : langPosts.filter((post) => !isNoindexEntity(post));
243
+ for (const p of filteredLangPosts) {
235
244
  const translationKey = getTranslationKey(p);
236
245
  if (!translationKey)
237
246
  continue;
@@ -246,7 +255,7 @@ export async function getPostEntriesForLang(langCode, cfg) {
246
255
  }
247
256
  }
248
257
  const basePath = ensureTrailingSlash(langCfg.basePath || "/");
249
- return posts
258
+ return indexablePosts
250
259
  .map((p) => {
251
260
  const category = p._embedded?.["wp:term"]?.[0]?.[0];
252
261
  const categorySlug = category?.slug;
@@ -294,12 +303,14 @@ export async function getLegalEntries(cfg) {
294
303
  "taxonomies",
295
304
  "parent",
296
305
  "language",
306
+ "yoast_head_json",
297
307
  ],
298
308
  });
299
309
  const filtered = pages.filter((p) => getEntityLanguageId(p)?.toString() === langCfg.langId?.toString());
300
- pagesPerLang[langCfg.code] = filtered;
310
+ const indexablePages = filtered.filter((page) => !isNoindexEntity(page));
311
+ pagesPerLang[langCfg.code] = indexablePages;
301
312
  const basePath = ensureTrailingSlash(langCfg.basePath || "/");
302
- for (const p of filtered) {
313
+ for (const p of indexablePages) {
303
314
  const translationKey = getTranslationKey(p);
304
315
  if (!translationKey)
305
316
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wpheadless-lib",
3
- "version": "1.1.7",
3
+ "version": "1.1.10",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "dist/index.js",