@consilioweb/payload-seo-analyzer 1.10.0 → 1.12.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
@@ -1703,6 +1703,7 @@ async function buildAuditCache(payload, collections, globals, seoConfig) {
1703
1703
  });
1704
1704
  for (const doc of result.docs) {
1705
1705
  if (ignoredSlugs.includes(doc.slug)) continue;
1706
+ if (doc._status === "draft") continue;
1706
1707
  if (allResults.length >= MAX_DOCS2) {
1707
1708
  capped = true;
1708
1709
  break collectionsLoop;
@@ -2678,6 +2679,9 @@ function createRedirectsHandler(redirectsCollection) {
2678
2679
  return Response.json({ error: "Missing id or ids" }, { status: 400 });
2679
2680
  }
2680
2681
  if (method === "PATCH") {
2682
+ if (!isAdmin3(req.user)) {
2683
+ return Response.json({ error: "Admin access required" }, { status: 403 });
2684
+ }
2681
2685
  const patchBody = await parseJsonBody(req);
2682
2686
  const id = typeof patchBody.id === "string" ? patchBody.id.trim() : void 0;
2683
2687
  const from = typeof patchBody.from === "string" ? patchBody.from.trim() : void 0;
@@ -3019,6 +3023,77 @@ function sanitizeSuggestions(s, currentFocusKeyword) {
3019
3023
  const rationale = s.rationale.map((r) => r.trim()).filter(Boolean).slice(0, RATIONALE_MAX_ITEMS).map((r) => r.length > RATIONALE_ITEM_MAX ? `${r.slice(0, RATIONALE_ITEM_MAX - 1)}\u2026` : r);
3020
3024
  return { metaTitle, metaDescription, focusKeyword, rationale };
3021
3025
  }
3026
+ async function optimizeDocMeta(payload, opts) {
3027
+ const { collection, id, mergedConfig } = opts;
3028
+ let doc;
3029
+ try {
3030
+ doc = await payload.findByID({ collection, id, depth: 1, overrideAccess: true });
3031
+ } catch {
3032
+ return { ok: false, error: `Document not found: ${collection}/${id}`, status: 404 };
3033
+ }
3034
+ const seoInput = buildSeoInputFromDoc(doc, collection);
3035
+ const analysis = analyzeSeo(seoInput, mergedConfig);
3036
+ const metaGroups = /* @__PURE__ */ new Set(["title", "meta-description", "content", "url", "headings"]);
3037
+ const issues = analysis.checks.filter((c) => (c.status === "fail" || c.status === "warning") && metaGroups.has(c.group)).map((c) => ({ label: c.label, message: c.message })).slice(0, 12);
3038
+ const pageTitle = doc.title || "";
3039
+ const slug = doc.slug || "";
3040
+ const currentFocusKeyword = doc.focusKeyword || "";
3041
+ const currentMetaTitle = seoInput.metaTitle || "";
3042
+ const currentMetaDescription = seoInput.metaDescription || "";
3043
+ const content = extractDocContent(doc).text;
3044
+ const apiKey = opts.apiKey;
3045
+ const model = opts.model || DEFAULT_MODEL;
3046
+ let suggestions;
3047
+ let method;
3048
+ const heuristicFallback = () => ({
3049
+ metaTitle: generateMetaTitle(pageTitle, currentFocusKeyword, slug),
3050
+ metaDescription: generateMetaDescription(content, currentFocusKeyword, slug),
3051
+ focusKeyword: currentFocusKeyword,
3052
+ rationale: []
3053
+ });
3054
+ if (apiKey) {
3055
+ try {
3056
+ const aiResult = await callClaudeOptimize(apiKey, model, {
3057
+ pageTitle,
3058
+ slug,
3059
+ focusKeyword: currentFocusKeyword,
3060
+ currentMetaTitle,
3061
+ currentMetaDescription,
3062
+ issues,
3063
+ content
3064
+ });
3065
+ if (aiResult) {
3066
+ suggestions = aiResult;
3067
+ method = "ai";
3068
+ } else {
3069
+ suggestions = heuristicFallback();
3070
+ method = "heuristic";
3071
+ }
3072
+ } catch (error) {
3073
+ payload.logger.error(`[seo] ai-optimize Claude API error: ${error instanceof Error ? error.message : "unknown"}`);
3074
+ suggestions = heuristicFallback();
3075
+ method = "heuristic";
3076
+ }
3077
+ } else {
3078
+ suggestions = heuristicFallback();
3079
+ method = "heuristic";
3080
+ }
3081
+ const sanitized = sanitizeSuggestions(suggestions, currentFocusKeyword);
3082
+ return {
3083
+ ok: true,
3084
+ title: pageTitle,
3085
+ method,
3086
+ ...method === "ai" ? { model } : {},
3087
+ score: analysis.score,
3088
+ current: {
3089
+ metaTitle: currentMetaTitle,
3090
+ metaDescription: currentMetaDescription,
3091
+ focusKeyword: currentFocusKeyword
3092
+ },
3093
+ suggestions: sanitized,
3094
+ issues
3095
+ };
3096
+ }
3022
3097
  function createAiOptimizeHandler(targetCollections, seoConfig, localeMapping) {
3023
3098
  return async (req) => {
3024
3099
  try {
@@ -3034,83 +3109,27 @@ function createAiOptimizeHandler(targetCollections, seoConfig, localeMapping) {
3034
3109
  if (targetCollections && !targetCollections.includes(collection)) {
3035
3110
  return Response.json({ error: "Collection not allowed" }, { status: 403 });
3036
3111
  }
3037
- let doc;
3038
- try {
3039
- const result = await req.payload.findByID({
3040
- collection,
3041
- id,
3042
- depth: 1,
3043
- overrideAccess: true
3044
- });
3045
- doc = result;
3046
- } catch {
3047
- return Response.json({ error: `Document not found: ${collection}/${id}` }, { status: 404 });
3048
- }
3049
3112
  const { config: mergedConfig } = await loadMergedConfig(req.payload, seoConfig, {
3050
3113
  reqLocale: req.locale,
3051
3114
  localeMapping
3052
3115
  });
3053
- const seoInput = buildSeoInputFromDoc(doc, collection);
3054
- const analysis = analyzeSeo(seoInput, mergedConfig);
3055
- const metaGroups = /* @__PURE__ */ new Set(["title", "meta-description", "content", "url", "headings"]);
3056
- const issues = analysis.checks.filter((c) => (c.status === "fail" || c.status === "warning") && metaGroups.has(c.group)).map((c) => ({ label: c.label, message: c.message })).slice(0, 12);
3057
- const pageTitle = doc.title || "";
3058
- const slug = doc.slug || "";
3059
- const currentFocusKeyword = doc.focusKeyword || "";
3060
- const currentMetaTitle = seoInput.metaTitle || "";
3061
- const currentMetaDescription = seoInput.metaDescription || "";
3062
- const content = extractDocContent(doc).text;
3063
- const apiKey = process.env.ANTHROPIC_API_KEY;
3064
- const model = process.env.SEO_AI_MODEL || DEFAULT_MODEL;
3065
- let suggestions;
3066
- let method;
3067
- const heuristicFallback = () => ({
3068
- metaTitle: generateMetaTitle(pageTitle, currentFocusKeyword, slug),
3069
- metaDescription: generateMetaDescription(content, currentFocusKeyword, slug),
3070
- focusKeyword: currentFocusKeyword,
3071
- rationale: []
3116
+ const r = await optimizeDocMeta(req.payload, {
3117
+ collection,
3118
+ id,
3119
+ mergedConfig,
3120
+ apiKey: process.env.ANTHROPIC_API_KEY,
3121
+ model: process.env.SEO_AI_MODEL
3072
3122
  });
3073
- if (apiKey) {
3074
- try {
3075
- const aiResult = await callClaudeOptimize(apiKey, model, {
3076
- pageTitle,
3077
- slug,
3078
- focusKeyword: currentFocusKeyword,
3079
- currentMetaTitle,
3080
- currentMetaDescription,
3081
- issues,
3082
- content
3083
- });
3084
- if (aiResult) {
3085
- suggestions = aiResult;
3086
- method = "ai";
3087
- } else {
3088
- suggestions = heuristicFallback();
3089
- method = "heuristic";
3090
- }
3091
- } catch (error) {
3092
- req.payload.logger.error(
3093
- `[seo] ai-optimize Claude API error: ${error instanceof Error ? error.message : "unknown"}`
3094
- );
3095
- suggestions = heuristicFallback();
3096
- method = "heuristic";
3097
- }
3098
- } else {
3099
- suggestions = heuristicFallback();
3100
- method = "heuristic";
3123
+ if (!r.ok) {
3124
+ return Response.json({ error: r.error }, { status: r.status || 500 });
3101
3125
  }
3102
- const sanitized = sanitizeSuggestions(suggestions, currentFocusKeyword);
3103
3126
  return Response.json({
3104
- method,
3105
- ...method === "ai" ? { model } : {},
3106
- score: analysis.score,
3107
- current: {
3108
- metaTitle: currentMetaTitle,
3109
- metaDescription: currentMetaDescription,
3110
- focusKeyword: currentFocusKeyword
3111
- },
3112
- suggestions: sanitized,
3113
- issues
3127
+ method: r.method,
3128
+ ...r.method === "ai" ? { model: r.model } : {},
3129
+ score: r.score,
3130
+ current: r.current,
3131
+ suggestions: r.suggestions,
3132
+ issues: r.issues
3114
3133
  });
3115
3134
  } catch (error) {
3116
3135
  const message = error instanceof Error ? error.message : "Internal server error";
@@ -3441,6 +3460,301 @@ function createAiAltTextHandler(uploadsCollection, seoConfig) {
3441
3460
  };
3442
3461
  }
3443
3462
 
3463
+ // src/endpoints/aiContentBrief.ts
3464
+ var DEFAULT_MODEL3 = "claude-opus-4-8";
3465
+ 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) : [];
3466
+ function parseBrief(raw) {
3467
+ let s = raw.trim();
3468
+ if (s.startsWith("```")) s = s.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
3469
+ if (!s.startsWith("{")) {
3470
+ const start = s.indexOf("{");
3471
+ const end = s.lastIndexOf("}");
3472
+ if (start === -1 || end === -1 || end <= start) return null;
3473
+ s = s.slice(start, end + 1);
3474
+ }
3475
+ try {
3476
+ const p = JSON.parse(s);
3477
+ return sanitizeBrief({
3478
+ outline: Array.isArray(p.outline) ? p.outline.map((o) => {
3479
+ const r = o || {};
3480
+ return { level: r.level === "h3" ? "h3" : "h2", text: typeof r.text === "string" ? r.text : "" };
3481
+ }) : [],
3482
+ entities: trimList(p.entities, 30),
3483
+ questions: trimList(p.questions, 15),
3484
+ internalLinkIdeas: trimList(p.internalLinkIdeas, 10),
3485
+ recommendedWordCount: typeof p.recommendedWordCount === "number" ? p.recommendedWordCount : 0,
3486
+ notes: trimList(p.notes, 6)
3487
+ });
3488
+ } catch {
3489
+ return null;
3490
+ }
3491
+ }
3492
+ function sanitizeBrief(b) {
3493
+ return {
3494
+ 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) })),
3495
+ entities: trimList(b.entities, 30),
3496
+ questions: trimList(b.questions, 15),
3497
+ internalLinkIdeas: trimList(b.internalLinkIdeas, 10),
3498
+ recommendedWordCount: Math.min(1e4, Math.max(0, Math.round(b.recommendedWordCount || 0))),
3499
+ notes: trimList(b.notes, 6)
3500
+ };
3501
+ }
3502
+ async function callClaudeBrief(apiKey, model, language, params) {
3503
+ const systemPrompt = `You are an SEO content strategist applying June 2026 best practices.
3504
+ Produce a concise WRITING BRIEF for the target keyword so a writer can create a page that ranks AND is citable by AI engines.
3505
+ Rules:
3506
+ - Base the brief on genuine search intent for the keyword; cover entities and questions a complete page must address.
3507
+ - Be specific and non-generic; no filler. Write in ${language === "en" ? "English" : "French"}.
3508
+ - Do not invent facts, brands, prices or statistics.
3509
+ Return ONLY a JSON object (no markdown, no prose) with EXACTLY this shape:
3510
+ {"outline":[{"level":"h2"|"h3","text":string}],"entities":[string],"questions":[string],"internalLinkIdeas":[string],"recommendedWordCount":number,"notes":[string]}
3511
+ - outline: 5-12 headings (logical H2/H3 structure).
3512
+ - entities: 8-20 key terms/concepts to mention.
3513
+ - questions: 4-10 questions the page should answer (People-Also-Ask style).
3514
+ - internalLinkIdeas: 3-8 topics worth linking to internally.
3515
+ - notes: up to 4 short strategic tips.`;
3516
+ const userPrompt = `Target keyword: ${params.keyword}
3517
+ ${params.pageTitle ? `Existing page title: ${params.pageTitle}` : ""}
3518
+ ${params.existingContent ? `Existing content (first 2000 chars, complement it \u2014 don't repeat):
3519
+ ${params.existingContent.substring(0, 2e3)}` : ""}
3520
+
3521
+ Return the JSON brief now:`;
3522
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
3523
+ method: "POST",
3524
+ headers: { "Content-Type": "application/json", "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
3525
+ body: JSON.stringify({
3526
+ model,
3527
+ max_tokens: 1500,
3528
+ system: systemPrompt,
3529
+ messages: [{ role: "user", content: userPrompt }]
3530
+ })
3531
+ });
3532
+ if (!response.ok) {
3533
+ const body = await response.text();
3534
+ throw new Error(`Claude API error ${response.status}: ${body}`);
3535
+ }
3536
+ const data = await response.json();
3537
+ if (data.stop_reason === "refusal") return null;
3538
+ const text = (data.content?.find((b) => b.type === "text")?.text || "").trim();
3539
+ if (!text) return null;
3540
+ return parseBrief(text);
3541
+ }
3542
+ function createAiContentBriefHandler(targetCollections, seoConfig) {
3543
+ return async (req) => {
3544
+ try {
3545
+ if (!req.user) return Response.json({ error: "Unauthorized" }, { status: 401 });
3546
+ const body = await parseJsonBody(req);
3547
+ const keyword = typeof body.keyword === "string" ? body.keyword.trim() : "";
3548
+ if (!keyword) return Response.json({ error: "Missing required field: keyword" }, { status: 400 });
3549
+ const apiKey = process.env.ANTHROPIC_API_KEY;
3550
+ if (!apiKey) {
3551
+ return Response.json(
3552
+ { error: "AI not configured. Set ANTHROPIC_API_KEY to generate a content brief.", code: "no_api_key" },
3553
+ { status: 400 }
3554
+ );
3555
+ }
3556
+ let pageTitle;
3557
+ let existingContent;
3558
+ const collection = typeof body.collection === "string" ? body.collection : void 0;
3559
+ const id = body.id != null ? String(body.id) : void 0;
3560
+ if (collection && id && (!targetCollections || targetCollections.includes(collection))) {
3561
+ try {
3562
+ const doc = await req.payload.findByID({ collection, id, depth: 1, overrideAccess: true });
3563
+ pageTitle = doc.title || void 0;
3564
+ existingContent = extractDocContent(doc).text || void 0;
3565
+ } catch {
3566
+ }
3567
+ }
3568
+ const model = process.env.SEO_AI_MODEL || DEFAULT_MODEL3;
3569
+ const language = seoConfig?.locale === "en" ? "en" : "fr";
3570
+ let brief;
3571
+ try {
3572
+ brief = await callClaudeBrief(apiKey, model, language, { keyword, pageTitle, existingContent });
3573
+ } catch (e) {
3574
+ req.payload.logger.error(`[seo] ai-content-brief Claude error: ${e instanceof Error ? e.message : "unknown"}`);
3575
+ return Response.json({ error: "Content brief generation failed." }, { status: 502 });
3576
+ }
3577
+ if (!brief) return Response.json({ error: "The model did not return a brief (possibly declined)." }, { status: 502 });
3578
+ return Response.json({ keyword, brief, model });
3579
+ } catch (error) {
3580
+ const message = error instanceof Error ? error.message : "Internal server error";
3581
+ req.payload.logger.error(`[seo] ai-content-brief error: ${message}`);
3582
+ return Response.json({ error: message }, { status: 500 });
3583
+ }
3584
+ };
3585
+ }
3586
+
3587
+ // src/endpoints/aiOptimizeBulk.ts
3588
+ var TITLE_MAX = 70;
3589
+ var DESC_MAX = 160;
3590
+ var KEYWORD_MAX2 = 60;
3591
+ function isAdmin5(user) {
3592
+ if (!user) return false;
3593
+ if (user.role === "admin") return true;
3594
+ if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
3595
+ return false;
3596
+ }
3597
+ function createAiOptimizeBulkHandler(targetCollections, seoConfig, localeMapping) {
3598
+ return async (req) => {
3599
+ try {
3600
+ if (!isAdmin5(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
3601
+ const body = await parseJsonBody(req);
3602
+ const rawIds = Array.isArray(body.ids) ? body.ids.map(String) : [];
3603
+ const defaultCollection = typeof body.collection === "string" ? body.collection : void 0;
3604
+ const apply = body.apply === true;
3605
+ const MAX = 100;
3606
+ const limit = Math.min(MAX, Math.max(1, parseInt(String(body.limit ?? 50), 10) || 50));
3607
+ const corrections = Array.isArray(body.corrections) ? body.corrections : null;
3608
+ if (apply && corrections && corrections.length > 0) {
3609
+ let applied2 = 0;
3610
+ let i2 = 0;
3611
+ const rows = [];
3612
+ for (const c of corrections.slice(0, MAX)) {
3613
+ i2++;
3614
+ const collection = typeof c.collection === "string" ? c.collection : defaultCollection;
3615
+ const id = c.id != null ? String(c.id) : void 0;
3616
+ if (!collection || !id || collection.startsWith("global:")) continue;
3617
+ if (targetCollections && !targetCollections.includes(collection)) continue;
3618
+ const metaTitle = typeof c.metaTitle === "string" ? truncateWords(c.metaTitle.trim(), TITLE_MAX) : "";
3619
+ const metaDescription = typeof c.metaDescription === "string" ? truncateWords(c.metaDescription.trim(), DESC_MAX) : "";
3620
+ const focusKeyword = typeof c.focusKeyword === "string" ? c.focusKeyword.trim().slice(0, KEYWORD_MAX2) : "";
3621
+ const patch = {};
3622
+ if (metaTitle || metaDescription) patch.meta = { title: metaTitle, description: metaDescription };
3623
+ if (focusKeyword) patch.focusKeyword = focusKeyword;
3624
+ try {
3625
+ if (Object.keys(patch).length > 0) {
3626
+ await req.payload.update({ collection, id, data: patch, overrideAccess: true });
3627
+ applied2++;
3628
+ rows.push({ collection, id, applied: true });
3629
+ }
3630
+ } catch (e) {
3631
+ rows.push({ collection, id, applied: false, error: e instanceof Error ? e.message : "error" });
3632
+ }
3633
+ if (i2 % 5 === 0) await new Promise((resolve) => setImmediate(resolve));
3634
+ }
3635
+ return Response.json(
3636
+ { processed: rows.length, applied: applied2, mode: "corrections", results: rows },
3637
+ { headers: { "Cache-Control": "no-store" } }
3638
+ );
3639
+ }
3640
+ if (rawIds.length === 0) {
3641
+ return Response.json({ error: 'Provide a non-empty "ids" array' }, { status: 400 });
3642
+ }
3643
+ const targets = [];
3644
+ for (const raw of rawIds) {
3645
+ let collection;
3646
+ let id;
3647
+ if (raw.includes("::")) {
3648
+ const [c, i2] = raw.split("::");
3649
+ collection = c;
3650
+ id = i2;
3651
+ } else {
3652
+ collection = defaultCollection;
3653
+ id = raw;
3654
+ }
3655
+ if (!collection || !id) continue;
3656
+ if (collection.startsWith("global:")) continue;
3657
+ if (targetCollections && !targetCollections.includes(collection)) continue;
3658
+ targets.push({ collection, id });
3659
+ }
3660
+ const capped = targets.length > limit;
3661
+ const slice = targets.slice(0, limit);
3662
+ const { config: mergedConfig } = await loadMergedConfig(req.payload, seoConfig, {
3663
+ reqLocale: req.locale,
3664
+ localeMapping
3665
+ });
3666
+ const apiKey = process.env.ANTHROPIC_API_KEY;
3667
+ const model = process.env.SEO_AI_MODEL;
3668
+ const results = [];
3669
+ let applied = 0;
3670
+ let i = 0;
3671
+ for (const target of slice) {
3672
+ i++;
3673
+ try {
3674
+ const r = await optimizeDocMeta(req.payload, {
3675
+ collection: target.collection,
3676
+ id: target.id,
3677
+ mergedConfig,
3678
+ apiKey,
3679
+ model
3680
+ });
3681
+ if (!r.ok || !r.current || !r.suggestions) {
3682
+ results.push({
3683
+ collection: target.collection,
3684
+ id: target.id,
3685
+ title: "",
3686
+ before: { metaTitle: "", metaDescription: "", focusKeyword: "" },
3687
+ after: { metaTitle: "", metaDescription: "", focusKeyword: "" },
3688
+ changed: false,
3689
+ applied: false,
3690
+ error: r.error || "optimize_failed"
3691
+ });
3692
+ continue;
3693
+ }
3694
+ const before = r.current;
3695
+ const after = {
3696
+ metaTitle: r.suggestions.metaTitle,
3697
+ metaDescription: r.suggestions.metaDescription,
3698
+ focusKeyword: r.suggestions.focusKeyword
3699
+ };
3700
+ const changed = after.metaTitle !== before.metaTitle || after.metaDescription !== before.metaDescription || after.focusKeyword !== before.focusKeyword;
3701
+ let didApply = false;
3702
+ if (apply && changed) {
3703
+ const patch = {};
3704
+ if (after.metaTitle || after.metaDescription) {
3705
+ patch.meta = { title: after.metaTitle, description: after.metaDescription };
3706
+ }
3707
+ if (after.focusKeyword && after.focusKeyword !== before.focusKeyword) {
3708
+ patch.focusKeyword = after.focusKeyword;
3709
+ }
3710
+ if (Object.keys(patch).length > 0) {
3711
+ await req.payload.update({ collection: target.collection, id: target.id, data: patch, overrideAccess: true });
3712
+ didApply = true;
3713
+ applied++;
3714
+ }
3715
+ }
3716
+ results.push({
3717
+ collection: target.collection,
3718
+ id: target.id,
3719
+ title: r.title || "",
3720
+ before,
3721
+ after,
3722
+ changed,
3723
+ applied: didApply,
3724
+ method: r.method
3725
+ });
3726
+ } catch (e) {
3727
+ results.push({
3728
+ collection: target.collection,
3729
+ id: target.id,
3730
+ title: "",
3731
+ before: { metaTitle: "", metaDescription: "", focusKeyword: "" },
3732
+ after: { metaTitle: "", metaDescription: "", focusKeyword: "" },
3733
+ changed: false,
3734
+ applied: false,
3735
+ error: e instanceof Error ? e.message : "error"
3736
+ });
3737
+ }
3738
+ if (i % 3 === 0) await new Promise((resolve) => setImmediate(resolve));
3739
+ }
3740
+ return Response.json(
3741
+ {
3742
+ processed: results.length,
3743
+ changedCount: results.filter((r) => r.changed).length,
3744
+ applied,
3745
+ capped,
3746
+ results
3747
+ },
3748
+ { headers: { "Cache-Control": "no-store" } }
3749
+ );
3750
+ } catch (error) {
3751
+ const message = error instanceof Error ? error.message : "Internal server error";
3752
+ req.payload.logger.error(`[seo] ai-optimize-bulk error: ${message}`);
3753
+ return Response.json({ error: message }, { status: 500 });
3754
+ }
3755
+ };
3756
+ }
3757
+
3444
3758
  // src/endpoints/cannibalization.ts
3445
3759
  function canonicalIntent(keyword) {
3446
3760
  return keyword.toLowerCase().normalize("NFD").replace(/\p{Diacritic}/gu, "").replace(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/).filter(Boolean).sort().join(" ");
@@ -4025,7 +4339,7 @@ function getDateThreshold(period) {
4025
4339
  return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1e3);
4026
4340
  }
4027
4341
  }
4028
- function isAdmin5(user) {
4342
+ function isAdmin6(user) {
4029
4343
  if (!user) return false;
4030
4344
  if (user.role === "admin") return true;
4031
4345
  if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
@@ -4130,7 +4444,7 @@ function createPerformanceHandler() {
4130
4444
  });
4131
4445
  }
4132
4446
  if (method === "POST") {
4133
- if (!isAdmin5(req.user)) {
4447
+ if (!isAdmin6(req.user)) {
4134
4448
  return Response.json({ error: "Admin access required" }, { status: 403 });
4135
4449
  }
4136
4450
  const body = await parseJsonBody(req);
@@ -5238,24 +5552,46 @@ function buildArticleSchema(doc, siteUrl) {
5238
5552
  }
5239
5553
  return schema;
5240
5554
  }
5241
- function buildLocalBusinessSchema(doc, siteUrl) {
5555
+ function buildLocationNode(loc, doc, siteUrl) {
5242
5556
  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 || ""}`
5557
+ const node = {
5558
+ "@type": typeof loc.type === "string" && loc.type || "LocalBusiness",
5559
+ name: loc.name || doc.title || meta.title || "",
5560
+ description: loc.description || meta.description || "",
5561
+ url: loc.url || `${siteUrl}/${doc.slug || ""}`
5249
5562
  };
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
5563
+ if (loc.telephone) node.telephone = loc.telephone;
5564
+ if (loc.email) node.email = loc.email;
5565
+ if (loc.priceRange) node.priceRange = loc.priceRange;
5566
+ const address = loc.address;
5567
+ if (address && typeof address === "object") {
5568
+ node.address = { "@type": "PostalAddress", ...address };
5569
+ } else if (typeof address === "string" && address) {
5570
+ node.address = address;
5571
+ }
5572
+ const geo = loc.geo || {};
5573
+ const lat = geo.latitude ?? loc.latitude ?? loc.lat;
5574
+ const lng = geo.longitude ?? loc.longitude ?? loc.lng;
5575
+ if (lat != null && lng != null) {
5576
+ node.geo = { "@type": "GeoCoordinates", latitude: lat, longitude: lng };
5577
+ }
5578
+ if (Array.isArray(loc.openingHours) && loc.openingHours.length > 0) {
5579
+ node.openingHours = loc.openingHours;
5580
+ } else if (typeof loc.openingHours === "string" && loc.openingHours) {
5581
+ node.openingHours = loc.openingHours;
5582
+ }
5583
+ return node;
5584
+ }
5585
+ function buildLocalBusinessSchema(doc, siteUrl) {
5586
+ const locations = Array.isArray(doc.locations) ? doc.locations.filter((l) => !!l && typeof l === "object") : [];
5587
+ if (locations.length > 1) {
5588
+ return {
5589
+ "@context": "https://schema.org",
5590
+ "@graph": locations.map((loc) => buildLocationNode(loc, doc, siteUrl))
5256
5591
  };
5257
5592
  }
5258
- return schema;
5593
+ const base = locations.length === 1 ? locations[0] : doc;
5594
+ return { "@context": "https://schema.org", ...buildLocationNode(base, doc, siteUrl) };
5259
5595
  }
5260
5596
  function buildBreadcrumbSchema(doc, siteUrl) {
5261
5597
  const slug = doc.slug || "";
@@ -5822,7 +6158,7 @@ function createAiRewriteHandler(targetCollections) {
5822
6158
  }
5823
6159
 
5824
6160
  // src/endpoints/robots.ts
5825
- function isAdmin6(user) {
6161
+ function isAdmin7(user) {
5826
6162
  if (!user) return false;
5827
6163
  if (user.role === "admin") return true;
5828
6164
  if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
@@ -5871,7 +6207,7 @@ function createRobotsUpdateHandler() {
5871
6207
  if (!req.user) {
5872
6208
  return Response.json({ error: "Unauthorized" }, { status: 401 });
5873
6209
  }
5874
- if (!isAdmin6(req.user)) {
6210
+ if (!isAdmin7(req.user)) {
5875
6211
  return Response.json({ error: "Admin access required" }, { status: 403 });
5876
6212
  }
5877
6213
  const body = await parseJsonBody(req);
@@ -6006,6 +6342,191 @@ function createSitemapHandler(targetCollections) {
6006
6342
  };
6007
6343
  }
6008
6344
 
6345
+ // src/endpoints/sitemapExtensions.ts
6346
+ function escapeXml2(str) {
6347
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
6348
+ }
6349
+ function resolveSiteUrl3(seoConfig) {
6350
+ return (seoConfig?.siteUrl || process.env.NEXT_PUBLIC_SERVER_URL || process.env.PAYLOAD_PUBLIC_SERVER_URL || "").replace(/\/$/, "");
6351
+ }
6352
+ function docPath(slug) {
6353
+ return slug === "home" || slug === "" ? "" : `/${slug}`;
6354
+ }
6355
+ function xmlResponse(xml, status = 200) {
6356
+ return new Response(xml, {
6357
+ status,
6358
+ headers: { "Content-Type": "application/xml", "Cache-Control": "public, max-age=3600, s-maxage=3600" }
6359
+ });
6360
+ }
6361
+ function mediaUrl(media, siteUrl) {
6362
+ if (typeof media.url === "string" && media.url) {
6363
+ return media.url.startsWith("http") ? media.url : `${siteUrl}${media.url}`;
6364
+ }
6365
+ if (typeof media.filename === "string" && media.filename) {
6366
+ return `${siteUrl}/media/${media.filename}`;
6367
+ }
6368
+ return void 0;
6369
+ }
6370
+ function collectMediaUrls(node, mimePrefix, siteUrl, out, depth = 0) {
6371
+ if (!node || typeof node !== "object" || depth > 8) return;
6372
+ if (Array.isArray(node)) {
6373
+ for (const item of node) collectMediaUrls(item, mimePrefix, siteUrl, out, depth + 1);
6374
+ return;
6375
+ }
6376
+ const obj = node;
6377
+ const mime = typeof obj.mimeType === "string" ? obj.mimeType : "";
6378
+ if (mime.startsWith(mimePrefix)) {
6379
+ const url = mediaUrl(obj, siteUrl);
6380
+ if (url) out.add(url);
6381
+ }
6382
+ for (const key of Object.keys(obj)) {
6383
+ if (key === "sizes" || key === "_status") continue;
6384
+ collectMediaUrls(obj[key], mimePrefix, siteUrl, out, depth + 1);
6385
+ }
6386
+ }
6387
+ async function eachPublishedDoc(payload, collections, depth, onDoc) {
6388
+ const BATCH = Math.min(100, Math.max(1, parseInt(process.env.SEO_SITEMAP_BATCH_SIZE || "50", 10) || 50));
6389
+ const MAX = Math.max(1, parseInt(process.env.SEO_SITEMAP_MAX_DOCS || "5000", 10) || 5e3);
6390
+ let count = 0;
6391
+ for (const collection of collections) {
6392
+ try {
6393
+ let page = 1;
6394
+ let hasMore = true;
6395
+ while (hasMore) {
6396
+ const res = await payload.find({ collection, limit: BATCH, page, depth, overrideAccess: true });
6397
+ for (const doc of res.docs) {
6398
+ if (doc._status === "draft") continue;
6399
+ if (count >= MAX) return;
6400
+ onDoc(doc, collection);
6401
+ count++;
6402
+ }
6403
+ hasMore = res.hasNextPage;
6404
+ page++;
6405
+ await new Promise((resolve) => setImmediate(resolve));
6406
+ }
6407
+ } catch {
6408
+ }
6409
+ }
6410
+ }
6411
+ function createNewsSitemapHandler(targetCollections, seoConfig) {
6412
+ return async (req) => {
6413
+ try {
6414
+ const siteUrl = resolveSiteUrl3(seoConfig);
6415
+ const language = seoConfig?.locale === "en" ? "en" : "fr";
6416
+ let publication = seoConfig?.siteName || "";
6417
+ if (!publication && siteUrl) {
6418
+ try {
6419
+ publication = new URL(siteUrl).hostname;
6420
+ } catch {
6421
+ }
6422
+ }
6423
+ const cutoff = Date.now() - 48 * 36e5;
6424
+ const entries = [];
6425
+ await eachPublishedDoc(req.payload, targetCollections, 0, (doc) => {
6426
+ const dateStr = typeof doc.publishedAt === "string" && doc.publishedAt || typeof doc.date === "string" && doc.date || typeof doc.createdAt === "string" && doc.createdAt || "";
6427
+ if (!dateStr) return;
6428
+ const t = new Date(dateStr).getTime();
6429
+ if (isNaN(t) || t < cutoff) return;
6430
+ const title = doc.title || doc.meta?.title || "";
6431
+ if (!title) return;
6432
+ const loc = `${siteUrl}${docPath(doc.slug || "")}`;
6433
+ entries.push(
6434
+ ` <url>
6435
+ <loc>${escapeXml2(loc)}</loc>
6436
+ <news:news>
6437
+ <news:publication>
6438
+ <news:name>${escapeXml2(publication)}</news:name>
6439
+ <news:language>${language}</news:language>
6440
+ </news:publication>
6441
+ <news:publication_date>${new Date(dateStr).toISOString()}</news:publication_date>
6442
+ <news:title>${escapeXml2(title)}</news:title>
6443
+ </news:news>
6444
+ </url>`
6445
+ );
6446
+ });
6447
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
6448
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">
6449
+ ${entries.join("\n")}
6450
+ </urlset>`;
6451
+ return xmlResponse(xml);
6452
+ } catch (error) {
6453
+ req.payload.logger.error(`[seo] sitemap-news error: ${error instanceof Error ? error.message : "unknown"}`);
6454
+ return xmlResponse('<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>', 500);
6455
+ }
6456
+ };
6457
+ }
6458
+ function createImageSitemapHandler(targetCollections, seoConfig) {
6459
+ return async (req) => {
6460
+ try {
6461
+ const siteUrl = resolveSiteUrl3(seoConfig);
6462
+ const entries = [];
6463
+ await eachPublishedDoc(req.payload, targetCollections, 1, (doc) => {
6464
+ const urls = /* @__PURE__ */ new Set();
6465
+ collectMediaUrls(doc, "image/", siteUrl, urls);
6466
+ if (urls.size === 0) return;
6467
+ const loc = `${siteUrl}${docPath(doc.slug || "")}`;
6468
+ const imgs = Array.from(urls).slice(0, 1e3).map((u) => ` <image:image><image:loc>${escapeXml2(u)}</image:loc></image:image>`).join("\n");
6469
+ entries.push(` <url>
6470
+ <loc>${escapeXml2(loc)}</loc>
6471
+ ${imgs}
6472
+ </url>`);
6473
+ });
6474
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
6475
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
6476
+ ${entries.join("\n")}
6477
+ </urlset>`;
6478
+ return xmlResponse(xml);
6479
+ } catch (error) {
6480
+ req.payload.logger.error(`[seo] sitemap-images error: ${error instanceof Error ? error.message : "unknown"}`);
6481
+ return xmlResponse('<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>', 500);
6482
+ }
6483
+ };
6484
+ }
6485
+ function createVideoSitemapHandler(targetCollections, seoConfig) {
6486
+ return async (req) => {
6487
+ try {
6488
+ const siteUrl = resolveSiteUrl3(seoConfig);
6489
+ const entries = [];
6490
+ await eachPublishedDoc(req.payload, targetCollections, 1, (doc) => {
6491
+ const meta = doc.meta || {};
6492
+ const videoUrls = /* @__PURE__ */ new Set();
6493
+ collectMediaUrls(doc, "video/", siteUrl, videoUrls);
6494
+ for (const k of ["videoUrl", "contentUrl", "playerUrl"]) {
6495
+ if (typeof doc[k] === "string" && doc[k]) videoUrls.add(doc[k]);
6496
+ }
6497
+ if (videoUrls.size === 0) return;
6498
+ const title = doc.title || meta.title || "";
6499
+ const description = meta.description || title;
6500
+ const thumbs = /* @__PURE__ */ new Set();
6501
+ collectMediaUrls(meta.image, "image/", siteUrl, thumbs);
6502
+ if (thumbs.size === 0) collectMediaUrls(doc, "image/", siteUrl, thumbs);
6503
+ const thumbnail = Array.from(thumbs)[0] || "";
6504
+ const loc = `${siteUrl}${docPath(doc.slug || "")}`;
6505
+ const videos = Array.from(videoUrls).slice(0, 100).map(
6506
+ (u) => ` <video:video>
6507
+ ${thumbnail ? ` <video:thumbnail_loc>${escapeXml2(thumbnail)}</video:thumbnail_loc>
6508
+ ` : ""} <video:title>${escapeXml2(title || "Video")}</video:title>
6509
+ <video:description>${escapeXml2(description || title || "Video")}</video:description>
6510
+ <video:content_loc>${escapeXml2(u)}</video:content_loc>
6511
+ </video:video>`
6512
+ ).join("\n");
6513
+ entries.push(` <url>
6514
+ <loc>${escapeXml2(loc)}</loc>
6515
+ ${videos}
6516
+ </url>`);
6517
+ });
6518
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
6519
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
6520
+ ${entries.join("\n")}
6521
+ </urlset>`;
6522
+ return xmlResponse(xml);
6523
+ } catch (error) {
6524
+ req.payload.logger.error(`[seo] sitemap-video error: ${error instanceof Error ? error.message : "unknown"}`);
6525
+ return xmlResponse('<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>', 500);
6526
+ }
6527
+ };
6528
+ }
6529
+
6009
6530
  // src/collections/SeoScoreHistory.ts
6010
6531
  function createSeoScoreHistoryCollection() {
6011
6532
  return {
@@ -6916,7 +7437,7 @@ function getClientIp(req) {
6916
7437
 
6917
7438
  // src/endpoints/seoLogs.ts
6918
7439
  var VALID_LOG_TYPES = ["404", "redirect", "error"];
6919
- function isAdmin7(user) {
7440
+ function isAdmin8(user) {
6920
7441
  if (!user) return false;
6921
7442
  if (user.role === "admin") return true;
6922
7443
  if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
@@ -7013,7 +7534,7 @@ function createSeoLogsHandler(seoLogsSecret) {
7013
7534
  return Response.json({ error: "Unauthorized" }, { status: 401 });
7014
7535
  }
7015
7536
  if (method === "DELETE") {
7016
- if (!isAdmin7(req.user)) {
7537
+ if (!isAdmin8(req.user)) {
7017
7538
  return Response.json({ error: "Admin access required" }, { status: 403 });
7018
7539
  }
7019
7540
  try {
@@ -7190,6 +7711,7 @@ async function doWarmUp(payload, collections = ["pages", "posts"], globals = [])
7190
7711
  }
7191
7712
  }
7192
7713
  function startCacheWarmUp(payload, _basePath, globals = [], collections = ["pages", "posts"]) {
7714
+ stopCacheWarmUp();
7193
7715
  setTimeout(() => {
7194
7716
  void doWarmUp(payload, collections, globals);
7195
7717
  }, STARTUP_DELAY);
@@ -7231,6 +7753,7 @@ async function doSnapshot(payload, basePath, seoConfig) {
7231
7753
  }
7232
7754
  }
7233
7755
  function startRankTracker(payload, basePath, seoConfig) {
7756
+ stopRankTracker();
7234
7757
  setTimeout(() => {
7235
7758
  void doSnapshot(payload, basePath, seoConfig);
7236
7759
  }, STARTUP_DELAY2);
@@ -7253,7 +7776,7 @@ function stopRankTracker() {
7253
7776
  }
7254
7777
 
7255
7778
  // src/endpoints/alerts.ts
7256
- function isAdmin8(user) {
7779
+ function isAdmin9(user) {
7257
7780
  if (!user) return false;
7258
7781
  if (user.role === "admin") return true;
7259
7782
  if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
@@ -7419,7 +7942,7 @@ async function deliverAlertDigest(payload, digest, cfg, siteUrl) {
7419
7942
  function createAlertsDigestHandler() {
7420
7943
  return async (req) => {
7421
7944
  try {
7422
- if (!isAdmin8(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
7945
+ if (!isAdmin9(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
7423
7946
  const cfg = getAlertConfig();
7424
7947
  const digest = await buildAlertDigest(req.payload, cfg);
7425
7948
  return Response.json(
@@ -7445,7 +7968,7 @@ function createAlertsDigestHandler() {
7445
7968
  function createAlertsRunHandler(siteUrl) {
7446
7969
  return async (req) => {
7447
7970
  try {
7448
- if (!isAdmin8(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
7971
+ if (!isAdmin9(req.user)) return Response.json({ error: "Forbidden" }, { status: 403 });
7449
7972
  const cfg = getAlertConfig();
7450
7973
  const digest = await buildAlertDigest(req.payload, cfg);
7451
7974
  const delivery = await deliverAlertDigest(req.payload, digest, cfg, siteUrl);
@@ -7480,6 +8003,7 @@ async function runDigest(payload, siteUrl) {
7480
8003
  }
7481
8004
  }
7482
8005
  function startAlertsScheduler(payload, siteUrl) {
8006
+ stopAlertsScheduler();
7483
8007
  const intervalHours = Math.max(1, parseInt(process.env.SEO_ALERT_INTERVAL_HOURS || "24", 10) || 24);
7484
8008
  const intervalMs = intervalHours * 60 * 60 * 1e3;
7485
8009
  setTimeout(() => {
@@ -8494,6 +9018,16 @@ var fr = {
8494
9018
  pagesAnalyzed: "pages analys\xE9es",
8495
9019
  markCornerstone: "Marquer pilier",
8496
9020
  unmarkCornerstone: "D\xE9marquer pilier",
9021
+ bulkOptimizeMeta: "Optimiser m\xE9ta (IA)",
9022
+ bulkOptimizing: "Analyse\u2026",
9023
+ bulkConfirm: "Confirmer ?",
9024
+ bulkPreviewTitle: "Corrections m\xE9ta propos\xE9es",
9025
+ bulkApply: "Appliquer",
9026
+ bulkApplying: "Application\u2026",
9027
+ bulkCancel: "Annuler",
9028
+ bulkExport: "Exporter CSV",
9029
+ bulkNoChanges: "Aucune correction n\xE9cessaire sur la s\xE9lection.",
9030
+ bulkCappedNote: "limite atteinte (affine la s\xE9lection)",
8497
9031
  searchPlaceholder: "Rechercher (titre, slug, keyword)...",
8498
9032
  allCollections: "Toutes les collections",
8499
9033
  allScores: "Tous les scores",
@@ -9086,6 +9620,16 @@ var en = {
9086
9620
  pagesAnalyzed: "pages analyzed",
9087
9621
  markCornerstone: "Mark as cornerstone",
9088
9622
  unmarkCornerstone: "Unmark cornerstone",
9623
+ bulkOptimizeMeta: "Optimize meta (AI)",
9624
+ bulkOptimizing: "Analyzing\u2026",
9625
+ bulkConfirm: "Confirm?",
9626
+ bulkPreviewTitle: "Proposed meta corrections",
9627
+ bulkApply: "Apply",
9628
+ bulkApplying: "Applying\u2026",
9629
+ bulkCancel: "Cancel",
9630
+ bulkExport: "Export CSV",
9631
+ bulkNoChanges: "No corrections needed on the selection.",
9632
+ bulkCappedNote: "limit reached (narrow the selection)",
9089
9633
  searchPlaceholder: "Search (title, slug, keyword)...",
9090
9634
  allCollections: "All collections",
9091
9635
  allScores: "All scores",
@@ -9937,7 +10481,9 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
9937
10481
  { path: `${basePath}/ai-rewrite`, method: "post", handler: createAiRewriteHandler(targetCollections) },
9938
10482
  { path: `${basePath}/ai-optimize`, method: "post", handler: createAiOptimizeHandler(targetCollections, seoConfig) },
9939
10483
  { path: `${basePath}/alt-text-audit`, method: "get", handler: createAltTextAuditHandler(uploadsCollection) },
9940
- { path: `${basePath}/ai-alt-text`, method: "post", handler: withRateLimit(createAiAltTextHandler(uploadsCollection, seoConfig)) }
10484
+ { path: `${basePath}/ai-alt-text`, method: "post", handler: withRateLimit(createAiAltTextHandler(uploadsCollection, seoConfig)) },
10485
+ { path: `${basePath}/ai-content-brief`, method: "post", handler: withRateLimit(createAiContentBriefHandler(targetCollections, seoConfig)) },
10486
+ { path: `${basePath}/ai-optimize-bulk`, method: "post", handler: withRateLimit(createAiOptimizeBulkHandler(targetCollections, seoConfig)) }
9941
10487
  );
9942
10488
  }
9943
10489
  if (features.cannibalization) {
@@ -10029,6 +10575,21 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
10029
10575
  path: `${basePath}/sitemap.xml`,
10030
10576
  method: "get",
10031
10577
  handler: createSitemapHandler(targetCollections)
10578
+ },
10579
+ {
10580
+ path: `${basePath}/sitemap-news.xml`,
10581
+ method: "get",
10582
+ handler: createNewsSitemapHandler(targetCollections, seoConfig)
10583
+ },
10584
+ {
10585
+ path: `${basePath}/sitemap-images.xml`,
10586
+ method: "get",
10587
+ handler: createImageSitemapHandler(targetCollections, seoConfig)
10588
+ },
10589
+ {
10590
+ path: `${basePath}/sitemap-video.xml`,
10591
+ method: "get",
10592
+ handler: createVideoSitemapHandler(targetCollections, seoConfig)
10032
10593
  }
10033
10594
  );
10034
10595
  config.endpoints = [
@@ -10133,7 +10694,7 @@ var seoAnalyzerPlugin = (pluginConfig = {}) => (incomingConfig) => {
10133
10694
  };
10134
10695
 
10135
10696
  // src/helpers/buildMetadata.ts
10136
- function resolveSiteUrl3(explicit) {
10697
+ function resolveSiteUrl4(explicit) {
10137
10698
  return (explicit || process.env.NEXT_PUBLIC_SERVER_URL || process.env.PAYLOAD_PUBLIC_SERVER_URL || "").replace(/\/$/, "");
10138
10699
  }
10139
10700
  function parseRobots(doc, meta) {
@@ -10167,7 +10728,7 @@ function absoluteUrl(value, siteUrl) {
10167
10728
  return `${siteUrl}${value.startsWith("/") ? "" : "/"}${value}`;
10168
10729
  }
10169
10730
  function buildSeoMetadata(doc, options = {}) {
10170
- const siteUrl = resolveSiteUrl3(options.siteUrl);
10731
+ const siteUrl = resolveSiteUrl4(options.siteUrl);
10171
10732
  const meta = doc.meta || {};
10172
10733
  const rawTitle = meta.title || doc.title || "";
10173
10734
  const title = options.titleTemplate && rawTitle ? options.titleTemplate.replace("%s", rawTitle) : rawTitle;