{"version":3,"file":"index.mjs","names":[],"sources":["../../src/seo/index.ts"],"sourcesContent":["/**\n * SEO Helpers\n *\n * Public API functions for generating SEO meta tags in Astro templates.\n *\n * @example\n * ```astro\n * ---\n * import { getEmDashEntry } from \"@premium-cms/emdash\";\n * import { getSeoMeta } from \"@premium-cms/emdash/seo\";\n *\n * const post = await getEmDashEntry(\"posts\", Astro.params.slug);\n * const meta = await getSeoMeta(post, {\n *   siteTitle: \"My Blog\",\n *   siteUrl: Astro.url.origin,\n * });\n * ---\n * <html>\n *   <head>\n *     <title>{meta.title}</title>\n *     <meta name=\"description\" content={meta.description} />\n *     <meta property=\"og:title\" content={meta.ogTitle} />\n *     <meta property=\"og:description\" content={meta.ogDescription} />\n *     {meta.ogImage && <meta property=\"og:image\" content={meta.ogImage} />}\n *     <link rel=\"canonical\" href={meta.canonical} />\n *     {meta.robots && <meta name=\"robots\" content={meta.robots} />}\n *   </head>\n * </html>\n * ```\n */\n\nimport type { ContentSeo } from \"../database/repositories/types.js\";\nimport { buildSeoImageUrl } from \"./media-url.js\";\n\nexport { getHreflangAlternates, getHreflangAlternatesWithDb } from \"./hreflang.js\";\nexport type { HreflangAlternate, HreflangOptions } from \"./hreflang.js\";\n\nconst TRAILING_SLASH_RE = /\\/$/;\nconst ABSOLUTE_URL_RE = /^https?:\\/\\//i;\n\n/**\n * Content input for SEO functions.\n * Accepts both ContentEntry<T> (from query functions) and ContentItem (internal).\n */\nexport interface SeoContentInput<T = Record<string, unknown>> {\n\t/** Content data object */\n\tdata: T & {\n\t\ttitle?: unknown;\n\t\texcerpt?: unknown;\n\t\tseo?: ContentSeo;\n\t};\n\t/** SEO metadata (legacy location, prefer data.seo) */\n\tseo?: ContentSeo;\n}\n\n/** Resolved SEO meta tags ready for use in templates */\nexport interface SeoMeta {\n\t/** Full <title> tag content (e.g., \"Post Title | Site Name\") */\n\ttitle: string;\n\t/** Meta description */\n\tdescription: string | null;\n\t/** OG title (same as title by default) */\n\togTitle: string;\n\t/** OG description */\n\togDescription: string | null;\n\t/** OG image URL (absolute) */\n\togImage: string | null;\n\t/** Canonical URL */\n\tcanonical: string | null;\n\t/** Robots directive (e.g., \"noindex, nofollow\") or null if default */\n\trobots: string | null;\n}\n\n/** Options for generating SEO meta from a content item */\nexport interface SeoMetaOptions {\n\t/** Site title for the suffix (e.g., \"My Blog\") */\n\tsiteTitle?: string;\n\t/** Site URL origin for building absolute URLs (e.g., \"https://example.com\") */\n\tsiteUrl?: string;\n\t/** Title separator between page title and site title */\n\ttitleSeparator?: string;\n\t/** Path to this content (e.g., \"/posts/my-post\") for canonical fallback */\n\tpath?: string;\n\t/** Default OG image URL if content has none */\n\tdefaultOgImage?: string;\n\t/**\n\t * Default page title used when the SEO panel has none. Ranks above the\n\t * `data.title` fallback, so computed titles (e.g. `` `${title} (cover of\n\t * ${artist})` ``) apply while an editor-set SEO title still wins.\n\t */\n\tdefaultTitle?: string;\n\t/**\n\t * Default description used when the SEO panel has none. Ranks above the\n\t * `data.excerpt` fallback; an editor-set SEO description still wins.\n\t */\n\tdefaultDescription?: string;\n}\n\n/**\n * Generate resolved SEO meta tags from a content item.\n *\n * Uses the content item's SEO fields, falling back to content data\n * (title from `data.title`, description from `data.excerpt`).\n *\n * @param content - The content item (from getEmDashEntry, etc.)\n * @param options - Configuration for title construction, canonical URLs, etc.\n * @returns Resolved meta tags ready for template use\n */\nexport function getSeoMeta<T>(content: SeoContentInput<T>, options: SeoMetaOptions = {}): SeoMeta {\n\tconst { siteTitle, siteUrl, path, defaultOgImage, defaultTitle, defaultDescription } = options;\n\tconst separator = options.titleSeparator || \" | \";\n\t// SEO can be in content.seo (ContentItem) or content.data.seo (ContentEntry)\n\tconst seo = content.seo ??\n\t\tcontent.data.seo ?? {\n\t\t\ttitle: null,\n\t\t\tdescription: null,\n\t\t\timage: null,\n\t\t\tcanonical: null,\n\t\t\tnoIndex: false,\n\t\t};\n\n\t// Title: SEO panel title > caller default > content title\n\tconst pageTitle =\n\t\tseo.title ||\n\t\tdefaultTitle ||\n\t\t(typeof content.data.title === \"string\" ? content.data.title : null) ||\n\t\t\"\";\n\n\tconst fullTitle = siteTitle && pageTitle ? `${pageTitle}${separator}${siteTitle}` : pageTitle;\n\n\t// Description: SEO panel description > caller default > excerpt\n\tconst description =\n\t\tseo.description ||\n\t\tdefaultDescription ||\n\t\t(typeof content.data.excerpt === \"string\" ? content.data.excerpt : null) ||\n\t\tnull;\n\n\t// OG image: SEO image > default\n\tconst ogImage = seo.image ? buildSeoImageUrl(seo.image, siteUrl) : (defaultOgImage ?? null);\n\n\t// Canonical: explicit > path-based > null\n\tlet canonical: string | null = null;\n\tif (seo.canonical) {\n\t\t// Ensure relative canonical paths get a leading slash so we don't\n\t\t// produce \"https://example.composts/x\" when joined with siteUrl\n\t\tif (siteUrl && !seo.canonical.startsWith(\"/\") && !ABSOLUTE_URL_RE.test(seo.canonical)) {\n\t\t\tcanonical = `${siteUrl.replace(TRAILING_SLASH_RE, \"\")}/${seo.canonical}`;\n\t\t} else {\n\t\t\tcanonical = seo.canonical;\n\t\t}\n\t} else if (siteUrl && path) {\n\t\tconst safePath = path.startsWith(\"/\") ? path : `/${path}`;\n\t\tcanonical = `${siteUrl.replace(TRAILING_SLASH_RE, \"\")}${safePath}`;\n\t}\n\n\t// Robots\n\tconst robots = seo.noIndex ? \"noindex, nofollow\" : null;\n\n\treturn {\n\t\ttitle: fullTitle,\n\t\tdescription,\n\t\togTitle: pageTitle || fullTitle,\n\t\togDescription: description,\n\t\togImage,\n\t\tcanonical,\n\t\trobots,\n\t};\n}\n\n/**\n * Extract SEO data from a content item.\n *\n * Convenience accessor for the raw SEO fields without template resolution.\n *\n * @param content - The content item\n * @returns The content's SEO fields\n */\nexport function getContentSeo<T>(content: SeoContentInput<T>): ContentSeo | undefined {\n\treturn content.seo ?? content.data.seo;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAqCA,MAAM,oBAAoB;AAC1B,MAAM,kBAAkB;;;;;;;;;;;AAsExB,SAAgB,WAAc,SAA6B,UAA0B,EAAE,EAAW;CACjG,MAAM,EAAE,WAAW,SAAS,MAAM,gBAAgB,cAAc,uBAAuB;CACvF,MAAM,YAAY,QAAQ,kBAAkB;CAE5C,MAAM,MAAM,QAAQ,OACnB,QAAQ,KAAK,OAAO;EACnB,OAAO;EACP,aAAa;EACb,OAAO;EACP,WAAW;EACX,SAAS;EACT;CAGF,MAAM,YACL,IAAI,SACJ,iBACC,OAAO,QAAQ,KAAK,UAAU,WAAW,QAAQ,KAAK,QAAQ,SAC/D;CAED,MAAM,YAAY,aAAa,YAAY,GAAG,YAAY,YAAY,cAAc;CAGpF,MAAM,cACL,IAAI,eACJ,uBACC,OAAO,QAAQ,KAAK,YAAY,WAAW,QAAQ,KAAK,UAAU,SACnE;CAGD,MAAM,UAAU,IAAI,QAAQ,iBAAiB,IAAI,OAAO,QAAQ,GAAI,kBAAkB;CAGtF,IAAI,YAA2B;AAC/B,KAAI,IAAI,UAGP,KAAI,WAAW,CAAC,IAAI,UAAU,WAAW,IAAI,IAAI,CAAC,gBAAgB,KAAK,IAAI,UAAU,CACpF,aAAY,GAAG,QAAQ,QAAQ,mBAAmB,GAAG,CAAC,GAAG,IAAI;KAE7D,aAAY,IAAI;UAEP,WAAW,MAAM;EAC3B,MAAM,WAAW,KAAK,WAAW,IAAI,GAAG,OAAO,IAAI;AACnD,cAAY,GAAG,QAAQ,QAAQ,mBAAmB,GAAG,GAAG;;CAIzD,MAAM,SAAS,IAAI,UAAU,sBAAsB;AAEnD,QAAO;EACN,OAAO;EACP;EACA,SAAS,aAAa;EACtB,eAAe;EACf;EACA;EACA;EACA;;;;;;;;;;AAWF,SAAgB,cAAiB,SAAqD;AACrF,QAAO,QAAQ,OAAO,QAAQ,KAAK"}