@burdenoff/website-sdk 2026.1031.3 → 2026.1101.1

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.
Files changed (47) hide show
  1. package/dist/components/contact.js +12 -12
  2. package/dist/components/contact.js.map +1 -1
  3. package/dist/components/contact.mjs +12 -12
  4. package/dist/components/contact.mjs.map +1 -1
  5. package/dist/components/explore.js +12 -12
  6. package/dist/components/explore.js.map +1 -1
  7. package/dist/components/explore.mjs +12 -12
  8. package/dist/components/explore.mjs.map +1 -1
  9. package/dist/components/newsletter.js +12 -12
  10. package/dist/components/newsletter.js.map +1 -1
  11. package/dist/components/newsletter.mjs +12 -12
  12. package/dist/components/newsletter.mjs.map +1 -1
  13. package/dist/components/partners.js +12 -12
  14. package/dist/components/partners.js.map +1 -1
  15. package/dist/components/partners.mjs +12 -12
  16. package/dist/components/partners.mjs.map +1 -1
  17. package/dist/components/pricing.js +12 -12
  18. package/dist/components/pricing.js.map +1 -1
  19. package/dist/components/pricing.mjs +12 -12
  20. package/dist/components/pricing.mjs.map +1 -1
  21. package/dist/components/unsubscribe.js +12 -12
  22. package/dist/components/unsubscribe.js.map +1 -1
  23. package/dist/components/unsubscribe.mjs +12 -12
  24. package/dist/components/unsubscribe.mjs.map +1 -1
  25. package/dist/components/waitlist.js +12 -12
  26. package/dist/components/waitlist.js.map +1 -1
  27. package/dist/components/waitlist.mjs +12 -12
  28. package/dist/components/waitlist.mjs.map +1 -1
  29. package/dist/content/index.js +12 -12
  30. package/dist/content/index.js.map +1 -1
  31. package/dist/content/index.mjs +12 -12
  32. package/dist/content/index.mjs.map +1 -1
  33. package/dist/index.d.mts +1 -1
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.js +12 -12
  36. package/dist/index.js.map +1 -1
  37. package/dist/index.mjs +12 -12
  38. package/dist/index.mjs.map +1 -1
  39. package/dist/seo/index.js +12 -12
  40. package/dist/seo/index.js.map +1 -1
  41. package/dist/seo/index.mjs +12 -12
  42. package/dist/seo/index.mjs.map +1 -1
  43. package/dist/store/index.js +12 -12
  44. package/dist/store/index.js.map +1 -1
  45. package/dist/store/index.mjs +12 -12
  46. package/dist/store/index.mjs.map +1 -1
  47. package/package.json +1 -1
package/dist/seo/index.js CHANGED
@@ -45,24 +45,24 @@ function PageHead({
45
45
  return /* @__PURE__ */ jsxRuntime.jsxs(reactHelmetAsync.Helmet, { children: [
46
46
  /* @__PURE__ */ jsxRuntime.jsx("title", { children: fullTitle }),
47
47
  /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "title", content: fullTitle }),
48
- description && /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "description", content: description }),
49
- keywords && /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "keywords", content: keywords }),
50
- author && /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "author", content: author }),
48
+ description ? /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "description", content: description }) : null,
49
+ keywords ? /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "keywords", content: keywords }) : null,
50
+ author ? /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "author", content: author }) : null,
51
51
  /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "robots", content: robots }),
52
- resolvedUrl && /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "canonical", href: resolvedUrl }),
53
- favicon && /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "icon", href: favicon }),
52
+ resolvedUrl ? /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "canonical", href: resolvedUrl }) : null,
53
+ favicon ? /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "icon", href: favicon }) : null,
54
54
  /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:type", content: "website" }),
55
55
  /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:title", content: fullTitle }),
56
- description && /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:description", content: description }),
57
- fullImageUrl && /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:image", content: fullImageUrl }),
58
- resolvedUrl && /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:url", content: resolvedUrl }),
56
+ description ? /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:description", content: description }) : null,
57
+ fullImageUrl ? /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:image", content: fullImageUrl }) : null,
58
+ resolvedUrl ? /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:url", content: resolvedUrl }) : null,
59
59
  /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:locale", content: locale }),
60
- productName && /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:site_name", content: productName }),
60
+ productName ? /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:site_name", content: productName }) : null,
61
61
  /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "twitter:card", content: "summary_large_image" }),
62
62
  /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "twitter:title", content: fullTitle }),
63
- description && /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "twitter:description", content: description }),
64
- fullImageUrl && /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "twitter:image", content: fullImageUrl }),
65
- twitterHandle && /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "twitter:creator", content: twitterHandle }),
63
+ description ? /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "twitter:description", content: description }) : null,
64
+ fullImageUrl ? /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "twitter:image", content: fullImageUrl }) : null,
65
+ twitterHandle ? /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "twitter:creator", content: twitterHandle }) : null,
66
66
  /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "theme-color", content: themeColor }),
67
67
  structuredDataItems.map((data, index) => /* @__PURE__ */ jsxRuntime.jsx("script", { type: "application/ld+json", children: serializeJsonLd(data) }, `ld-${index}`)),
68
68
  additionalMeta?.map((meta, index) => /* @__PURE__ */ jsxRuntime.jsx(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/seo/page-head.tsx"],"names":["Helmet","jsx"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * PageHead Component\n * Manages dynamic meta tags for SEO optimization on a per-page basis.\n * Supports JSON-LD structured data, Open Graph, Twitter Cards, favicons, and robots directives.\n *\n * @example\n * ```tsx\n * <PageHead\n * title=\"About Us\"\n * description=\"Learn about our mission and team\"\n * productName=\"MyProduct\"\n * keywords=\"about, team, mission\"\n * image=\"/og-about.png\"\n * url=\"https://myproduct.com/about\"\n * favicon=\"/favicon.ico\"\n * robots=\"index, follow\"\n * locale=\"en_US\"\n * twitterHandle=\"@burdenoff\"\n * themeColor=\"#1a1a2e\"\n * structuredData={{\n * \"@context\": \"https://schema.org\",\n * \"@type\": \"WebPage\",\n * name: \"About Us\",\n * }}\n * />\n * ```\n */\n\nimport { Helmet } from \"react-helmet-async\";\n\nexport interface PageHeadProps {\n /** Page title */\n title: string;\n /** Meta description */\n description?: string;\n /** Product/brand name (appended to title) */\n productName?: string;\n /** SEO keywords */\n keywords?: string;\n /** Open Graph image URL */\n image?: string;\n /** Page canonical URL */\n url?: string;\n /** Favicon URL */\n favicon?: string;\n /** Robots directive (e.g., \"index, follow\") */\n robots?: string;\n /** Open Graph locale (e.g., \"en_US\") */\n locale?: string;\n /** Twitter handle (e.g., \"@burdenoff\") */\n twitterHandle?: string;\n /** Theme color for mobile browsers */\n themeColor?: string;\n /**\n * Value for `<meta name=\"author\">`. Omitted by default.\n *\n * Each product site already declares its author in `index.html` as the\n * legal entity that publicly holds that product (BUR-SOP-029, \"Holder is\n * what you publish\"), and that entity differs between products. A default\n * here would add a second, conflicting author tag to every route, so the\n * tag is only rendered when a site passes the value explicitly.\n */\n author?: string;\n /** JSON-LD structured data objects */\n structuredData?: Record<string, unknown> | Array<Record<string, unknown>>;\n /** Additional meta tags */\n additionalMeta?: Array<{\n name?: string;\n property?: string;\n content: string;\n }>;\n}\n\n/**\n * Build a self-referencing canonical URL from the current location.\n *\n * Query strings and hashes are dropped (they never identify a distinct\n * canonical document here) and the trailing slash is normalised away for\n * everything but the site root. Returns `undefined` outside a DOM so the\n * component degrades to its previous behaviour under plain string rendering.\n */\nfunction canonicalFromLocation(): string | undefined {\n if (typeof window === \"undefined\" || !window.location) return undefined;\n const { origin, pathname } = window.location;\n if (!origin || origin === \"null\") return undefined;\n const path = pathname === \"/\" ? \"/\" : pathname.replace(/\\/+$/, \"\");\n return `${origin}${path}`;\n}\n\n/**\n * Absolutise an Open Graph image URL.\n *\n * Already-absolute URLs pass through. A root-relative path resolves against\n * the origin, and any other relative path against the page URL. Without a\n * resolvable base the original value is returned unchanged.\n */\nfunction resolveImageUrl(\n image: string | undefined,\n base: string | undefined,\n): string | undefined {\n if (!image || /^https?:\\/\\//i.test(image)) return image;\n if (!base) return image;\n try {\n return new URL(image, base).toString();\n } catch {\n return image;\n }\n}\n\n/**\n * JSON-LD as the body of a `<script>` element.\n *\n * `JSON.stringify` leaves `<` alone, and a script body ends at the first `</script` whatever\n * the quoting around it. The prerender serialises the live DOM with script text verbatim,\n * so a `</script><script>…` inside any string field (a caption, a title) would close the\n * JSON-LD block in the static HTML and run what follows. `\\u003c` is the same character to\n * every JSON parser; U+2028 / U+2029 are escaped for engines that still read them as line\n * breaks.\n */\nfunction serializeJsonLd(data: Record<string, unknown>): string {\n return JSON.stringify(data)\n .replace(/</g, \"\\\\u003c\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n}\n\nexport function PageHead({\n title,\n description,\n productName,\n keywords,\n image,\n url,\n favicon,\n robots = \"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\",\n locale = \"en_US\",\n twitterHandle,\n themeColor = \"#000000\",\n author,\n structuredData,\n additionalMeta,\n}: PageHeadProps) {\n const fullTitle =\n productName && !title.includes(productName)\n ? `${title} | ${productName}`\n : title;\n\n // Self-referencing canonical by default.\n //\n // Without this, a route that renders PageHead but omits `url` emits no\n // canonical at all, so the static `<link rel=\"canonical\">` in index.html\n // survives and every such page declares itself a duplicate of the home\n // page — Google then drops /pricing, /privacy, /terms and friends from the\n // index. Deriving it from the current location keeps each page pointing at\n // itself; callers that genuinely need a cross-canonical still pass `url`.\n //\n // During prerender the DOM location already reflects the route being\n // rendered, so this resolves correctly ahead of hydration.\n const resolvedUrl = url ?? canonicalFromLocation();\n\n // Root-relative images (\"/og.png\") belong to the ORIGIN, not to the current\n // path — concatenating them onto the page URL yields /features/og.png and a\n // broken share card. Anything else is resolved relative to the page itself.\n const fullImageUrl = resolveImageUrl(image, resolvedUrl);\n\n // Normalize structured data to an array\n const structuredDataItems: Array<Record<string, unknown>> = structuredData\n ? Array.isArray(structuredData)\n ? structuredData\n : [structuredData]\n : [];\n\n return (\n <Helmet>\n {/* Primary Meta Tags */}\n <title>{fullTitle}</title>\n <meta name=\"title\" content={fullTitle} />\n {description && <meta name=\"description\" content={description} />}\n {keywords && <meta name=\"keywords\" content={keywords} />}\n {author && <meta name=\"author\" content={author} />}\n <meta name=\"robots\" content={robots} />\n\n {/* Canonical URL */}\n {resolvedUrl && <link rel=\"canonical\" href={resolvedUrl} />}\n\n {/* Favicon */}\n {favicon && <link rel=\"icon\" href={favicon} />}\n\n {/* Open Graph / Facebook */}\n <meta property=\"og:type\" content=\"website\" />\n <meta property=\"og:title\" content={fullTitle} />\n {description && <meta property=\"og:description\" content={description} />}\n {fullImageUrl && <meta property=\"og:image\" content={fullImageUrl} />}\n {resolvedUrl && <meta property=\"og:url\" content={resolvedUrl} />}\n <meta property=\"og:locale\" content={locale} />\n {productName && <meta property=\"og:site_name\" content={productName} />}\n\n {/* Twitter Card */}\n <meta name=\"twitter:card\" content=\"summary_large_image\" />\n <meta name=\"twitter:title\" content={fullTitle} />\n {description && <meta name=\"twitter:description\" content={description} />}\n {fullImageUrl && <meta name=\"twitter:image\" content={fullImageUrl} />}\n {twitterHandle && <meta name=\"twitter:creator\" content={twitterHandle} />}\n\n {/* Theme Color */}\n <meta name=\"theme-color\" content={themeColor} />\n\n {/* JSON-LD Structured Data */}\n {structuredDataItems.map((data, index) => (\n <script key={`ld-${index}`} type=\"application/ld+json\">\n {serializeJsonLd(data)}\n </script>\n ))}\n\n {/* Additional Meta Tags */}\n {additionalMeta?.map((meta, index) => (\n <meta\n key={`meta-${index}`}\n {...(meta.name ? { name: meta.name } : {})}\n {...(meta.property ? { property: meta.property } : {})}\n content={meta.content}\n />\n ))}\n </Helmet>\n );\n}\n"]}
1
+ {"version":3,"sources":["../../src/seo/page-head.tsx"],"names":["Helmet","jsx"],"mappings":";;;;;AAiFA,SAAS,qBAAA,GAA4C;AACnD,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,CAAC,MAAA,CAAO,UAAU,OAAO,MAAA;AAC9D,EAAA,MAAM,EAAE,MAAA,EAAQ,QAAA,EAAS,GAAI,MAAA,CAAO,QAAA;AACpC,EAAA,IAAI,CAAC,MAAA,IAAU,MAAA,KAAW,MAAA,EAAQ,OAAO,MAAA;AACzC,EAAA,MAAM,OAAO,QAAA,KAAa,GAAA,GAAM,MAAM,QAAA,CAAS,OAAA,CAAQ,QAAQ,EAAE,CAAA;AACjE,EAAA,OAAO,CAAA,EAAG,MAAM,CAAA,EAAG,IAAI,CAAA,CAAA;AACzB;AASA,SAAS,eAAA,CACP,OACA,IAAA,EACoB;AACpB,EAAA,IAAI,CAAC,KAAA,IAAS,eAAA,CAAgB,IAAA,CAAK,KAAK,GAAG,OAAO,KAAA;AAClD,EAAA,IAAI,CAAC,MAAM,OAAO,KAAA;AAClB,EAAA,IAAI;AACF,IAAA,OAAO,IAAI,GAAA,CAAI,KAAA,EAAO,IAAI,EAAE,QAAA,EAAS;AAAA,EACvC,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAYA,SAAS,gBAAgB,IAAA,EAAuC;AAC9D,EAAA,OAAO,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA,CACvB,QAAQ,IAAA,EAAM,SAAS,CAAA,CACvB,OAAA,CAAQ,SAAA,EAAW,SAAS,CAAA,CAC5B,OAAA,CAAQ,WAAW,SAAS,CAAA;AACjC;AAEO,SAAS,QAAA,CAAS;AAAA,EACvB,KAAA;AAAA,EACA,WAAA;AAAA,EACA,WAAA;AAAA,EACA,QAAA;AAAA,EACA,KAAA;AAAA,EACA,GAAA;AAAA,EACA,OAAA;AAAA,EACA,MAAA,GAAS,8EAAA;AAAA,EACT,MAAA,GAAS,OAAA;AAAA,EACT,aAAA;AAAA,EACA,UAAA,GAAa,SAAA;AAAA,EACb,MAAA;AAAA,EACA,cAAA;AAAA,EACA;AACF,CAAA,EAAkB;AAChB,EAAA,MAAM,SAAA,GACJ,WAAA,IAAe,CAAC,KAAA,CAAM,QAAA,CAAS,WAAW,CAAA,GACtC,CAAA,EAAG,KAAK,CAAA,GAAA,EAAM,WAAW,CAAA,CAAA,GACzB,KAAA;AAaN,EAAA,MAAM,WAAA,GAAc,OAAO,qBAAA,EAAsB;AAKjD,EAAA,MAAM,YAAA,GAAe,eAAA,CAAgB,KAAA,EAAO,WAAW,CAAA;AAGvD,EAAA,MAAM,mBAAA,GAAsD,cAAA,GACxD,KAAA,CAAM,OAAA,CAAQ,cAAc,IAC1B,cAAA,GACA,CAAC,cAAc,CAAA,GACjB,EAAC;AASL,EAAA,uCACGA,uBAAA,EAAA,EAEC,QAAA,EAAA;AAAA,oBAAAC,cAAA,CAAC,WAAO,QAAA,EAAA,SAAA,EAAU,CAAA;AAAA,oBAClBA,cAAA,CAAC,MAAA,EAAA,EAAK,IAAA,EAAK,OAAA,EAAQ,SAAS,SAAA,EAAW,CAAA;AAAA,IACtC,8BAAcA,cAAA,CAAC,MAAA,EAAA,EAAK,MAAK,aAAA,EAAc,OAAA,EAAS,aAAa,CAAA,GAAK,IAAA;AAAA,IAClE,2BAAWA,cAAA,CAAC,MAAA,EAAA,EAAK,MAAK,UAAA,EAAW,OAAA,EAAS,UAAU,CAAA,GAAK,IAAA;AAAA,IACzD,yBAASA,cAAA,CAAC,MAAA,EAAA,EAAK,MAAK,QAAA,EAAS,OAAA,EAAS,QAAQ,CAAA,GAAK,IAAA;AAAA,oBACpDA,cAAA,CAAC,MAAA,EAAA,EAAK,IAAA,EAAK,QAAA,EAAS,SAAS,MAAA,EAAQ,CAAA;AAAA,IAGpC,8BAAcA,cAAA,CAAC,MAAA,EAAA,EAAK,KAAI,WAAA,EAAY,IAAA,EAAM,aAAa,CAAA,GAAK,IAAA;AAAA,IAG5D,0BAAUA,cAAA,CAAC,MAAA,EAAA,EAAK,KAAI,MAAA,EAAO,IAAA,EAAM,SAAS,CAAA,GAAK,IAAA;AAAA,oBAGhDA,cAAA,CAAC,MAAA,EAAA,EAAK,QAAA,EAAS,SAAA,EAAU,SAAQ,SAAA,EAAU,CAAA;AAAA,oBAC3CA,cAAA,CAAC,MAAA,EAAA,EAAK,QAAA,EAAS,UAAA,EAAW,SAAS,SAAA,EAAW,CAAA;AAAA,IAC7C,8BACCA,cAAA,CAAC,MAAA,EAAA,EAAK,UAAS,gBAAA,EAAiB,OAAA,EAAS,aAAa,CAAA,GACpD,IAAA;AAAA,IACH,+BACCA,cAAA,CAAC,MAAA,EAAA,EAAK,UAAS,UAAA,EAAW,OAAA,EAAS,cAAc,CAAA,GAC/C,IAAA;AAAA,IACH,8BAAcA,cAAA,CAAC,MAAA,EAAA,EAAK,UAAS,QAAA,EAAS,OAAA,EAAS,aAAa,CAAA,GAAK,IAAA;AAAA,oBAClEA,cAAA,CAAC,MAAA,EAAA,EAAK,QAAA,EAAS,WAAA,EAAY,SAAS,MAAA,EAAQ,CAAA;AAAA,IAC3C,8BACCA,cAAA,CAAC,MAAA,EAAA,EAAK,UAAS,cAAA,EAAe,OAAA,EAAS,aAAa,CAAA,GAClD,IAAA;AAAA,oBAGJA,cAAA,CAAC,MAAA,EAAA,EAAK,IAAA,EAAK,cAAA,EAAe,SAAQ,qBAAA,EAAsB,CAAA;AAAA,oBACxDA,cAAA,CAAC,MAAA,EAAA,EAAK,IAAA,EAAK,eAAA,EAAgB,SAAS,SAAA,EAAW,CAAA;AAAA,IAC9C,8BACCA,cAAA,CAAC,MAAA,EAAA,EAAK,MAAK,qBAAA,EAAsB,OAAA,EAAS,aAAa,CAAA,GACrD,IAAA;AAAA,IACH,+BACCA,cAAA,CAAC,MAAA,EAAA,EAAK,MAAK,eAAA,EAAgB,OAAA,EAAS,cAAc,CAAA,GAChD,IAAA;AAAA,IACH,gCACCA,cAAA,CAAC,MAAA,EAAA,EAAK,MAAK,iBAAA,EAAkB,OAAA,EAAS,eAAe,CAAA,GACnD,IAAA;AAAA,oBAGJA,cAAA,CAAC,MAAA,EAAA,EAAK,IAAA,EAAK,aAAA,EAAc,SAAS,UAAA,EAAY,CAAA;AAAA,IAG7C,mBAAA,CAAoB,GAAA,CAAI,CAAC,IAAA,EAAM,0BAC9BA,cAAA,CAAC,QAAA,EAAA,EAA2B,IAAA,EAAK,qBAAA,EAC9B,0BAAgB,IAAI,CAAA,EAAA,EADV,CAAA,GAAA,EAAM,KAAK,EAExB,CACD,CAAA;AAAA,IAGA,cAAA,EAAgB,GAAA,CAAI,CAAC,IAAA,EAAM,KAAA,qBAC1BA,cAAA;AAAA,MAAC,MAAA;AAAA,MAAA;AAAA,QAEE,GAAI,KAAK,IAAA,GAAO,EAAE,MAAM,IAAA,CAAK,IAAA,KAAS,EAAC;AAAA,QACvC,GAAI,KAAK,QAAA,GAAW,EAAE,UAAU,IAAA,CAAK,QAAA,KAAa,EAAC;AAAA,QACpD,SAAS,IAAA,CAAK;AAAA,OAAA;AAAA,MAHT,QAAQ,KAAK,CAAA;AAAA,KAKrB;AAAA,GAAA,EACH,CAAA;AAEJ","file":"index.js","sourcesContent":["/**\n * PageHead Component\n * Manages dynamic meta tags for SEO optimization on a per-page basis.\n * Supports JSON-LD structured data, Open Graph, Twitter Cards, favicons, and robots directives.\n *\n * @example\n * ```tsx\n * <PageHead\n * title=\"About Us\"\n * description=\"Learn about our mission and team\"\n * productName=\"MyProduct\"\n * keywords=\"about, team, mission\"\n * image=\"/og-about.png\"\n * url=\"https://myproduct.com/about\"\n * favicon=\"/favicon.ico\"\n * robots=\"index, follow\"\n * locale=\"en_US\"\n * twitterHandle=\"@burdenoff\"\n * themeColor=\"#1a1a2e\"\n * structuredData={{\n * \"@context\": \"https://schema.org\",\n * \"@type\": \"WebPage\",\n * name: \"About Us\",\n * }}\n * />\n * ```\n */\n\nimport { Helmet } from \"react-helmet-async\";\n\nexport interface PageHeadProps {\n /** Page title */\n title: string;\n /** Meta description */\n description?: string;\n /** Product/brand name (appended to title) */\n productName?: string;\n /** SEO keywords */\n keywords?: string;\n /** Open Graph image URL */\n image?: string;\n /** Page canonical URL */\n url?: string;\n /** Favicon URL */\n favicon?: string;\n /** Robots directive (e.g., \"index, follow\") */\n robots?: string;\n /** Open Graph locale (e.g., \"en_US\") */\n locale?: string;\n /** Twitter handle (e.g., \"@burdenoff\") */\n twitterHandle?: string;\n /** Theme color for mobile browsers */\n themeColor?: string;\n /**\n * Value for `<meta name=\"author\">`. Omitted by default.\n *\n * Each product site already declares its author in `index.html` as the\n * legal entity that publicly holds that product (BUR-SOP-029, \"Holder is\n * what you publish\"), and that entity differs between products. A default\n * here would add a second, conflicting author tag to every route, so the\n * tag is only rendered when a site passes the value explicitly.\n */\n author?: string;\n /** JSON-LD structured data objects */\n structuredData?: Record<string, unknown> | Array<Record<string, unknown>>;\n /** Additional meta tags */\n additionalMeta?: Array<{\n name?: string;\n property?: string;\n content: string;\n }>;\n}\n\n/**\n * Build a self-referencing canonical URL from the current location.\n *\n * Query strings and hashes are dropped (they never identify a distinct\n * canonical document here) and the trailing slash is normalised away for\n * everything but the site root. Returns `undefined` outside a DOM so the\n * component degrades to its previous behaviour under plain string rendering.\n */\nfunction canonicalFromLocation(): string | undefined {\n if (typeof window === \"undefined\" || !window.location) return undefined;\n const { origin, pathname } = window.location;\n if (!origin || origin === \"null\") return undefined;\n const path = pathname === \"/\" ? \"/\" : pathname.replace(/\\/+$/, \"\");\n return `${origin}${path}`;\n}\n\n/**\n * Absolutise an Open Graph image URL.\n *\n * Already-absolute URLs pass through. A root-relative path resolves against\n * the origin, and any other relative path against the page URL. Without a\n * resolvable base the original value is returned unchanged.\n */\nfunction resolveImageUrl(\n image: string | undefined,\n base: string | undefined,\n): string | undefined {\n if (!image || /^https?:\\/\\//i.test(image)) return image;\n if (!base) return image;\n try {\n return new URL(image, base).toString();\n } catch {\n return image;\n }\n}\n\n/**\n * JSON-LD as the body of a `<script>` element.\n *\n * `JSON.stringify` leaves `<` alone, and a script body ends at the first `</script` whatever\n * the quoting around it. The prerender serialises the live DOM with script text verbatim,\n * so a `</script><script>…` inside any string field (a caption, a title) would close the\n * JSON-LD block in the static HTML and run what follows. `\\u003c` is the same character to\n * every JSON parser; U+2028 / U+2029 are escaped for engines that still read them as line\n * breaks.\n */\nfunction serializeJsonLd(data: Record<string, unknown>): string {\n return JSON.stringify(data)\n .replace(/</g, \"\\\\u003c\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n}\n\nexport function PageHead({\n title,\n description,\n productName,\n keywords,\n image,\n url,\n favicon,\n robots = \"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\",\n locale = \"en_US\",\n twitterHandle,\n themeColor = \"#000000\",\n author,\n structuredData,\n additionalMeta,\n}: PageHeadProps) {\n const fullTitle =\n productName && !title.includes(productName)\n ? `${title} | ${productName}`\n : title;\n\n // Self-referencing canonical by default.\n //\n // Without this, a route that renders PageHead but omits `url` emits no\n // canonical at all, so the static `<link rel=\"canonical\">` in index.html\n // survives and every such page declares itself a duplicate of the home\n // page — Google then drops /pricing, /privacy, /terms and friends from the\n // index. Deriving it from the current location keeps each page pointing at\n // itself; callers that genuinely need a cross-canonical still pass `url`.\n //\n // During prerender the DOM location already reflects the route being\n // rendered, so this resolves correctly ahead of hydration.\n const resolvedUrl = url ?? canonicalFromLocation();\n\n // Root-relative images (\"/og.png\") belong to the ORIGIN, not to the current\n // path — concatenating them onto the page URL yields /features/og.png and a\n // broken share card. Anything else is resolved relative to the page itself.\n const fullImageUrl = resolveImageUrl(image, resolvedUrl);\n\n // Normalize structured data to an array\n const structuredDataItems: Array<Record<string, unknown>> = structuredData\n ? Array.isArray(structuredData)\n ? structuredData\n : [structuredData]\n : [];\n\n // Every conditional below is a `? … : null` ternary, never `x && <el />`.\n // `&&` forwards the left operand when it is falsy, so an empty-string prop\n // (keywords is legitimately \"\" on generated answer pages) would reach Helmet\n // as a string child. react-helmet-async skips any falsy child, but its\n // maintained fork @dr.pogodin/react-helmet skips only null/undefined and\n // throws `\"string\" is not a valid <Helmet> descendant` — which crashes the\n // whole page at render time. `null` is skipped by both.\n return (\n <Helmet>\n {/* Primary Meta Tags */}\n <title>{fullTitle}</title>\n <meta name=\"title\" content={fullTitle} />\n {description ? <meta name=\"description\" content={description} /> : null}\n {keywords ? <meta name=\"keywords\" content={keywords} /> : null}\n {author ? <meta name=\"author\" content={author} /> : null}\n <meta name=\"robots\" content={robots} />\n\n {/* Canonical URL */}\n {resolvedUrl ? <link rel=\"canonical\" href={resolvedUrl} /> : null}\n\n {/* Favicon */}\n {favicon ? <link rel=\"icon\" href={favicon} /> : null}\n\n {/* Open Graph / Facebook */}\n <meta property=\"og:type\" content=\"website\" />\n <meta property=\"og:title\" content={fullTitle} />\n {description ? (\n <meta property=\"og:description\" content={description} />\n ) : null}\n {fullImageUrl ? (\n <meta property=\"og:image\" content={fullImageUrl} />\n ) : null}\n {resolvedUrl ? <meta property=\"og:url\" content={resolvedUrl} /> : null}\n <meta property=\"og:locale\" content={locale} />\n {productName ? (\n <meta property=\"og:site_name\" content={productName} />\n ) : null}\n\n {/* Twitter Card */}\n <meta name=\"twitter:card\" content=\"summary_large_image\" />\n <meta name=\"twitter:title\" content={fullTitle} />\n {description ? (\n <meta name=\"twitter:description\" content={description} />\n ) : null}\n {fullImageUrl ? (\n <meta name=\"twitter:image\" content={fullImageUrl} />\n ) : null}\n {twitterHandle ? (\n <meta name=\"twitter:creator\" content={twitterHandle} />\n ) : null}\n\n {/* Theme Color */}\n <meta name=\"theme-color\" content={themeColor} />\n\n {/* JSON-LD Structured Data */}\n {structuredDataItems.map((data, index) => (\n <script key={`ld-${index}`} type=\"application/ld+json\">\n {serializeJsonLd(data)}\n </script>\n ))}\n\n {/* Additional Meta Tags */}\n {additionalMeta?.map((meta, index) => (\n <meta\n key={`meta-${index}`}\n {...(meta.name ? { name: meta.name } : {})}\n {...(meta.property ? { property: meta.property } : {})}\n content={meta.content}\n />\n ))}\n </Helmet>\n );\n}\n"]}
@@ -43,24 +43,24 @@ function PageHead({
43
43
  return /* @__PURE__ */ jsxs(Helmet, { children: [
44
44
  /* @__PURE__ */ jsx("title", { children: fullTitle }),
45
45
  /* @__PURE__ */ jsx("meta", { name: "title", content: fullTitle }),
46
- description && /* @__PURE__ */ jsx("meta", { name: "description", content: description }),
47
- keywords && /* @__PURE__ */ jsx("meta", { name: "keywords", content: keywords }),
48
- author && /* @__PURE__ */ jsx("meta", { name: "author", content: author }),
46
+ description ? /* @__PURE__ */ jsx("meta", { name: "description", content: description }) : null,
47
+ keywords ? /* @__PURE__ */ jsx("meta", { name: "keywords", content: keywords }) : null,
48
+ author ? /* @__PURE__ */ jsx("meta", { name: "author", content: author }) : null,
49
49
  /* @__PURE__ */ jsx("meta", { name: "robots", content: robots }),
50
- resolvedUrl && /* @__PURE__ */ jsx("link", { rel: "canonical", href: resolvedUrl }),
51
- favicon && /* @__PURE__ */ jsx("link", { rel: "icon", href: favicon }),
50
+ resolvedUrl ? /* @__PURE__ */ jsx("link", { rel: "canonical", href: resolvedUrl }) : null,
51
+ favicon ? /* @__PURE__ */ jsx("link", { rel: "icon", href: favicon }) : null,
52
52
  /* @__PURE__ */ jsx("meta", { property: "og:type", content: "website" }),
53
53
  /* @__PURE__ */ jsx("meta", { property: "og:title", content: fullTitle }),
54
- description && /* @__PURE__ */ jsx("meta", { property: "og:description", content: description }),
55
- fullImageUrl && /* @__PURE__ */ jsx("meta", { property: "og:image", content: fullImageUrl }),
56
- resolvedUrl && /* @__PURE__ */ jsx("meta", { property: "og:url", content: resolvedUrl }),
54
+ description ? /* @__PURE__ */ jsx("meta", { property: "og:description", content: description }) : null,
55
+ fullImageUrl ? /* @__PURE__ */ jsx("meta", { property: "og:image", content: fullImageUrl }) : null,
56
+ resolvedUrl ? /* @__PURE__ */ jsx("meta", { property: "og:url", content: resolvedUrl }) : null,
57
57
  /* @__PURE__ */ jsx("meta", { property: "og:locale", content: locale }),
58
- productName && /* @__PURE__ */ jsx("meta", { property: "og:site_name", content: productName }),
58
+ productName ? /* @__PURE__ */ jsx("meta", { property: "og:site_name", content: productName }) : null,
59
59
  /* @__PURE__ */ jsx("meta", { name: "twitter:card", content: "summary_large_image" }),
60
60
  /* @__PURE__ */ jsx("meta", { name: "twitter:title", content: fullTitle }),
61
- description && /* @__PURE__ */ jsx("meta", { name: "twitter:description", content: description }),
62
- fullImageUrl && /* @__PURE__ */ jsx("meta", { name: "twitter:image", content: fullImageUrl }),
63
- twitterHandle && /* @__PURE__ */ jsx("meta", { name: "twitter:creator", content: twitterHandle }),
61
+ description ? /* @__PURE__ */ jsx("meta", { name: "twitter:description", content: description }) : null,
62
+ fullImageUrl ? /* @__PURE__ */ jsx("meta", { name: "twitter:image", content: fullImageUrl }) : null,
63
+ twitterHandle ? /* @__PURE__ */ jsx("meta", { name: "twitter:creator", content: twitterHandle }) : null,
64
64
  /* @__PURE__ */ jsx("meta", { name: "theme-color", content: themeColor }),
65
65
  structuredDataItems.map((data, index) => /* @__PURE__ */ jsx("script", { type: "application/ld+json", children: serializeJsonLd(data) }, `ld-${index}`)),
66
66
  additionalMeta?.map((meta, index) => /* @__PURE__ */ jsx(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/seo/page-head.tsx"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * PageHead Component\n * Manages dynamic meta tags for SEO optimization on a per-page basis.\n * Supports JSON-LD structured data, Open Graph, Twitter Cards, favicons, and robots directives.\n *\n * @example\n * ```tsx\n * <PageHead\n * title=\"About Us\"\n * description=\"Learn about our mission and team\"\n * productName=\"MyProduct\"\n * keywords=\"about, team, mission\"\n * image=\"/og-about.png\"\n * url=\"https://myproduct.com/about\"\n * favicon=\"/favicon.ico\"\n * robots=\"index, follow\"\n * locale=\"en_US\"\n * twitterHandle=\"@burdenoff\"\n * themeColor=\"#1a1a2e\"\n * structuredData={{\n * \"@context\": \"https://schema.org\",\n * \"@type\": \"WebPage\",\n * name: \"About Us\",\n * }}\n * />\n * ```\n */\n\nimport { Helmet } from \"react-helmet-async\";\n\nexport interface PageHeadProps {\n /** Page title */\n title: string;\n /** Meta description */\n description?: string;\n /** Product/brand name (appended to title) */\n productName?: string;\n /** SEO keywords */\n keywords?: string;\n /** Open Graph image URL */\n image?: string;\n /** Page canonical URL */\n url?: string;\n /** Favicon URL */\n favicon?: string;\n /** Robots directive (e.g., \"index, follow\") */\n robots?: string;\n /** Open Graph locale (e.g., \"en_US\") */\n locale?: string;\n /** Twitter handle (e.g., \"@burdenoff\") */\n twitterHandle?: string;\n /** Theme color for mobile browsers */\n themeColor?: string;\n /**\n * Value for `<meta name=\"author\">`. Omitted by default.\n *\n * Each product site already declares its author in `index.html` as the\n * legal entity that publicly holds that product (BUR-SOP-029, \"Holder is\n * what you publish\"), and that entity differs between products. A default\n * here would add a second, conflicting author tag to every route, so the\n * tag is only rendered when a site passes the value explicitly.\n */\n author?: string;\n /** JSON-LD structured data objects */\n structuredData?: Record<string, unknown> | Array<Record<string, unknown>>;\n /** Additional meta tags */\n additionalMeta?: Array<{\n name?: string;\n property?: string;\n content: string;\n }>;\n}\n\n/**\n * Build a self-referencing canonical URL from the current location.\n *\n * Query strings and hashes are dropped (they never identify a distinct\n * canonical document here) and the trailing slash is normalised away for\n * everything but the site root. Returns `undefined` outside a DOM so the\n * component degrades to its previous behaviour under plain string rendering.\n */\nfunction canonicalFromLocation(): string | undefined {\n if (typeof window === \"undefined\" || !window.location) return undefined;\n const { origin, pathname } = window.location;\n if (!origin || origin === \"null\") return undefined;\n const path = pathname === \"/\" ? \"/\" : pathname.replace(/\\/+$/, \"\");\n return `${origin}${path}`;\n}\n\n/**\n * Absolutise an Open Graph image URL.\n *\n * Already-absolute URLs pass through. A root-relative path resolves against\n * the origin, and any other relative path against the page URL. Without a\n * resolvable base the original value is returned unchanged.\n */\nfunction resolveImageUrl(\n image: string | undefined,\n base: string | undefined,\n): string | undefined {\n if (!image || /^https?:\\/\\//i.test(image)) return image;\n if (!base) return image;\n try {\n return new URL(image, base).toString();\n } catch {\n return image;\n }\n}\n\n/**\n * JSON-LD as the body of a `<script>` element.\n *\n * `JSON.stringify` leaves `<` alone, and a script body ends at the first `</script` whatever\n * the quoting around it. The prerender serialises the live DOM with script text verbatim,\n * so a `</script><script>…` inside any string field (a caption, a title) would close the\n * JSON-LD block in the static HTML and run what follows. `\\u003c` is the same character to\n * every JSON parser; U+2028 / U+2029 are escaped for engines that still read them as line\n * breaks.\n */\nfunction serializeJsonLd(data: Record<string, unknown>): string {\n return JSON.stringify(data)\n .replace(/</g, \"\\\\u003c\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n}\n\nexport function PageHead({\n title,\n description,\n productName,\n keywords,\n image,\n url,\n favicon,\n robots = \"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\",\n locale = \"en_US\",\n twitterHandle,\n themeColor = \"#000000\",\n author,\n structuredData,\n additionalMeta,\n}: PageHeadProps) {\n const fullTitle =\n productName && !title.includes(productName)\n ? `${title} | ${productName}`\n : title;\n\n // Self-referencing canonical by default.\n //\n // Without this, a route that renders PageHead but omits `url` emits no\n // canonical at all, so the static `<link rel=\"canonical\">` in index.html\n // survives and every such page declares itself a duplicate of the home\n // page — Google then drops /pricing, /privacy, /terms and friends from the\n // index. Deriving it from the current location keeps each page pointing at\n // itself; callers that genuinely need a cross-canonical still pass `url`.\n //\n // During prerender the DOM location already reflects the route being\n // rendered, so this resolves correctly ahead of hydration.\n const resolvedUrl = url ?? canonicalFromLocation();\n\n // Root-relative images (\"/og.png\") belong to the ORIGIN, not to the current\n // path — concatenating them onto the page URL yields /features/og.png and a\n // broken share card. Anything else is resolved relative to the page itself.\n const fullImageUrl = resolveImageUrl(image, resolvedUrl);\n\n // Normalize structured data to an array\n const structuredDataItems: Array<Record<string, unknown>> = structuredData\n ? Array.isArray(structuredData)\n ? structuredData\n : [structuredData]\n : [];\n\n return (\n <Helmet>\n {/* Primary Meta Tags */}\n <title>{fullTitle}</title>\n <meta name=\"title\" content={fullTitle} />\n {description && <meta name=\"description\" content={description} />}\n {keywords && <meta name=\"keywords\" content={keywords} />}\n {author && <meta name=\"author\" content={author} />}\n <meta name=\"robots\" content={robots} />\n\n {/* Canonical URL */}\n {resolvedUrl && <link rel=\"canonical\" href={resolvedUrl} />}\n\n {/* Favicon */}\n {favicon && <link rel=\"icon\" href={favicon} />}\n\n {/* Open Graph / Facebook */}\n <meta property=\"og:type\" content=\"website\" />\n <meta property=\"og:title\" content={fullTitle} />\n {description && <meta property=\"og:description\" content={description} />}\n {fullImageUrl && <meta property=\"og:image\" content={fullImageUrl} />}\n {resolvedUrl && <meta property=\"og:url\" content={resolvedUrl} />}\n <meta property=\"og:locale\" content={locale} />\n {productName && <meta property=\"og:site_name\" content={productName} />}\n\n {/* Twitter Card */}\n <meta name=\"twitter:card\" content=\"summary_large_image\" />\n <meta name=\"twitter:title\" content={fullTitle} />\n {description && <meta name=\"twitter:description\" content={description} />}\n {fullImageUrl && <meta name=\"twitter:image\" content={fullImageUrl} />}\n {twitterHandle && <meta name=\"twitter:creator\" content={twitterHandle} />}\n\n {/* Theme Color */}\n <meta name=\"theme-color\" content={themeColor} />\n\n {/* JSON-LD Structured Data */}\n {structuredDataItems.map((data, index) => (\n <script key={`ld-${index}`} type=\"application/ld+json\">\n {serializeJsonLd(data)}\n </script>\n ))}\n\n {/* Additional Meta Tags */}\n {additionalMeta?.map((meta, index) => (\n <meta\n key={`meta-${index}`}\n {...(meta.name ? { name: meta.name } : {})}\n {...(meta.property ? { property: meta.property } : {})}\n content={meta.content}\n />\n ))}\n </Helmet>\n );\n}\n"]}
1
+ {"version":3,"sources":["../../src/seo/page-head.tsx"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * PageHead Component\n * Manages dynamic meta tags for SEO optimization on a per-page basis.\n * Supports JSON-LD structured data, Open Graph, Twitter Cards, favicons, and robots directives.\n *\n * @example\n * ```tsx\n * <PageHead\n * title=\"About Us\"\n * description=\"Learn about our mission and team\"\n * productName=\"MyProduct\"\n * keywords=\"about, team, mission\"\n * image=\"/og-about.png\"\n * url=\"https://myproduct.com/about\"\n * favicon=\"/favicon.ico\"\n * robots=\"index, follow\"\n * locale=\"en_US\"\n * twitterHandle=\"@burdenoff\"\n * themeColor=\"#1a1a2e\"\n * structuredData={{\n * \"@context\": \"https://schema.org\",\n * \"@type\": \"WebPage\",\n * name: \"About Us\",\n * }}\n * />\n * ```\n */\n\nimport { Helmet } from \"react-helmet-async\";\n\nexport interface PageHeadProps {\n /** Page title */\n title: string;\n /** Meta description */\n description?: string;\n /** Product/brand name (appended to title) */\n productName?: string;\n /** SEO keywords */\n keywords?: string;\n /** Open Graph image URL */\n image?: string;\n /** Page canonical URL */\n url?: string;\n /** Favicon URL */\n favicon?: string;\n /** Robots directive (e.g., \"index, follow\") */\n robots?: string;\n /** Open Graph locale (e.g., \"en_US\") */\n locale?: string;\n /** Twitter handle (e.g., \"@burdenoff\") */\n twitterHandle?: string;\n /** Theme color for mobile browsers */\n themeColor?: string;\n /**\n * Value for `<meta name=\"author\">`. Omitted by default.\n *\n * Each product site already declares its author in `index.html` as the\n * legal entity that publicly holds that product (BUR-SOP-029, \"Holder is\n * what you publish\"), and that entity differs between products. A default\n * here would add a second, conflicting author tag to every route, so the\n * tag is only rendered when a site passes the value explicitly.\n */\n author?: string;\n /** JSON-LD structured data objects */\n structuredData?: Record<string, unknown> | Array<Record<string, unknown>>;\n /** Additional meta tags */\n additionalMeta?: Array<{\n name?: string;\n property?: string;\n content: string;\n }>;\n}\n\n/**\n * Build a self-referencing canonical URL from the current location.\n *\n * Query strings and hashes are dropped (they never identify a distinct\n * canonical document here) and the trailing slash is normalised away for\n * everything but the site root. Returns `undefined` outside a DOM so the\n * component degrades to its previous behaviour under plain string rendering.\n */\nfunction canonicalFromLocation(): string | undefined {\n if (typeof window === \"undefined\" || !window.location) return undefined;\n const { origin, pathname } = window.location;\n if (!origin || origin === \"null\") return undefined;\n const path = pathname === \"/\" ? \"/\" : pathname.replace(/\\/+$/, \"\");\n return `${origin}${path}`;\n}\n\n/**\n * Absolutise an Open Graph image URL.\n *\n * Already-absolute URLs pass through. A root-relative path resolves against\n * the origin, and any other relative path against the page URL. Without a\n * resolvable base the original value is returned unchanged.\n */\nfunction resolveImageUrl(\n image: string | undefined,\n base: string | undefined,\n): string | undefined {\n if (!image || /^https?:\\/\\//i.test(image)) return image;\n if (!base) return image;\n try {\n return new URL(image, base).toString();\n } catch {\n return image;\n }\n}\n\n/**\n * JSON-LD as the body of a `<script>` element.\n *\n * `JSON.stringify` leaves `<` alone, and a script body ends at the first `</script` whatever\n * the quoting around it. The prerender serialises the live DOM with script text verbatim,\n * so a `</script><script>…` inside any string field (a caption, a title) would close the\n * JSON-LD block in the static HTML and run what follows. `\\u003c` is the same character to\n * every JSON parser; U+2028 / U+2029 are escaped for engines that still read them as line\n * breaks.\n */\nfunction serializeJsonLd(data: Record<string, unknown>): string {\n return JSON.stringify(data)\n .replace(/</g, \"\\\\u003c\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n}\n\nexport function PageHead({\n title,\n description,\n productName,\n keywords,\n image,\n url,\n favicon,\n robots = \"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\",\n locale = \"en_US\",\n twitterHandle,\n themeColor = \"#000000\",\n author,\n structuredData,\n additionalMeta,\n}: PageHeadProps) {\n const fullTitle =\n productName && !title.includes(productName)\n ? `${title} | ${productName}`\n : title;\n\n // Self-referencing canonical by default.\n //\n // Without this, a route that renders PageHead but omits `url` emits no\n // canonical at all, so the static `<link rel=\"canonical\">` in index.html\n // survives and every such page declares itself a duplicate of the home\n // page — Google then drops /pricing, /privacy, /terms and friends from the\n // index. Deriving it from the current location keeps each page pointing at\n // itself; callers that genuinely need a cross-canonical still pass `url`.\n //\n // During prerender the DOM location already reflects the route being\n // rendered, so this resolves correctly ahead of hydration.\n const resolvedUrl = url ?? canonicalFromLocation();\n\n // Root-relative images (\"/og.png\") belong to the ORIGIN, not to the current\n // path — concatenating them onto the page URL yields /features/og.png and a\n // broken share card. Anything else is resolved relative to the page itself.\n const fullImageUrl = resolveImageUrl(image, resolvedUrl);\n\n // Normalize structured data to an array\n const structuredDataItems: Array<Record<string, unknown>> = structuredData\n ? Array.isArray(structuredData)\n ? structuredData\n : [structuredData]\n : [];\n\n // Every conditional below is a `? … : null` ternary, never `x && <el />`.\n // `&&` forwards the left operand when it is falsy, so an empty-string prop\n // (keywords is legitimately \"\" on generated answer pages) would reach Helmet\n // as a string child. react-helmet-async skips any falsy child, but its\n // maintained fork @dr.pogodin/react-helmet skips only null/undefined and\n // throws `\"string\" is not a valid <Helmet> descendant` — which crashes the\n // whole page at render time. `null` is skipped by both.\n return (\n <Helmet>\n {/* Primary Meta Tags */}\n <title>{fullTitle}</title>\n <meta name=\"title\" content={fullTitle} />\n {description ? <meta name=\"description\" content={description} /> : null}\n {keywords ? <meta name=\"keywords\" content={keywords} /> : null}\n {author ? <meta name=\"author\" content={author} /> : null}\n <meta name=\"robots\" content={robots} />\n\n {/* Canonical URL */}\n {resolvedUrl ? <link rel=\"canonical\" href={resolvedUrl} /> : null}\n\n {/* Favicon */}\n {favicon ? <link rel=\"icon\" href={favicon} /> : null}\n\n {/* Open Graph / Facebook */}\n <meta property=\"og:type\" content=\"website\" />\n <meta property=\"og:title\" content={fullTitle} />\n {description ? (\n <meta property=\"og:description\" content={description} />\n ) : null}\n {fullImageUrl ? (\n <meta property=\"og:image\" content={fullImageUrl} />\n ) : null}\n {resolvedUrl ? <meta property=\"og:url\" content={resolvedUrl} /> : null}\n <meta property=\"og:locale\" content={locale} />\n {productName ? (\n <meta property=\"og:site_name\" content={productName} />\n ) : null}\n\n {/* Twitter Card */}\n <meta name=\"twitter:card\" content=\"summary_large_image\" />\n <meta name=\"twitter:title\" content={fullTitle} />\n {description ? (\n <meta name=\"twitter:description\" content={description} />\n ) : null}\n {fullImageUrl ? (\n <meta name=\"twitter:image\" content={fullImageUrl} />\n ) : null}\n {twitterHandle ? (\n <meta name=\"twitter:creator\" content={twitterHandle} />\n ) : null}\n\n {/* Theme Color */}\n <meta name=\"theme-color\" content={themeColor} />\n\n {/* JSON-LD Structured Data */}\n {structuredDataItems.map((data, index) => (\n <script key={`ld-${index}`} type=\"application/ld+json\">\n {serializeJsonLd(data)}\n </script>\n ))}\n\n {/* Additional Meta Tags */}\n {additionalMeta?.map((meta, index) => (\n <meta\n key={`meta-${index}`}\n {...(meta.name ? { name: meta.name } : {})}\n {...(meta.property ? { property: meta.property } : {})}\n content={meta.content}\n />\n ))}\n </Helmet>\n );\n}\n"]}
@@ -493,24 +493,24 @@ function PageHead({
493
493
  return /* @__PURE__ */ jsxRuntime.jsxs(reactHelmetAsync.Helmet, { children: [
494
494
  /* @__PURE__ */ jsxRuntime.jsx("title", { children: fullTitle }),
495
495
  /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "title", content: fullTitle }),
496
- description && /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "description", content: description }),
497
- keywords && /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "keywords", content: keywords }),
498
- author && /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "author", content: author }),
496
+ description ? /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "description", content: description }) : null,
497
+ keywords ? /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "keywords", content: keywords }) : null,
498
+ author ? /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "author", content: author }) : null,
499
499
  /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "robots", content: robots }),
500
- resolvedUrl && /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "canonical", href: resolvedUrl }),
501
- favicon && /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "icon", href: favicon }),
500
+ resolvedUrl ? /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "canonical", href: resolvedUrl }) : null,
501
+ favicon ? /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "icon", href: favicon }) : null,
502
502
  /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:type", content: "website" }),
503
503
  /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:title", content: fullTitle }),
504
- description && /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:description", content: description }),
505
- fullImageUrl && /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:image", content: fullImageUrl }),
506
- resolvedUrl && /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:url", content: resolvedUrl }),
504
+ description ? /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:description", content: description }) : null,
505
+ fullImageUrl ? /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:image", content: fullImageUrl }) : null,
506
+ resolvedUrl ? /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:url", content: resolvedUrl }) : null,
507
507
  /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:locale", content: locale }),
508
- productName && /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:site_name", content: productName }),
508
+ productName ? /* @__PURE__ */ jsxRuntime.jsx("meta", { property: "og:site_name", content: productName }) : null,
509
509
  /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "twitter:card", content: "summary_large_image" }),
510
510
  /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "twitter:title", content: fullTitle }),
511
- description && /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "twitter:description", content: description }),
512
- fullImageUrl && /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "twitter:image", content: fullImageUrl }),
513
- twitterHandle && /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "twitter:creator", content: twitterHandle }),
511
+ description ? /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "twitter:description", content: description }) : null,
512
+ fullImageUrl ? /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "twitter:image", content: fullImageUrl }) : null,
513
+ twitterHandle ? /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "twitter:creator", content: twitterHandle }) : null,
514
514
  /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "theme-color", content: themeColor }),
515
515
  structuredDataItems.map((data, index) => /* @__PURE__ */ jsxRuntime.jsx("script", { type: "application/ld+json", children: serializeJsonLd(data) }, `ld-${index}`)),
516
516
  additionalMeta?.map((meta, index) => /* @__PURE__ */ jsxRuntime.jsx(