@consilioweb/payload-seo-analyzer 1.10.0 → 1.11.0

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
@@ -3441,6 +3441,130 @@ function createAiAltTextHandler(uploadsCollection, seoConfig) {
3441
3441
  };
3442
3442
  }
3443
3443
 
3444
+ // src/endpoints/aiContentBrief.ts
3445
+ var DEFAULT_MODEL3 = "claude-opus-4-8";
3446
+ var trimList = (arr, max, itemMax = 160) => Array.isArray(arr) ? arr.filter((x) => typeof x === "string").map((x) => x.trim()).filter(Boolean).slice(0, max).map((x) => x.length > itemMax ? `${x.slice(0, itemMax - 1)}\u2026` : x) : [];
3447
+ function parseBrief(raw) {
3448
+ let s = raw.trim();
3449
+ if (s.startsWith("```")) s = s.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
3450
+ if (!s.startsWith("{")) {
3451
+ const start = s.indexOf("{");
3452
+ const end = s.lastIndexOf("}");
3453
+ if (start === -1 || end === -1 || end <= start) return null;
3454
+ s = s.slice(start, end + 1);
3455
+ }
3456
+ try {
3457
+ const p = JSON.parse(s);
3458
+ return sanitizeBrief({
3459
+ outline: Array.isArray(p.outline) ? p.outline.map((o) => {
3460
+ const r = o || {};
3461
+ return { level: r.level === "h3" ? "h3" : "h2", text: typeof r.text === "string" ? r.text : "" };
3462
+ }) : [],
3463
+ entities: trimList(p.entities, 30),
3464
+ questions: trimList(p.questions, 15),
3465
+ internalLinkIdeas: trimList(p.internalLinkIdeas, 10),
3466
+ recommendedWordCount: typeof p.recommendedWordCount === "number" ? p.recommendedWordCount : 0,
3467
+ notes: trimList(p.notes, 6)
3468
+ });
3469
+ } catch {
3470
+ return null;
3471
+ }
3472
+ }
3473
+ function sanitizeBrief(b) {
3474
+ return {
3475
+ outline: b.outline.filter((o) => o.text && o.text.trim()).slice(0, 25).map((o) => ({ level: o.level === "h3" ? "h3" : "h2", text: o.text.trim().slice(0, 160) })),
3476
+ entities: trimList(b.entities, 30),
3477
+ questions: trimList(b.questions, 15),
3478
+ internalLinkIdeas: trimList(b.internalLinkIdeas, 10),
3479
+ recommendedWordCount: Math.min(1e4, Math.max(0, Math.round(b.recommendedWordCount || 0))),
3480
+ notes: trimList(b.notes, 6)
3481
+ };
3482
+ }
3483
+ async function callClaudeBrief(apiKey, model, language, params) {
3484
+ const systemPrompt = `You are an SEO content strategist applying June 2026 best practices.
3485
+ Produce a concise WRITING BRIEF for the target keyword so a writer can create a page that ranks AND is citable by AI engines.
3486
+ Rules:
3487
+ - Base the brief on genuine search intent for the keyword; cover entities and questions a complete page must address.
3488
+ - Be specific and non-generic; no filler. Write in ${language === "en" ? "English" : "French"}.
3489
+ - Do not invent facts, brands, prices or statistics.
3490
+ Return ONLY a JSON object (no markdown, no prose) with EXACTLY this shape:
3491
+ {"outline":[{"level":"h2"|"h3","text":string}],"entities":[string],"questions":[string],"internalLinkIdeas":[string],"recommendedWordCount":number,"notes":[string]}
3492
+ - outline: 5-12 headings (logical H2/H3 structure).
3493
+ - entities: 8-20 key terms/concepts to mention.
3494
+ - questions: 4-10 questions the page should answer (People-Also-Ask style).
3495
+ - internalLinkIdeas: 3-8 topics worth linking to internally.
3496
+ - notes: up to 4 short strategic tips.`;
3497
+ const userPrompt = `Target keyword: ${params.keyword}
3498
+ ${params.pageTitle ? `Existing page title: ${params.pageTitle}` : ""}
3499
+ ${params.existingContent ? `Existing content (first 2000 chars, complement it \u2014 don't repeat):
3500
+ ${params.existingContent.substring(0, 2e3)}` : ""}
3501
+
3502
+ Return the JSON brief now:`;
3503
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
3504
+ method: "POST",
3505
+ headers: { "Content-Type": "application/json", "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
3506
+ body: JSON.stringify({
3507
+ model,
3508
+ max_tokens: 1500,
3509
+ system: systemPrompt,
3510
+ messages: [{ role: "user", content: userPrompt }]
3511
+ })
3512
+ });
3513
+ if (!response.ok) {
3514
+ const body = await response.text();
3515
+ throw new Error(`Claude API error ${response.status}: ${body}`);
3516
+ }
3517
+ const data = await response.json();
3518
+ if (data.stop_reason === "refusal") return null;
3519
+ const text = (data.content?.find((b) => b.type === "text")?.text || "").trim();
3520
+ if (!text) return null;
3521
+ return parseBrief(text);
3522
+ }
3523
+ function createAiContentBriefHandler(targetCollections, seoConfig) {
3524
+ return async (req) => {
3525
+ try {
3526
+ if (!req.user) return Response.json({ error: "Unauthorized" }, { status: 401 });
3527
+ const body = await parseJsonBody(req);
3528
+ const keyword = typeof body.keyword === "string" ? body.keyword.trim() : "";
3529
+ if (!keyword) return Response.json({ error: "Missing required field: keyword" }, { status: 400 });
3530
+ const apiKey = process.env.ANTHROPIC_API_KEY;
3531
+ if (!apiKey) {
3532
+ return Response.json(
3533
+ { error: "AI not configured. Set ANTHROPIC_API_KEY to generate a content brief.", code: "no_api_key" },
3534
+ { status: 400 }
3535
+ );
3536
+ }
3537
+ let pageTitle;
3538
+ let existingContent;
3539
+ const collection = typeof body.collection === "string" ? body.collection : void 0;
3540
+ const id = body.id != null ? String(body.id) : void 0;
3541
+ if (collection && id && (!targetCollections || targetCollections.includes(collection))) {
3542
+ try {
3543
+ const doc = await req.payload.findByID({ collection, id, depth: 1, overrideAccess: true });
3544
+ pageTitle = doc.title || void 0;
3545
+ existingContent = extractDocContent(doc).text || void 0;
3546
+ } catch {
3547
+ }
3548
+ }
3549
+ const model = process.env.SEO_AI_MODEL || DEFAULT_MODEL3;
3550
+ const language = seoConfig?.locale === "en" ? "en" : "fr";
3551
+ let brief;
3552
+ try {
3553
+ brief = await callClaudeBrief(apiKey, model, language, { keyword, pageTitle, existingContent });
3554
+ } catch (e) {
3555
+ req.payload.logger.error(`[seo] ai-content-brief Claude error: ${e instanceof Error ? e.message : "unknown"}`);
3556
+ return Response.json({ error: "Content brief generation failed." }, { status: 502 });
3557
+ }
3558
+ if (!brief) return Response.json({ error: "The model did not return a brief (possibly declined)." }, { status: 502 });
3559
+ return Response.json({ keyword, brief, model });
3560
+ } catch (error) {
3561
+ const message = error instanceof Error ? error.message : "Internal server error";
3562
+ req.payload.logger.error(`[seo] ai-content-brief error: ${message}`);
3563
+ return Response.json({ error: message }, { status: 500 });
3564
+ }
3565
+ };
3566
+ }
3567
+
3444
3568
  // src/endpoints/cannibalization.ts
3445
3569
  function canonicalIntent(keyword) {
3446
3570
  return keyword.toLowerCase().normalize("NFD").replace(/\p{Diacritic}/gu, "").replace(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/).filter(Boolean).sort().join(" ");
@@ -5238,24 +5362,46 @@ function buildArticleSchema(doc, siteUrl) {
5238
5362
  }
5239
5363
  return schema;
5240
5364
  }
5241
- function buildLocalBusinessSchema(doc, siteUrl) {
5365
+ function buildLocationNode(loc, doc, siteUrl) {
5242
5366
  const meta = doc.meta || {};
5243
- const schema = {
5244
- "@context": "https://schema.org",
5245
- "@type": "LocalBusiness",
5246
- name: doc.title || meta.title || "",
5247
- description: meta.description || "",
5248
- url: `${siteUrl}/${doc.slug || ""}`
5367
+ const node = {
5368
+ "@type": typeof loc.type === "string" && loc.type || "LocalBusiness",
5369
+ name: loc.name || doc.title || meta.title || "",
5370
+ description: loc.description || meta.description || "",
5371
+ url: loc.url || `${siteUrl}/${doc.slug || ""}`
5249
5372
  };
5250
- if (doc.telephone) schema.telephone = doc.telephone;
5251
- if (doc.email) schema.email = doc.email;
5252
- if (doc.address && typeof doc.address === "object") {
5253
- schema.address = {
5254
- "@type": "PostalAddress",
5255
- ...doc.address
5373
+ if (loc.telephone) node.telephone = loc.telephone;
5374
+ if (loc.email) node.email = loc.email;
5375
+ if (loc.priceRange) node.priceRange = loc.priceRange;
5376
+ const address = loc.address;
5377
+ if (address && typeof address === "object") {
5378
+ node.address = { "@type": "PostalAddress", ...address };
5379
+ } else if (typeof address === "string" && address) {
5380
+ node.address = address;
5381
+ }
5382
+ const geo = loc.geo || {};
5383
+ const lat = geo.latitude ?? loc.latitude ?? loc.lat;
5384
+ const lng = geo.longitude ?? loc.longitude ?? loc.lng;
5385
+ if (lat != null && lng != null) {
5386
+ node.geo = { "@type": "GeoCoordinates", latitude: lat, longitude: lng };
5387
+ }
5388
+ if (Array.isArray(loc.openingHours) && loc.openingHours.length > 0) {
5389
+ node.openingHours = loc.openingHours;
5390
+ } else if (typeof loc.openingHours === "string" && loc.openingHours) {
5391
+ node.openingHours = loc.openingHours;
5392
+ }
5393
+ return node;
5394
+ }
5395
+ function buildLocalBusinessSchema(doc, siteUrl) {
5396
+ const locations = Array.isArray(doc.locations) ? doc.locations.filter((l) => !!l && typeof l === "object") : [];
5397
+ if (locations.length > 1) {
5398
+ return {
5399
+ "@context": "https://schema.org",
5400
+ "@graph": locations.map((loc) => buildLocationNode(loc, doc, siteUrl))
5256
5401
  };
5257
5402
  }
5258
- return schema;
5403
+ const base = locations.length === 1 ? locations[0] : doc;
5404
+ return { "@context": "https://schema.org", ...buildLocationNode(base, doc, siteUrl) };
5259
5405
  }
5260
5406
  function buildBreadcrumbSchema(doc, siteUrl) {
5261
5407
  const slug = doc.slug || "";
@@ -6006,6 +6152,191 @@ function createSitemapHandler(targetCollections) {
6006
6152
  };
6007
6153
  }
6008
6154
 
6155
+ // src/endpoints/sitemapExtensions.ts
6156
+ function escapeXml2(str) {
6157
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
6158
+ }
6159
+ function resolveSiteUrl3(seoConfig) {
6160
+ return (seoConfig?.siteUrl || process.env.NEXT_PUBLIC_SERVER_URL || process.env.PAYLOAD_PUBLIC_SERVER_URL || "").replace(/\/$/, "");
6161
+ }
6162
+ function docPath(slug) {
6163
+ return slug === "home" || slug === "" ? "" : `/${slug}`;
6164
+ }
6165
+ function xmlResponse(xml, status = 200) {
6166
+ return new Response(xml, {
6167
+ status,
6168
+ headers: { "Content-Type": "application/xml", "Cache-Control": "public, max-age=3600, s-maxage=3600" }
6169
+ });
6170
+ }
6171
+ function mediaUrl(media, siteUrl) {
6172
+ if (typeof media.url === "string" && media.url) {
6173
+ return media.url.startsWith("http") ? media.url : `${siteUrl}${media.url}`;
6174
+ }
6175
+ if (typeof media.filename === "string" && media.filename) {
6176
+ return `${siteUrl}/media/${media.filename}`;
6177
+ }
6178
+ return void 0;
6179
+ }
6180
+ function collectMediaUrls(node, mimePrefix, siteUrl, out, depth = 0) {
6181
+ if (!node || typeof node !== "object" || depth > 8) return;
6182
+ if (Array.isArray(node)) {
6183
+ for (const item of node) collectMediaUrls(item, mimePrefix, siteUrl, out, depth + 1);
6184
+ return;
6185
+ }
6186
+ const obj = node;
6187
+ const mime = typeof obj.mimeType === "string" ? obj.mimeType : "";
6188
+ if (mime.startsWith(mimePrefix)) {
6189
+ const url = mediaUrl(obj, siteUrl);
6190
+ if (url) out.add(url);
6191
+ }
6192
+ for (const key of Object.keys(obj)) {
6193
+ if (key === "sizes" || key === "_status") continue;
6194
+ collectMediaUrls(obj[key], mimePrefix, siteUrl, out, depth + 1);
6195
+ }
6196
+ }
6197
+ async function eachPublishedDoc(payload, collections, depth, onDoc) {
6198
+ const BATCH = Math.min(100, Math.max(1, parseInt(process.env.SEO_SITEMAP_BATCH_SIZE || "50", 10) || 50));
6199
+ const MAX = Math.max(1, parseInt(process.env.SEO_SITEMAP_MAX_DOCS || "5000", 10) || 5e3);
6200
+ let count = 0;
6201
+ for (const collection of collections) {
6202
+ try {
6203
+ let page = 1;
6204
+ let hasMore = true;
6205
+ while (hasMore) {
6206
+ const res = await payload.find({ collection, limit: BATCH, page, depth, overrideAccess: true });
6207
+ for (const doc of res.docs) {
6208
+ if (doc._status === "draft") continue;
6209
+ if (count >= MAX) return;
6210
+ onDoc(doc, collection);
6211
+ count++;
6212
+ }
6213
+ hasMore = res.hasNextPage;
6214
+ page++;
6215
+ await new Promise((resolve) => setImmediate(resolve));
6216
+ }
6217
+ } catch {
6218
+ }
6219
+ }
6220
+ }
6221
+ function createNewsSitemapHandler(targetCollections, seoConfig) {
6222
+ return async (req) => {
6223
+ try {
6224
+ const siteUrl = resolveSiteUrl3(seoConfig);
6225
+ const language = seoConfig?.locale === "en" ? "en" : "fr";
6226
+ let publication = seoConfig?.siteName || "";
6227
+ if (!publication && siteUrl) {
6228
+ try {
6229
+ publication = new URL(siteUrl).hostname;
6230
+ } catch {
6231
+ }
6232
+ }
6233
+ const cutoff = Date.now() - 48 * 36e5;
6234
+ const entries = [];
6235
+ await eachPublishedDoc(req.payload, targetCollections, 0, (doc) => {
6236
+ const dateStr = typeof doc.publishedAt === "string" && doc.publishedAt || typeof doc.date === "string" && doc.date || typeof doc.createdAt === "string" && doc.createdAt || "";
6237
+ if (!dateStr) return;
6238
+ const t = new Date(dateStr).getTime();
6239
+ if (isNaN(t) || t < cutoff) return;
6240
+ const title = doc.title || doc.meta?.title || "";
6241
+ if (!title) return;
6242
+ const loc = `${siteUrl}${docPath(doc.slug || "")}`;
6243
+ entries.push(
6244
+ ` <url>
6245
+ <loc>${escapeXml2(loc)}</loc>
6246
+ <news:news>
6247
+ <news:publication>
6248
+ <news:name>${escapeXml2(publication)}</news:name>
6249
+ <news:language>${language}</news:language>
6250
+ </news:publication>
6251
+ <news:publication_date>${new Date(dateStr).toISOString()}</news:publication_date>
6252
+ <news:title>${escapeXml2(title)}</news:title>
6253
+ </news:news>
6254
+ </url>`
6255
+ );
6256
+ });
6257
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
6258
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">
6259
+ ${entries.join("\n")}
6260
+ </urlset>`;
6261
+ return xmlResponse(xml);
6262
+ } catch (error) {
6263
+ req.payload.logger.error(`[seo] sitemap-news error: ${error instanceof Error ? error.message : "unknown"}`);
6264
+ return xmlResponse('<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>', 500);
6265
+ }
6266
+ };
6267
+ }
6268
+ function createImageSitemapHandler(targetCollections, seoConfig) {
6269
+ return async (req) => {
6270
+ try {
6271
+ const siteUrl = resolveSiteUrl3(seoConfig);
6272
+ const entries = [];
6273
+ await eachPublishedDoc(req.payload, targetCollections, 1, (doc) => {
6274
+ const urls = /* @__PURE__ */ new Set();
6275
+ collectMediaUrls(doc, "image/", siteUrl, urls);
6276
+ if (urls.size === 0) return;
6277
+ const loc = `${siteUrl}${docPath(doc.slug || "")}`;
6278
+ const imgs = Array.from(urls).slice(0, 1e3).map((u) => ` <image:image><image:loc>${escapeXml2(u)}</image:loc></image:image>`).join("\n");
6279
+ entries.push(` <url>
6280
+ <loc>${escapeXml2(loc)}</loc>
6281
+ ${imgs}
6282
+ </url>`);
6283
+ });
6284
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
6285
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
6286
+ ${entries.join("\n")}
6287
+ </urlset>`;
6288
+ return xmlResponse(xml);
6289
+ } catch (error) {
6290
+ req.payload.logger.error(`[seo] sitemap-images error: ${error instanceof Error ? error.message : "unknown"}`);
6291
+ return xmlResponse('<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>', 500);
6292
+ }
6293
+ };
6294
+ }
6295
+ function createVideoSitemapHandler(targetCollections, seoConfig) {
6296
+ return async (req) => {
6297
+ try {
6298
+ const siteUrl = resolveSiteUrl3(seoConfig);
6299
+ const entries = [];
6300
+ await eachPublishedDoc(req.payload, targetCollections, 1, (doc) => {
6301
+ const meta = doc.meta || {};
6302
+ const videoUrls = /* @__PURE__ */ new Set();
6303
+ collectMediaUrls(doc, "video/", siteUrl, videoUrls);
6304
+ for (const k of ["videoUrl", "contentUrl", "playerUrl"]) {
6305
+ if (typeof doc[k] === "string" && doc[k]) videoUrls.add(doc[k]);
6306
+ }
6307
+ if (videoUrls.size === 0) return;
6308
+ const title = doc.title || meta.title || "";
6309
+ const description = meta.description || title;
6310
+ const thumbs = /* @__PURE__ */ new Set();
6311
+ collectMediaUrls(meta.image, "image/", siteUrl, thumbs);
6312
+ if (thumbs.size === 0) collectMediaUrls(doc, "image/", siteUrl, thumbs);
6313
+ const thumbnail = Array.from(thumbs)[0] || "";
6314
+ const loc = `${siteUrl}${docPath(doc.slug || "")}`;
6315
+ const videos = Array.from(videoUrls).slice(0, 100).map(
6316
+ (u) => ` <video:video>
6317
+ ${thumbnail ? ` <video:thumbnail_loc>${escapeXml2(thumbnail)}</video:thumbnail_loc>
6318
+ ` : ""} <video:title>${escapeXml2(title || "Video")}</video:title>
6319
+ <video:description>${escapeXml2(description || title || "Video")}</video:description>
6320
+ <video:content_loc>${escapeXml2(u)}</video:content_loc>
6321
+ </video:video>`
6322
+ ).join("\n");
6323
+ entries.push(` <url>
6324
+ <loc>${escapeXml2(loc)}</loc>
6325
+ ${videos}
6326
+ </url>`);
6327
+ });
6328
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
6329
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
6330
+ ${entries.join("\n")}
6331
+ </urlset>`;
6332
+ return xmlResponse(xml);
6333
+ } catch (error) {
6334
+ req.payload.logger.error(`[seo] sitemap-video error: ${error instanceof Error ? error.message : "unknown"}`);
6335
+ return xmlResponse('<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>', 500);
6336
+ }
6337
+ };
6338
+ }
6339
+
6009
6340
  // src/collections/SeoScoreHistory.ts
6010
6341
  function createSeoScoreHistoryCollection() {
6011
6342
  return {
@@ -8494,6 +8825,9 @@ var fr = {
8494
8825
  pagesAnalyzed: "pages analys\xE9es",
8495
8826
  markCornerstone: "Marquer pilier",
8496
8827
  unmarkCornerstone: "D\xE9marquer pilier",
8828
+ bulkOptimizeMeta: "Optimiser m\xE9ta (IA)",
8829
+ bulkOptimizing: "Optimisation\u2026",
8830
+ bulkConfirm: "Confirmer ?",
8497
8831
  searchPlaceholder: "Rechercher (titre, slug, keyword)...",
8498
8832
  allCollections: "Toutes les collections",
8499
8833
  allScores: "Tous les scores",
@@ -9086,6 +9420,9 @@ var en = {
9086
9420
  pagesAnalyzed: "pages analyzed",
9087
9421
  markCornerstone: "Mark as cornerstone",
9088
9422
  unmarkCornerstone: "Unmark cornerstone",
9423
+ bulkOptimizeMeta: "Optimize meta (AI)",
9424
+ bulkOptimizing: "Optimizing\u2026",
9425
+ bulkConfirm: "Confirm?",
9089
9426
  searchPlaceholder: "Search (title, slug, keyword)...",
9090
9427
  allCollections: "All collections",
9091
9428
  allScores: "All scores",
@@ -9937,7 +10274,8 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
9937
10274
  { path: `${basePath}/ai-rewrite`, method: "post", handler: createAiRewriteHandler(targetCollections) },
9938
10275
  { path: `${basePath}/ai-optimize`, method: "post", handler: createAiOptimizeHandler(targetCollections, seoConfig) },
9939
10276
  { path: `${basePath}/alt-text-audit`, method: "get", handler: createAltTextAuditHandler(uploadsCollection) },
9940
- { path: `${basePath}/ai-alt-text`, method: "post", handler: withRateLimit(createAiAltTextHandler(uploadsCollection, seoConfig)) }
10277
+ { path: `${basePath}/ai-alt-text`, method: "post", handler: withRateLimit(createAiAltTextHandler(uploadsCollection, seoConfig)) },
10278
+ { path: `${basePath}/ai-content-brief`, method: "post", handler: withRateLimit(createAiContentBriefHandler(targetCollections, seoConfig)) }
9941
10279
  );
9942
10280
  }
9943
10281
  if (features.cannibalization) {
@@ -10029,6 +10367,21 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
10029
10367
  path: `${basePath}/sitemap.xml`,
10030
10368
  method: "get",
10031
10369
  handler: createSitemapHandler(targetCollections)
10370
+ },
10371
+ {
10372
+ path: `${basePath}/sitemap-news.xml`,
10373
+ method: "get",
10374
+ handler: createNewsSitemapHandler(targetCollections, seoConfig)
10375
+ },
10376
+ {
10377
+ path: `${basePath}/sitemap-images.xml`,
10378
+ method: "get",
10379
+ handler: createImageSitemapHandler(targetCollections, seoConfig)
10380
+ },
10381
+ {
10382
+ path: `${basePath}/sitemap-video.xml`,
10383
+ method: "get",
10384
+ handler: createVideoSitemapHandler(targetCollections, seoConfig)
10032
10385
  }
10033
10386
  );
10034
10387
  config.endpoints = [
@@ -10133,7 +10486,7 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
10133
10486
  };
10134
10487
 
10135
10488
  // src/helpers/buildMetadata.ts
10136
- function resolveSiteUrl3(explicit) {
10489
+ function resolveSiteUrl4(explicit) {
10137
10490
  return (explicit || process.env.NEXT_PUBLIC_SERVER_URL || process.env.PAYLOAD_PUBLIC_SERVER_URL || "").replace(/\/$/, "");
10138
10491
  }
10139
10492
  function parseRobots(doc, meta) {
@@ -10167,7 +10520,7 @@ function absoluteUrl(value, siteUrl) {
10167
10520
  return `${siteUrl}${value.startsWith("/") ? "" : "/"}${value}`;
10168
10521
  }
10169
10522
  function buildSeoMetadata(doc, options = {}) {
10170
- const siteUrl = resolveSiteUrl3(options.siteUrl);
10523
+ const siteUrl = resolveSiteUrl4(options.siteUrl);
10171
10524
  const meta = doc.meta || {};
10172
10525
  const rawTitle = meta.title || doc.title || "";
10173
10526
  const title = options.titleTemplate && rawTitle ? options.titleTemplate.replace("%s", rawTitle) : rawTitle;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consilioweb/payload-seo-analyzer",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "Payload CMS SEO plugin — 50+ checks, dashboard, Lexical JSON support, Flesch FR/EN readability, i18n",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",