Initial commit

This commit is contained in:
2026-08-02 20:55:23 +02:00
parent 411f484b40
commit d558d18d9a
295 changed files with 9 additions and 5880 deletions
-21
View File
@@ -1,21 +0,0 @@
import type { CollectionBeforeChangeHook } from 'payload';
/**
* Maps document fields to SEO meta fields for auto-fill.
* Defaults match the Payload website template (title → meta.title).
*/
export type AutoFillMapping = {
/** Document field used to fill meta.description when empty. */
description?: string;
/** Document field used to fill meta.title when empty. Default: 'title'. */
title?: string;
};
/**
* Builds a beforeChange hook that fills empty SEO meta fields from document
* content. Only fills when the meta field is blank — never overwrites what an
* editor typed. This runs server-side on every create/update, so editors get
* sensible meta without clicking "auto-generate".
*
* The plugin doesn't know the client's collection shape, so the field mapping
* is configurable; defaults follow the website template.
*/
export declare function buildAutoFillMetaHook(mapping?: AutoFillMapping): CollectionBeforeChangeHook;
-47
View File
@@ -1,47 +0,0 @@
/**
* Reads a possibly-localized field value into a plain string.
* With localized fields at this hook stage the value is the active-locale
* string, so we just coerce defensively.
*/ function readString(value) {
if (typeof value === 'string' && value.trim()) {
return value.trim();
}
return undefined;
}
/**
* Builds a beforeChange hook that fills empty SEO meta fields from document
* content. Only fills when the meta field is blank — never overwrites what an
* editor typed. This runs server-side on every create/update, so editors get
* sensible meta without clicking "auto-generate".
*
* The plugin doesn't know the client's collection shape, so the field mapping
* is configurable; defaults follow the website template.
*/ export function buildAutoFillMetaHook(mapping = {}) {
const titleField = mapping.title ?? 'title';
const descriptionField = mapping.description;
return ({ data })=>{
if (!data) {
return data;
}
// plugin-seo stores meta as a group under `meta`
const meta = data.meta ?? {};
// Fill meta.title from the document title when empty
if (!readString(meta.title)) {
const sourceTitle = readString(data[titleField]);
if (sourceTitle) {
meta.title = sourceTitle;
}
}
// Fill meta.description from a configured field when empty
if (descriptionField && !readString(meta.description)) {
const sourceDescription = readString(data[descriptionField]);
if (sourceDescription) {
meta.description = sourceDescription;
}
}
data.meta = meta;
return data;
};
}
//# sourceMappingURL=autoFillMeta.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":["../../../src/modules/seo/autoFillMeta.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\n/**\n * Maps document fields to SEO meta fields for auto-fill.\n * Defaults match the Payload website template (title → meta.title).\n */\nexport type AutoFillMapping = {\n /** Document field used to fill meta.description when empty. */\n description?: string\n /** Document field used to fill meta.title when empty. Default: 'title'. */\n title?: string\n}\n\n/**\n * Reads a possibly-localized field value into a plain string.\n * With localized fields at this hook stage the value is the active-locale\n * string, so we just coerce defensively.\n */\nfunction readString(value: unknown): string | undefined {\n if (typeof value === 'string' && value.trim()) {return value.trim()}\n return undefined\n}\n\n/**\n * Builds a beforeChange hook that fills empty SEO meta fields from document\n * content. Only fills when the meta field is blank — never overwrites what an\n * editor typed. This runs server-side on every create/update, so editors get\n * sensible meta without clicking \"auto-generate\".\n *\n * The plugin doesn't know the client's collection shape, so the field mapping\n * is configurable; defaults follow the website template.\n */\nexport function buildAutoFillMetaHook(mapping: AutoFillMapping = {}): CollectionBeforeChangeHook {\n const titleField = mapping.title ?? 'title'\n const descriptionField = mapping.description\n\n return ({ data }) => {\n if (!data) {return data}\n\n // plugin-seo stores meta as a group under `meta`\n const meta = (data.meta as Record<string, unknown> | undefined) ?? {}\n\n // Fill meta.title from the document title when empty\n if (!readString(meta.title)) {\n const sourceTitle = readString(data[titleField])\n if (sourceTitle) {meta.title = sourceTitle}\n }\n\n // Fill meta.description from a configured field when empty\n if (descriptionField && !readString(meta.description)) {\n const sourceDescription = readString(data[descriptionField])\n if (sourceDescription) {meta.description = sourceDescription}\n }\n\n data.meta = meta\n return data\n }\n}\n"],"names":["readString","value","trim","undefined","buildAutoFillMetaHook","mapping","titleField","title","descriptionField","description","data","meta","sourceTitle","sourceDescription"],"mappings":"AAaA;;;;CAIC,GACD,SAASA,WAAWC,KAAc;IAChC,IAAI,OAAOA,UAAU,YAAYA,MAAMC,IAAI,IAAI;QAAC,OAAOD,MAAMC,IAAI;IAAE;IACnE,OAAOC;AACT;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASC,sBAAsBC,UAA2B,CAAC,CAAC;IACjE,MAAMC,aAAaD,QAAQE,KAAK,IAAI;IACpC,MAAMC,mBAAmBH,QAAQI,WAAW;IAE5C,OAAO,CAAC,EAAEC,IAAI,EAAE;QACd,IAAI,CAACA,MAAM;YAAC,OAAOA;QAAI;QAEvB,iDAAiD;QACjD,MAAMC,OAAO,AAACD,KAAKC,IAAI,IAA4C,CAAC;QAEpE,qDAAqD;QACrD,IAAI,CAACX,WAAWW,KAAKJ,KAAK,GAAG;YAC3B,MAAMK,cAAcZ,WAAWU,IAAI,CAACJ,WAAW;YAC/C,IAAIM,aAAa;gBAACD,KAAKJ,KAAK,GAAGK;YAAW;QAC5C;QAEA,2DAA2D;QAC3D,IAAIJ,oBAAoB,CAACR,WAAWW,KAAKF,WAAW,GAAG;YACrD,MAAMI,oBAAoBb,WAAWU,IAAI,CAACF,iBAAiB;YAC3D,IAAIK,mBAAmB;gBAACF,KAAKF,WAAW,GAAGI;YAAiB;QAC9D;QAEAH,KAAKC,IAAI,GAAGA;QACZ,OAAOD;IACT;AACF"}
-69
View File
@@ -1,69 +0,0 @@
import type { I18nConfig, LocalizedSlugs } from '../i18n/index.js';
import type { TitleOrder } from './composeTitle.js';
import type { SeoMeta } from './types.js';
/**
* Subset of Next.js `Metadata` this helper produces. Kept local so the plugin
* doesn't depend on `next` types; the shape is assignable to Next's Metadata.
*/
export type PageMetadata = {
alternates?: {
canonical?: string;
languages?: Record<string, string>;
};
description?: string;
openGraph?: {
description?: string;
images?: {
url: string;
}[];
locale?: string;
title: string;
};
title: string;
};
type BuildMetadataArgs = {
/** Absolute site origin, e.g. 'https://example.com'. */
baseUrl?: string;
config: I18nConfig;
/** Home slug that collapses to the locale root. Defaults to 'home'. */
homeSlug?: string;
/** Resolved OG image URL (page image or site defaultShareImage). */
imageUrl?: null | string;
/** Current locale being rendered. */
locale: string;
/** SEO meta from the document (plugin-seo group). */
meta?: null | SeoMeta;
/** Page title or site name first. Defaults to 'page-first'. */
order?: TitleOrder;
/**
* Localized segment the document lives under (an archive page's slugs).
* Feeds both canonical and hreflang, so /pl/artykuly/moj-post and
* /en/articles/my-post point at each other correctly.
*/
prefix?: LocalizedSlugs;
/**
* Query string appended to canonical and every hreflang, e.g. '?page=2'.
*
* A paginated listing must be canonical to itself — pointing page 2 at page 1
* tells Google the entries on it don't exist. Alternates carry the same page,
* since /pl/artykuly?page=2 corresponds to /en/articles?page=2.
*/
query?: string;
/** Separator between page title and site name. Defaults to ' | '. */
separator?: string;
/** Site name for title composition and OG. */
siteName?: null | string;
/** slug per locale for this document — drives canonical + hreflang. */
slugs: LocalizedSlugs;
};
/**
* Assembles a Next.js-compatible Metadata object from document SEO fields and
* site-level data. Locale-aware: canonical points at the current locale's
* path, and hreflang alternates cover every locale the document exists in.
*
* Designed for use inside Next.js `generateMetadata`. The caller resolves the
* pieces (meta group, site name, image URL, localized slugs) and passes them
* in — the plugin composes, it doesn't fetch.
*/
export declare function buildMetadata({ baseUrl, config, homeSlug, imageUrl, locale, meta, order, prefix, query, separator, siteName, slugs, }: BuildMetadataArgs): PageMetadata;
export {};
-76
View File
@@ -1,76 +0,0 @@
import { buildLocalizedPath } from '../i18n/index.js';
import { composeTitle } from './composeTitle.js';
import { buildHreflangAlternates } from './hreflang.js';
/**
* Assembles a Next.js-compatible Metadata object from document SEO fields and
* site-level data. Locale-aware: canonical points at the current locale's
* path, and hreflang alternates cover every locale the document exists in.
*
* Designed for use inside Next.js `generateMetadata`. The caller resolves the
* pieces (meta group, site name, image URL, localized slugs) and passes them
* in — the plugin composes, it doesn't fetch.
*/ export function buildMetadata({ baseUrl, config, homeSlug = 'home', imageUrl, locale, meta, order, prefix, query, separator, siteName, slugs }) {
// titleOverride wins outright: an editor who filled it in wants that exact
// string in the tab, not a composition.
const override = meta?.titleOverride?.trim();
const title = override || composeTitle({
order,
pageTitle: meta?.title,
separator,
siteName
});
const description = meta?.description?.trim() || undefined;
const origin = baseUrl?.replace(/\/$/, '') ?? '';
const suffix = query ?? '';
const currentPath = buildLocalizedPath({
config,
homeSlug,
locale,
prefix,
slugs
});
const canonical = currentPath ? `${origin}${currentPath}${suffix}` : undefined;
const languages = buildHreflangAlternates({
baseUrl,
config,
homeSlug,
prefix,
slugs
});
if (suffix) {
for (const code of Object.keys(languages)){
languages[code] = `${languages[code]}${suffix}`;
}
}
const images = imageUrl ? [
{
url: imageUrl
}
] : undefined;
return {
title,
...description && {
description
},
alternates: {
...canonical && {
canonical
},
...Object.keys(languages).length > 0 && {
languages
}
},
openGraph: {
title,
...description && {
description
},
...images && {
images
},
locale
}
};
}
//# sourceMappingURL=buildMetadata.js.map
File diff suppressed because one or more lines are too long
-36
View File
@@ -1,36 +0,0 @@
type BuildRobotsArgs = {
/** Absolute origin, e.g. 'https://example.com'. */
baseUrl: string;
/** Paths to disallow. Defaults to Payload's admin and API. */
disallow?: string[];
/** Whether to advertise the sitemap. Defaults to true. */
sitemap?: boolean;
};
/**
* Shape for Next's `app/robots.ts`.
*/
export type RobotsRules = {
host?: string;
rules: {
allow?: string;
disallow?: string[];
userAgent: string;
};
sitemap?: string;
};
/**
* Builds robots rules that allow crawling everything except the admin panel and
* API, and point crawlers at the sitemap.
*
* ```ts
* // app/robots.ts
* export default function robots() {
* return buildRobots({ baseUrl: process.env.NEXT_PUBLIC_SERVER_URL! })
* }
* ```
*
* The admin and API are disallowed by default — no reason for them in the index,
* and it keeps crawl budget on real content.
*/
export declare function buildRobots({ baseUrl, disallow, sitemap, }: BuildRobotsArgs): RobotsRules;
export {};
-32
View File
@@ -1,32 +0,0 @@
/**
* Builds robots rules that allow crawling everything except the admin panel and
* API, and point crawlers at the sitemap.
*
* ```ts
* // app/robots.ts
* export default function robots() {
* return buildRobots({ baseUrl: process.env.NEXT_PUBLIC_SERVER_URL! })
* }
* ```
*
* The admin and API are disallowed by default — no reason for them in the index,
* and it keeps crawl budget on real content.
*/ export function buildRobots({ baseUrl, disallow = [
'/admin',
'/api'
], sitemap = true }) {
const origin = baseUrl.replace(/\/$/, '');
return {
rules: {
allow: '/',
disallow,
userAgent: '*'
},
...sitemap ? {
sitemap: `${origin}/sitemap.xml`
} : {},
host: origin
};
}
//# sourceMappingURL=buildRobots.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":["../../../src/modules/seo/buildRobots.ts"],"sourcesContent":["type BuildRobotsArgs = {\n /** Absolute origin, e.g. 'https://example.com'. */\n baseUrl: string\n /** Paths to disallow. Defaults to Payload's admin and API. */\n disallow?: string[]\n /** Whether to advertise the sitemap. Defaults to true. */\n sitemap?: boolean\n}\n\n/**\n * Shape for Next's `app/robots.ts`.\n */\nexport type RobotsRules = {\n host?: string\n rules: { allow?: string; disallow?: string[]; userAgent: string }\n sitemap?: string\n}\n\n/**\n * Builds robots rules that allow crawling everything except the admin panel and\n * API, and point crawlers at the sitemap.\n *\n * ```ts\n * // app/robots.ts\n * export default function robots() {\n * return buildRobots({ baseUrl: process.env.NEXT_PUBLIC_SERVER_URL! })\n * }\n * ```\n *\n * The admin and API are disallowed by default — no reason for them in the index,\n * and it keeps crawl budget on real content.\n */\nexport function buildRobots({\n baseUrl,\n disallow = ['/admin', '/api'],\n sitemap = true,\n}: BuildRobotsArgs): RobotsRules {\n const origin = baseUrl.replace(/\\/$/, '')\n\n return {\n rules: {\n allow: '/',\n disallow,\n userAgent: '*',\n },\n ...(sitemap ? { sitemap: `${origin}/sitemap.xml` } : {}),\n host: origin,\n }\n}\n"],"names":["buildRobots","baseUrl","disallow","sitemap","origin","replace","rules","allow","userAgent","host"],"mappings":"AAkBA;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASA,YAAY,EAC1BC,OAAO,EACPC,WAAW;IAAC;IAAU;CAAO,EAC7BC,UAAU,IAAI,EACE;IAChB,MAAMC,SAASH,QAAQI,OAAO,CAAC,OAAO;IAEtC,OAAO;QACLC,OAAO;YACLC,OAAO;YACPL;YACAM,WAAW;QACb;QACA,GAAIL,UAAU;YAAEA,SAAS,GAAGC,OAAO,YAAY,CAAC;QAAC,IAAI,CAAC,CAAC;QACvDK,MAAML;IACR;AACF"}
-61
View File
@@ -1,61 +0,0 @@
import type { BasePayload } from 'payload';
import type { ContentOption } from '../content/index.js';
import type { I18nConfig } from '../i18n/index.js';
/**
* One sitemap entry, shaped for Next's `app/sitemap.ts`.
*
* `alternates.languages` is the important part for a multilingual site: Next
* renders it as `<xhtml:link rel="alternate" hreflang=…>` per URL, which is
* exactly what Google uses to connect language versions. A sitemap without it
* is the common, weaker kind.
*/
export type SitemapEntry = {
alternates?: {
languages: Record<string, string>;
};
changeFrequency?: 'always' | 'daily' | 'hourly' | 'monthly' | 'never' | 'weekly' | 'yearly';
lastModified?: Date | string;
priority?: number;
url: string;
};
type BuildSitemapArgs = {
/** Absolute origin, e.g. 'https://example.com'. Required for valid sitemap URLs. */
baseUrl: string;
changeFrequency?: SitemapEntry['changeFrequency'];
config: I18nConfig;
/** Archive-backed collections, same value as the plugin option. */
content?: ContentOption;
/**
* Slug of the page that is the site root (collapses to /{locale}).
* Read from System Pages when omitted.
*/
homeSlug?: string;
/** Pages collection slug. Defaults to 'pages'. */
pagesSlug?: string;
payload: BasePayload;
/** SiteSettings global slug. Defaults to 'site-settings'. */
settingsSlug?: string;
};
/**
* Collects every public URL — pages and archive entries — as sitemap entries
* with per-URL hreflang and lastmod.
*
* Reuses the same path/hreflang builders as page metadata, so the sitemap can't
* drift from what the pages actually render (a classic source of sitemap bugs:
* a URL listed one way and served another). Drafts and noindex documents are
* omitted.
*
* ```ts
* // app/sitemap.ts
* export default async function sitemap() {
* return buildSitemapEntries({
* payload: await getPayload({ config }),
* config: i18nConfig,
* baseUrl: process.env.NEXT_PUBLIC_SERVER_URL!,
* content: contentConfig,
* })
* }
* ```
*/
export declare function buildSitemapEntries({ baseUrl, changeFrequency, config, content, homeSlug, pagesSlug, payload, settingsSlug, }: BuildSitemapArgs): Promise<SitemapEntry[]>;
export {};
-158
View File
@@ -1,158 +0,0 @@
import { archiveFieldName } from '../content/index.js';
import { buildLocalizedPath, getLocalizedSlugs } from '../i18n/index.js';
import { buildHreflangAlternates } from './hreflang.js';
/** Skip drafts and anything flagged noindex in the SEO tab. */ function isIndexable(doc) {
if (doc._status && doc._status !== 'published') {
return false;
}
if (doc.meta?.noindex) {
return false;
}
return true;
}
/**
* Turns one document into a sitemap entry per default locale isn't needed —
* one entry with all languages as alternates is the correct, de-duplicated
* shape. The `url` is the current locale's path; `alternates.languages` carries
* every locale (including itself, per Google's guidance).
*/ function entryFor(doc, locale, config, baseUrl, homeSlug, prefix, changeFrequency) {
const slugs = doc.slug && typeof doc.slug === 'object' ? getLocalizedSlugs({
config,
slugField: doc.slug
}) : {};
const path = buildLocalizedPath({
config,
homeSlug,
locale,
prefix,
slugs
});
if (!path) {
return null;
}
const origin = baseUrl.replace(/\/$/, '');
const languages = buildHreflangAlternates({
baseUrl,
config,
homeSlug,
prefix,
slugs
});
return {
url: `${origin}${path}`,
...doc.updatedAt ? {
lastModified: doc.updatedAt
} : {},
...changeFrequency ? {
changeFrequency
} : {},
...Object.keys(languages).length ? {
alternates: {
languages
}
} : {}
};
}
/**
* Collects every public URL — pages and archive entries — as sitemap entries
* with per-URL hreflang and lastmod.
*
* Reuses the same path/hreflang builders as page metadata, so the sitemap can't
* drift from what the pages actually render (a classic source of sitemap bugs:
* a URL listed one way and served another). Drafts and noindex documents are
* omitted.
*
* ```ts
* // app/sitemap.ts
* export default async function sitemap() {
* return buildSitemapEntries({
* payload: await getPayload({ config }),
* config: i18nConfig,
* baseUrl: process.env.NEXT_PUBLIC_SERVER_URL!,
* content: contentConfig,
* })
* }
* ```
*/ export async function buildSitemapEntries({ baseUrl, changeFrequency = 'weekly', config, content, homeSlug, pagesSlug = 'pages', payload, settingsSlug = 'site-settings' }) {
const locales = config.locales.map((l)=>l.code);
const defaultLocale = config.defaultLocale;
// Resolve homeSlug and archive prefixes from System Pages (read once, in all
// locales so archive prefixes are available per language).
const settings = await payload.findGlobal({
slug: settingsSlug,
depth: 1,
locale: 'all'
});
const resolvedHomeSlug = homeSlug ?? extractSlugInLocale(settings.homepage, defaultLocale) ?? 'home';
// Which collections to walk: pages (no prefix) + each content collection with
// its archive-page slugs as the localized prefix.
const collections = [
{
slug: pagesSlug
}
];
for (const c of content?.collections ?? []){
const archive = settings[archiveFieldName(c.slug)];
const prefixSlugs = archive ? slugMapAllLocales(archive, locales) : undefined;
collections.push({
slug: c.slug,
prefixSlugs
});
}
const entries = [];
for (const collection of collections){
// Read every doc once, across locales, so one row yields all its language
// alternates without re-querying per locale.
const result = await payload.find({
collection: collection.slug,
depth: 0,
limit: 0,
locale: 'all',
pagination: false
});
for (const raw of result.docs){
if (!isIndexable(raw)) {
continue;
}
// Emit the entry under the default locale's URL; alternates cover the rest.
const entry = entryFor(raw, defaultLocale, config, baseUrl, resolvedHomeSlug, collection.prefixSlugs, changeFrequency);
if (entry) {
entries.push(entry);
}
}
}
return entries;
}
/** Pulls a slug string from a populated relationship in a specific locale. */ function extractSlugInLocale(rel, locale) {
if (!rel || typeof rel !== 'object') {
return undefined;
}
const slug = rel.slug;
if (typeof slug === 'string') {
return slug;
}
if (slug && typeof slug === 'object') {
const v = slug[locale];
return typeof v === 'string' ? v : undefined;
}
return undefined;
}
/** Builds a locale→slug map from a populated archive relationship. */ function slugMapAllLocales(rel, locales) {
if (!rel || typeof rel !== 'object') {
return undefined;
}
const slug = rel.slug;
if (!slug || typeof slug !== 'object') {
return undefined;
}
const map = {};
for (const locale of locales){
const v = slug[locale];
if (typeof v === 'string') {
map[locale] = v;
}
}
return Object.keys(map).length ? map : undefined;
}
//# sourceMappingURL=buildSitemapEntries.js.map
File diff suppressed because one or more lines are too long
-25
View File
@@ -1,25 +0,0 @@
/** Which part comes first in a composed title. */
export type TitleOrder = 'page-first' | 'site-first';
type ComposeTitleArgs = {
/** Defaults to 'page-first' — the page title is what a visitor scans for. */
order?: TitleOrder;
/** Page-specific title, e.g. 'About Us'. */
pageTitle?: null | string;
/** Separator between page title and site name. Defaults to ' | '. */
separator?: string;
/** Site name, e.g. 'Acme Inc'. */
siteName?: null | string;
};
/**
* Composes a full document title from a page title and the site name.
*
* - both, page-first: "About Us | Acme Inc"
* - both, site-first: "Acme Inc | About Us"
* - page only: "About Us"
* - site only: "Acme Inc"
* - neither: ""
*
* Pure function — no dependency on Payload or request state.
*/
export declare function composeTitle({ order, pageTitle, separator, siteName, }: ComposeTitleArgs): string;
export {};
-20
View File
@@ -1,20 +0,0 @@
/** Which part comes first in a composed title. */ /**
* Composes a full document title from a page title and the site name.
*
* - both, page-first: "About Us | Acme Inc"
* - both, site-first: "Acme Inc | About Us"
* - page only: "About Us"
* - site only: "Acme Inc"
* - neither: ""
*
* Pure function — no dependency on Payload or request state.
*/ export function composeTitle({ order = 'page-first', pageTitle, separator = ' | ', siteName }) {
const page = pageTitle?.trim();
const site = siteName?.trim();
if (page && site) {
return order === 'site-first' ? `${site}${separator}${page}` : `${page}${separator}${site}`;
}
return page || site || '';
}
//# sourceMappingURL=composeTitle.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":["../../../src/modules/seo/composeTitle.ts"],"sourcesContent":["/** Which part comes first in a composed title. */\nexport type TitleOrder = 'page-first' | 'site-first'\n\ntype ComposeTitleArgs = {\n /** Defaults to 'page-first' — the page title is what a visitor scans for. */\n order?: TitleOrder\n /** Page-specific title, e.g. 'About Us'. */\n pageTitle?: null | string\n /** Separator between page title and site name. Defaults to ' | '. */\n separator?: string\n /** Site name, e.g. 'Acme Inc'. */\n siteName?: null | string\n}\n\n/**\n * Composes a full document title from a page title and the site name.\n *\n * - both, page-first: \"About Us | Acme Inc\"\n * - both, site-first: \"Acme Inc | About Us\"\n * - page only: \"About Us\"\n * - site only: \"Acme Inc\"\n * - neither: \"\"\n *\n * Pure function — no dependency on Payload or request state.\n */\nexport function composeTitle({\n order = 'page-first',\n pageTitle,\n separator = ' | ',\n siteName,\n}: ComposeTitleArgs): string {\n const page = pageTitle?.trim()\n const site = siteName?.trim()\n\n if (page && site) {\n return order === 'site-first' ? `${site}${separator}${page}` : `${page}${separator}${site}`\n }\n return page || site || ''\n}\n"],"names":["composeTitle","order","pageTitle","separator","siteName","page","trim","site"],"mappings":"AAAA,gDAAgD,GAchD;;;;;;;;;;CAUC,GACD,OAAO,SAASA,aAAa,EAC3BC,QAAQ,YAAY,EACpBC,SAAS,EACTC,YAAY,KAAK,EACjBC,QAAQ,EACS;IACjB,MAAMC,OAAOH,WAAWI;IACxB,MAAMC,OAAOH,UAAUE;IAEvB,IAAID,QAAQE,MAAM;QAChB,OAAON,UAAU,eAAe,GAAGM,OAAOJ,YAAYE,MAAM,GAAG,GAAGA,OAAOF,YAAYI,MAAM;IAC7F;IACA,OAAOF,QAAQE,QAAQ;AACzB"}
-74
View File
@@ -1,74 +0,0 @@
import type { BasePayload } from 'payload';
import type { I18nConfig } from '../i18n/index.js';
import type { PageMetadata } from './buildMetadata.js';
import type { SeoMeta } from './types.js';
/**
* A document as needed for metadata: its SEO meta group and localized slug.
*/
type MetadataDocument = {
meta?: null | SeoMeta;
slug?: null | Record<string, unknown>;
};
type CreateMetadataGeneratorArgs = {
/** Absolute site origin, e.g. 'https://example.com'. */
baseUrl?: string;
config: I18nConfig;
/** Home slug that collapses to the locale root. Defaults to 'home'. */
homeSlug?: string;
/**
* Resolves the document to build metadata for, given route params.
* The client supplies this (they own the collections and routing); it should
* fetch with `locale: 'all'` so the slug field is a locale→value map.
*/
resolveDocument: (args: {
locale: string;
params: Record<string, string | string[]>;
payload: BasePayload;
}) => Promise<MetadataDocument | null>;
/** Resolves the OG image URL for the document, if any. */
resolveImageUrl?: (args: {
doc: MetadataDocument;
locale: string;
payload: BasePayload;
}) => Promise<null | string>;
/** Resolves the site name (e.g. from SiteSettings). */
resolveSiteName?: (args: {
locale: string;
payload: BasePayload;
}) => Promise<null | string>;
};
/**
* Builds a metadata generator, collapsing the usual generateMetadata
* boilerplate into a single wired-up function.
*
* The client provides resolvers (they own collections/routing); the plugin
* owns the assembly (title composition, canonical, hreflang, OG).
*
* The returned function takes `{ payload, params, locale }` — Next calls
* `generateMetadata({ params })` without those, and the plugin never calls
* getPayload itself, so the client wraps it in their route file:
*
* ```ts
* // app/(frontend)/[locale]/[[...slug]]/page.tsx
* const generate = createMetadataGenerator({
* config: i18nConfig,
* baseUrl: process.env.NEXT_PUBLIC_SERVER_URL,
* resolveDocument: async ({ payload, params, locale }) => { ... },
* })
*
* export async function generateMetadata({ params }) {
* const { locale, slug } = await params
* const payload = await getPayload({ config: await config })
* return generate({ payload, params: { slug: slug ?? [] }, locale })
* }
* ```
*
* `resolveDocument` should fetch with `locale: 'all'` so the slug field comes
* back as a locale→value map — that's what hreflang alternates are built from.
*/
export declare function createMetadataGenerator(args: CreateMetadataGeneratorArgs): (context: {
locale: string;
params: Record<string, string | string[]>;
payload: BasePayload;
}) => Promise<PageMetadata>;
export {};
-68
View File
@@ -1,68 +0,0 @@
import { getLocalizedSlugs } from '../i18n/index.js';
import { buildMetadata } from './buildMetadata.js';
/**
* Builds a metadata generator, collapsing the usual generateMetadata
* boilerplate into a single wired-up function.
*
* The client provides resolvers (they own collections/routing); the plugin
* owns the assembly (title composition, canonical, hreflang, OG).
*
* The returned function takes `{ payload, params, locale }` — Next calls
* `generateMetadata({ params })` without those, and the plugin never calls
* getPayload itself, so the client wraps it in their route file:
*
* ```ts
* // app/(frontend)/[locale]/[[...slug]]/page.tsx
* const generate = createMetadataGenerator({
* config: i18nConfig,
* baseUrl: process.env.NEXT_PUBLIC_SERVER_URL,
* resolveDocument: async ({ payload, params, locale }) => { ... },
* })
*
* export async function generateMetadata({ params }) {
* const { locale, slug } = await params
* const payload = await getPayload({ config: await config })
* return generate({ payload, params: { slug: slug ?? [] }, locale })
* }
* ```
*
* `resolveDocument` should fetch with `locale: 'all'` so the slug field comes
* back as a locale→value map — that's what hreflang alternates are built from.
*/ export function createMetadataGenerator(args) {
const { baseUrl, config, homeSlug, resolveDocument, resolveImageUrl, resolveSiteName } = args;
return async function generateMetadata(context) {
const { locale, params, payload } = context;
const doc = await resolveDocument({
locale,
params,
payload
});
const slugs = doc?.slug ? getLocalizedSlugs({
config,
slugField: doc.slug
}) : {};
const [siteName, imageUrl] = await Promise.all([
resolveSiteName?.({
locale,
payload
}) ?? Promise.resolve(null),
doc && resolveImageUrl ? resolveImageUrl({
doc,
locale,
payload
}) : Promise.resolve(null)
]);
return buildMetadata({
baseUrl,
config,
homeSlug,
imageUrl,
locale,
meta: doc?.meta,
siteName,
slugs
});
};
}
//# sourceMappingURL=createMetadataGenerator.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":["../../../src/modules/seo/createMetadataGenerator.ts"],"sourcesContent":["import type { BasePayload } from 'payload'\n\nimport type { I18nConfig, LocalizedSlugs } from '../i18n/index.js'\nimport type { PageMetadata } from './buildMetadata.js'\nimport type { SeoMeta } from './types.js'\n\nimport { getLocalizedSlugs } from '../i18n/index.js'\nimport { buildMetadata } from './buildMetadata.js'\n\n/**\n * A document as needed for metadata: its SEO meta group and localized slug.\n */\ntype MetadataDocument = {\n meta?: null | SeoMeta\n slug?: null | Record<string, unknown>\n}\n\ntype CreateMetadataGeneratorArgs = {\n /** Absolute site origin, e.g. 'https://example.com'. */\n baseUrl?: string\n config: I18nConfig\n /** Home slug that collapses to the locale root. Defaults to 'home'. */\n homeSlug?: string\n /**\n * Resolves the document to build metadata for, given route params.\n * The client supplies this (they own the collections and routing); it should\n * fetch with `locale: 'all'` so the slug field is a locale→value map.\n */\n resolveDocument: (args: {\n locale: string\n params: Record<string, string | string[]>\n payload: BasePayload\n }) => Promise<MetadataDocument | null>\n /** Resolves the OG image URL for the document, if any. */\n resolveImageUrl?: (args: {\n doc: MetadataDocument\n locale: string\n payload: BasePayload\n }) => Promise<null | string>\n /** Resolves the site name (e.g. from SiteSettings). */\n resolveSiteName?: (args: { locale: string; payload: BasePayload }) => Promise<null | string>\n}\n\n/**\n * Builds a metadata generator, collapsing the usual generateMetadata\n * boilerplate into a single wired-up function.\n *\n * The client provides resolvers (they own collections/routing); the plugin\n * owns the assembly (title composition, canonical, hreflang, OG).\n *\n * The returned function takes `{ payload, params, locale }` — Next calls\n * `generateMetadata({ params })` without those, and the plugin never calls\n * getPayload itself, so the client wraps it in their route file:\n *\n * ```ts\n * // app/(frontend)/[locale]/[[...slug]]/page.tsx\n * const generate = createMetadataGenerator({\n * config: i18nConfig,\n * baseUrl: process.env.NEXT_PUBLIC_SERVER_URL,\n * resolveDocument: async ({ payload, params, locale }) => { ... },\n * })\n *\n * export async function generateMetadata({ params }) {\n * const { locale, slug } = await params\n * const payload = await getPayload({ config: await config })\n * return generate({ payload, params: { slug: slug ?? [] }, locale })\n * }\n * ```\n *\n * `resolveDocument` should fetch with `locale: 'all'` so the slug field comes\n * back as a locale→value map — that's what hreflang alternates are built from.\n */\nexport function createMetadataGenerator(args: CreateMetadataGeneratorArgs) {\n const { baseUrl, config, homeSlug, resolveDocument, resolveImageUrl, resolveSiteName } = args\n\n return async function generateMetadata(context: {\n locale: string\n params: Record<string, string | string[]>\n payload: BasePayload\n }): Promise<PageMetadata> {\n const { locale, params, payload } = context\n\n const doc = await resolveDocument({ locale, params, payload })\n\n const slugs: LocalizedSlugs = doc?.slug\n ? getLocalizedSlugs({ config, slugField: doc.slug })\n : {}\n\n const [siteName, imageUrl] = await Promise.all([\n resolveSiteName?.({ locale, payload }) ?? Promise.resolve(null),\n doc && resolveImageUrl ? resolveImageUrl({ doc, locale, payload }) : Promise.resolve(null),\n ])\n\n return buildMetadata({\n baseUrl,\n config,\n homeSlug,\n imageUrl,\n locale,\n meta: doc?.meta,\n siteName,\n slugs,\n })\n }\n}\n"],"names":["getLocalizedSlugs","buildMetadata","createMetadataGenerator","args","baseUrl","config","homeSlug","resolveDocument","resolveImageUrl","resolveSiteName","generateMetadata","context","locale","params","payload","doc","slugs","slug","slugField","siteName","imageUrl","Promise","all","resolve","meta"],"mappings":"AAMA,SAASA,iBAAiB,QAAQ,mBAAkB;AACpD,SAASC,aAAa,QAAQ,qBAAoB;AAoClD;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GACD,OAAO,SAASC,wBAAwBC,IAAiC;IACvE,MAAM,EAAEC,OAAO,EAAEC,MAAM,EAAEC,QAAQ,EAAEC,eAAe,EAAEC,eAAe,EAAEC,eAAe,EAAE,GAAGN;IAEzF,OAAO,eAAeO,iBAAiBC,OAItC;QACC,MAAM,EAAEC,MAAM,EAAEC,MAAM,EAAEC,OAAO,EAAE,GAAGH;QAEpC,MAAMI,MAAM,MAAMR,gBAAgB;YAAEK;YAAQC;YAAQC;QAAQ;QAE5D,MAAME,QAAwBD,KAAKE,OAC/BjB,kBAAkB;YAAEK;YAAQa,WAAWH,IAAIE,IAAI;QAAC,KAChD,CAAC;QAEL,MAAM,CAACE,UAAUC,SAAS,GAAG,MAAMC,QAAQC,GAAG,CAAC;YAC7Cb,kBAAkB;gBAAEG;gBAAQE;YAAQ,MAAMO,QAAQE,OAAO,CAAC;YAC1DR,OAAOP,kBAAkBA,gBAAgB;gBAAEO;gBAAKH;gBAAQE;YAAQ,KAAKO,QAAQE,OAAO,CAAC;SACtF;QAED,OAAOtB,cAAc;YACnBG;YACAC;YACAC;YACAc;YACAR;YACAY,MAAMT,KAAKS;YACXL;YACAH;QACF;IACF;AACF"}
-59
View File
@@ -1,59 +0,0 @@
import type { BasePayload } from 'payload';
import type { ContentOption } from '../content/index.js';
import type { I18nConfig } from '../i18n/index.js';
import type { PageMetadata } from './buildMetadata.js';
type CreatePageMetadataArgs = {
/** Absolute site origin, e.g. 'https://example.com'. */
baseUrl?: string;
/** Collection holding pages. Defaults to 'pages'. */
collection?: string;
config: I18nConfig;
/**
* Archive-backed collections, same value as the plugin option. Pass it and
* entry URLs (/pl/artykuly/moj-post) get correct canonical and hreflang;
* omit it and only pages are handled.
*/
content?: ContentOption;
/** SiteSettings global slug. Defaults to 'site-settings'. */
settingsSlug?: string;
/** Field on SiteSettings holding the site name. Defaults to 'siteName'. */
siteNameField?: string;
};
type PageMetadataContext = {
locale: string;
/**
* Page number from ?page= on an archive listing. Pass it and page 2 gets a
* canonical to itself; leave it out and every page claims to be page 1.
*/
page?: number;
payload: BasePayload;
/** Route slug segments; empty/undefined means the locale root. */
slug?: string[];
};
/**
* Metadata for the page route, with every resolver already wired.
*
* `createMetadataGenerator` asks the client for resolvers because it can't know
* their collections. But for the plugin's own conventions it does know: pages
* live in one collection, the site name sits on SiteSettings, the OG image sits
* on the plugin-seo `meta` group, the home page is whatever System Pages points
* at, and — with `content` — entries live under their collection's archive page.
* Re-declaring all that in every project is copy-paste, so this resolves it.
*
* Reach for `createMetadataGenerator` instead when a route doesn't follow those
* conventions — custom image logic, a different global, hand-rolled paths.
*
* The plugin never calls getPayload, and Next calls generateMetadata without a
* payload, so the client keeps a small wrapper:
*
* ```ts
* const pageMetadata = createPageMetadata({ config: i18nConfig, baseUrl, content })
*
* export async function generateMetadata({ params }) {
* const { locale, slug } = await params
* return pageMetadata({ payload: await getPayload({ config }), locale, slug })
* }
* ```
*/
export declare function createPageMetadata(args: CreatePageMetadataArgs): ({ slug, locale, page, payload, }: PageMetadataContext) => Promise<PageMetadata>;
export {};
-100
View File
@@ -1,100 +0,0 @@
import { resolveRoute } from '../content/index.js';
import { buildMetadata } from './buildMetadata.js';
import { readSiteMetaConfig } from './readSiteMetaConfig.js';
import { slugsAcrossLocales } from './slugsAcrossLocales.js';
/** plugin-seo stores the OG image as an upload relationship. */ function resolveOgImage(doc) {
const image = doc.meta?.image;
if (image && typeof image === 'object' && 'url' in image) {
return image.url ?? null;
}
return null;
}
/**
* Metadata for the page route, with every resolver already wired.
*
* `createMetadataGenerator` asks the client for resolvers because it can't know
* their collections. But for the plugin's own conventions it does know: pages
* live in one collection, the site name sits on SiteSettings, the OG image sits
* on the plugin-seo `meta` group, the home page is whatever System Pages points
* at, and — with `content` — entries live under their collection's archive page.
* Re-declaring all that in every project is copy-paste, so this resolves it.
*
* Reach for `createMetadataGenerator` instead when a route doesn't follow those
* conventions — custom image logic, a different global, hand-rolled paths.
*
* The plugin never calls getPayload, and Next calls generateMetadata without a
* payload, so the client keeps a small wrapper:
*
* ```ts
* const pageMetadata = createPageMetadata({ config: i18nConfig, baseUrl, content })
*
* export async function generateMetadata({ params }) {
* const { locale, slug } = await params
* return pageMetadata({ payload: await getPayload({ config }), locale, slug })
* }
* ```
*/ export function createPageMetadata(args) {
const { baseUrl, collection = 'pages', config, content, settingsSlug = 'site-settings', siteNameField = 'siteName' } = args;
return async function pageMetadata({ slug, locale, page, payload }) {
const site = await readSiteMetaConfig({
locale,
payload,
settingsSlug,
siteNameField
});
const base = {
baseUrl,
config,
homeSlug: site.homeSlug,
locale,
order: site.order,
separator: site.separator,
siteName: site.siteName
};
const route = await resolveRoute({
content,
locale,
page,
pagesSlug: collection,
payload,
segments: slug,
settingsSlug
});
// Unknown route (the page component will 404) — still return something
// coherent rather than throwing during metadata generation.
if (!route) {
return buildMetadata({
...base,
meta: null,
slugs: {}
});
}
const doc = route.doc;
// An entry sits under its archive, so its URLs need that segment — and the
// segment differs per locale, since it's the archive page's own slug.
const prefix = route.type === 'entry' ? await slugsAcrossLocales({
id: route.archive.id,
collection,
config,
payload
}) : undefined;
const slugs = await slugsAcrossLocales({
id: doc.id,
collection: route.type === 'entry' ? route.collection : collection,
config,
payload
});
// Page 2 of a listing is its own URL, not a variant of page 1.
const query = route.type === 'archive' && route.page > 1 ? `?page=${route.page}` : undefined;
return buildMetadata({
...base,
imageUrl: resolveOgImage(doc),
meta: doc.meta,
prefix,
query,
slugs
});
};
}
//# sourceMappingURL=createPageMetadata.js.map
File diff suppressed because one or more lines are too long
-34
View File
@@ -1,34 +0,0 @@
import type { I18nConfig, LocalizedSlugs } from '../i18n/index.js';
type BuildHreflangArgs = {
/** Absolute site origin, e.g. 'https://example.com'. Omit for relative paths. */
baseUrl?: string;
config: I18nConfig;
/** Home slug that collapses to the locale root. Defaults to 'home'. */
homeSlug?: string;
/**
* Localized segment the document lives under (an archive page's slugs),
* e.g. { pl: 'artykuly', en: 'articles' }. Locales missing from the prefix
* are omitted — an entry with no archive in that language has no URL there.
*/
prefix?: LocalizedSlugs;
/** slug per locale for the current document, e.g. { pl: 'o-nas', en: 'about' } */
slugs: LocalizedSlugs;
};
/**
* Builds a map of locale → URL for hreflang alternate links, suitable for
* Next.js Metadata `alternates.languages`.
*
* Bridges SEO and i18n: for each configured locale that the document has a
* slug in, it produces the locale-aware path (via buildLocalizedPath),
* optionally prefixed with an absolute origin.
*
* @example
* buildHreflangAlternates({
* slugs: { pl: 'o-nas', en: 'about' },
* config,
* baseUrl: 'https://example.com',
* })
* // → { pl: 'https://example.com/pl/o-nas', en: 'https://example.com/en/about' }
*/
export declare function buildHreflangAlternates({ baseUrl, config, homeSlug, prefix, slugs, }: BuildHreflangArgs): Record<string, string>;
export {};
-35
View File
@@ -1,35 +0,0 @@
import { buildLocalizedPath, getLocaleCodes } from '../i18n/index.js';
/**
* Builds a map of locale → URL for hreflang alternate links, suitable for
* Next.js Metadata `alternates.languages`.
*
* Bridges SEO and i18n: for each configured locale that the document has a
* slug in, it produces the locale-aware path (via buildLocalizedPath),
* optionally prefixed with an absolute origin.
*
* @example
* buildHreflangAlternates({
* slugs: { pl: 'o-nas', en: 'about' },
* config,
* baseUrl: 'https://example.com',
* })
* // → { pl: 'https://example.com/pl/o-nas', en: 'https://example.com/en/about' }
*/ export function buildHreflangAlternates({ baseUrl, config, homeSlug = 'home', prefix, slugs }) {
const origin = baseUrl?.replace(/\/$/, '') ?? '';
const alternates = {};
for (const locale of getLocaleCodes(config)){
const path = buildLocalizedPath({
config,
homeSlug,
locale,
prefix,
slugs
});
if (path) {
alternates[locale] = `${origin}${path}`;
}
}
return alternates;
}
//# sourceMappingURL=hreflang.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":["../../../src/modules/seo/hreflang.ts"],"sourcesContent":["import type { I18nConfig, LocalizedSlugs } from '../i18n/index.js'\n\nimport { buildLocalizedPath, getLocaleCodes } from '../i18n/index.js'\n\ntype BuildHreflangArgs = {\n /** Absolute site origin, e.g. 'https://example.com'. Omit for relative paths. */\n baseUrl?: string\n config: I18nConfig\n /** Home slug that collapses to the locale root. Defaults to 'home'. */\n homeSlug?: string\n /**\n * Localized segment the document lives under (an archive page's slugs),\n * e.g. { pl: 'artykuly', en: 'articles' }. Locales missing from the prefix\n * are omitted — an entry with no archive in that language has no URL there.\n */\n prefix?: LocalizedSlugs\n /** slug per locale for the current document, e.g. { pl: 'o-nas', en: 'about' } */\n slugs: LocalizedSlugs\n}\n\n/**\n * Builds a map of locale → URL for hreflang alternate links, suitable for\n * Next.js Metadata `alternates.languages`.\n *\n * Bridges SEO and i18n: for each configured locale that the document has a\n * slug in, it produces the locale-aware path (via buildLocalizedPath),\n * optionally prefixed with an absolute origin.\n *\n * @example\n * buildHreflangAlternates({\n * slugs: { pl: 'o-nas', en: 'about' },\n * config,\n * baseUrl: 'https://example.com',\n * })\n * // → { pl: 'https://example.com/pl/o-nas', en: 'https://example.com/en/about' }\n */\nexport function buildHreflangAlternates({\n baseUrl,\n config,\n homeSlug = 'home',\n prefix,\n slugs,\n}: BuildHreflangArgs): Record<string, string> {\n const origin = baseUrl?.replace(/\\/$/, '') ?? ''\n const alternates: Record<string, string> = {}\n\n for (const locale of getLocaleCodes(config)) {\n const path = buildLocalizedPath({ config, homeSlug, locale, prefix, slugs })\n if (path) {\n alternates[locale] = `${origin}${path}`\n }\n }\n\n return alternates\n}\n"],"names":["buildLocalizedPath","getLocaleCodes","buildHreflangAlternates","baseUrl","config","homeSlug","prefix","slugs","origin","replace","alternates","locale","path"],"mappings":"AAEA,SAASA,kBAAkB,EAAEC,cAAc,QAAQ,mBAAkB;AAkBrE;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASC,wBAAwB,EACtCC,OAAO,EACPC,MAAM,EACNC,WAAW,MAAM,EACjBC,MAAM,EACNC,KAAK,EACa;IAClB,MAAMC,SAASL,SAASM,QAAQ,OAAO,OAAO;IAC9C,MAAMC,aAAqC,CAAC;IAE5C,KAAK,MAAMC,UAAUV,eAAeG,QAAS;QAC3C,MAAMQ,OAAOZ,mBAAmB;YAAEI;YAAQC;YAAUM;YAAQL;YAAQC;QAAM;QAC1E,IAAIK,MAAM;YACRF,UAAU,CAACC,OAAO,GAAG,GAAGH,SAASI,MAAM;QACzC;IACF;IAEA,OAAOF;AACT"}
-20
View File
@@ -1,20 +0,0 @@
export { buildAutoFillMetaHook } from './autoFillMeta.js';
export type { AutoFillMapping } from './autoFillMeta.js';
export { buildMetadata } from './buildMetadata.js';
export type { PageMetadata } from './buildMetadata.js';
export { buildRobots } from './buildRobots.js';
export type { RobotsRules } from './buildRobots.js';
export { buildSitemapEntries } from './buildSitemapEntries.js';
export type { SitemapEntry } from './buildSitemapEntries.js';
export { composeTitle } from './composeTitle.js';
export type { TitleOrder } from './composeTitle.js';
export { createMetadataGenerator } from './createMetadataGenerator.js';
export { createPageMetadata } from './createPageMetadata.js';
export { buildHreflangAlternates } from './hreflang.js';
export { injectAutoFillMeta } from './injectAutoFillMeta.js';
export { injectSeoTabs } from './injectSeoTabs.js';
export { readSiteMetaConfig } from './readSiteMetaConfig.js';
export type { SiteMetaConfig } from './readSiteMetaConfig.js';
export { buildSeoPlugin } from './seoPluginConfig.js';
export { slugsAcrossLocales } from './slugsAcrossLocales.js';
export type { SeoMeta, SeoOption } from './types.js';
-15
View File
@@ -1,15 +0,0 @@
export { buildAutoFillMetaHook } from './autoFillMeta.js';
export { buildMetadata } from './buildMetadata.js';
export { buildRobots } from './buildRobots.js';
export { buildSitemapEntries } from './buildSitemapEntries.js';
export { composeTitle } from './composeTitle.js';
export { createMetadataGenerator } from './createMetadataGenerator.js';
export { createPageMetadata } from './createPageMetadata.js';
export { buildHreflangAlternates } from './hreflang.js';
export { injectAutoFillMeta } from './injectAutoFillMeta.js';
export { injectSeoTabs } from './injectSeoTabs.js';
export { readSiteMetaConfig } from './readSiteMetaConfig.js';
export { buildSeoPlugin } from './seoPluginConfig.js';
export { slugsAcrossLocales } from './slugsAcrossLocales.js';
//# sourceMappingURL=index.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":["../../../src/modules/seo/index.ts"],"sourcesContent":["export { buildAutoFillMetaHook } from './autoFillMeta.js'\nexport type { AutoFillMapping } from './autoFillMeta.js'\nexport { buildMetadata } from './buildMetadata.js'\nexport type { PageMetadata } from './buildMetadata.js'\nexport { buildRobots } from './buildRobots.js'\nexport type { RobotsRules } from './buildRobots.js'\nexport { buildSitemapEntries } from './buildSitemapEntries.js'\nexport type { SitemapEntry } from './buildSitemapEntries.js'\nexport { composeTitle } from './composeTitle.js'\nexport type { TitleOrder } from './composeTitle.js'\nexport { createMetadataGenerator } from './createMetadataGenerator.js'\nexport { createPageMetadata } from './createPageMetadata.js'\nexport { buildHreflangAlternates } from './hreflang.js'\nexport { injectAutoFillMeta } from './injectAutoFillMeta.js'\nexport { injectSeoTabs } from './injectSeoTabs.js'\nexport { readSiteMetaConfig } from './readSiteMetaConfig.js'\nexport type { SiteMetaConfig } from './readSiteMetaConfig.js'\nexport { buildSeoPlugin } from './seoPluginConfig.js'\nexport { slugsAcrossLocales } from './slugsAcrossLocales.js'\nexport type { SeoMeta, SeoOption } from './types.js'\n"],"names":["buildAutoFillMetaHook","buildMetadata","buildRobots","buildSitemapEntries","composeTitle","createMetadataGenerator","createPageMetadata","buildHreflangAlternates","injectAutoFillMeta","injectSeoTabs","readSiteMetaConfig","buildSeoPlugin","slugsAcrossLocales"],"mappings":"AAAA,SAASA,qBAAqB,QAAQ,oBAAmB;AAEzD,SAASC,aAAa,QAAQ,qBAAoB;AAElD,SAASC,WAAW,QAAQ,mBAAkB;AAE9C,SAASC,mBAAmB,QAAQ,2BAA0B;AAE9D,SAASC,YAAY,QAAQ,oBAAmB;AAEhD,SAASC,uBAAuB,QAAQ,+BAA8B;AACtE,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,uBAAuB,QAAQ,gBAAe;AACvD,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,aAAa,QAAQ,qBAAoB;AAClD,SAASC,kBAAkB,QAAQ,0BAAyB;AAE5D,SAASC,cAAc,QAAQ,uBAAsB;AACrD,SAASC,kBAAkB,QAAQ,0BAAyB"}
-11
View File
@@ -1,11 +0,0 @@
import type { Config } from 'payload';
import type { SeoOption } from './types.js';
/**
* Injects the auto-fill meta hook into every SEO-enabled collection.
*
* Runs after @payloadcms/plugin-seo has added the meta group, appending a
* beforeChange hook that fills empty meta from document content. Existing
* hooks are preserved (plugin hook runs last, so editor input and other hooks
* win first).
*/
export declare function injectAutoFillMeta(config: Config, seo: SeoOption): Config;
-35
View File
@@ -1,35 +0,0 @@
import { buildAutoFillMetaHook } from './autoFillMeta.js';
/**
* Injects the auto-fill meta hook into every SEO-enabled collection.
*
* Runs after @payloadcms/plugin-seo has added the meta group, appending a
* beforeChange hook that fills empty meta from document content. Existing
* hooks are preserved (plugin hook runs last, so editor input and other hooks
* win first).
*/ export function injectAutoFillMeta(config, seo) {
// autoFill disabled explicitly — leave collections untouched
if (seo.autoFill === false) {
return config;
}
const hook = buildAutoFillMetaHook(seo.autoFill);
return {
...config,
collections: (config.collections ?? []).map((collection)=>{
if (!seo.collections.includes(collection.slug)) {
return collection;
}
return {
...collection,
hooks: {
...collection.hooks,
beforeChange: [
...collection.hooks?.beforeChange ?? [],
hook
]
}
};
})
};
}
//# sourceMappingURL=injectAutoFillMeta.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":["../../../src/modules/seo/injectAutoFillMeta.ts"],"sourcesContent":["import type { Config } from 'payload'\n\nimport type { SeoOption } from './types.js'\n\nimport { buildAutoFillMetaHook } from './autoFillMeta.js'\n\n/**\n * Injects the auto-fill meta hook into every SEO-enabled collection.\n *\n * Runs after @payloadcms/plugin-seo has added the meta group, appending a\n * beforeChange hook that fills empty meta from document content. Existing\n * hooks are preserved (plugin hook runs last, so editor input and other hooks\n * win first).\n */\nexport function injectAutoFillMeta(config: Config, seo: SeoOption): Config {\n // autoFill disabled explicitly — leave collections untouched\n if (seo.autoFill === false) {\n return config\n }\n\n const hook = buildAutoFillMetaHook(seo.autoFill)\n\n return {\n ...config,\n collections: (config.collections ?? []).map((collection) => {\n if (!seo.collections.includes(collection.slug)) {\n return collection\n }\n return {\n ...collection,\n hooks: {\n ...collection.hooks,\n beforeChange: [...(collection.hooks?.beforeChange ?? []), hook],\n },\n }\n }),\n }\n}\n"],"names":["buildAutoFillMetaHook","injectAutoFillMeta","config","seo","autoFill","hook","collections","map","collection","includes","slug","hooks","beforeChange"],"mappings":"AAIA,SAASA,qBAAqB,QAAQ,oBAAmB;AAEzD;;;;;;;CAOC,GACD,OAAO,SAASC,mBAAmBC,MAAc,EAAEC,GAAc;IAC/D,6DAA6D;IAC7D,IAAIA,IAAIC,QAAQ,KAAK,OAAO;QAC1B,OAAOF;IACT;IAEA,MAAMG,OAAOL,sBAAsBG,IAAIC,QAAQ;IAE/C,OAAO;QACL,GAAGF,MAAM;QACTI,aAAa,AAACJ,CAAAA,OAAOI,WAAW,IAAI,EAAE,AAAD,EAAGC,GAAG,CAAC,CAACC;YAC3C,IAAI,CAACL,IAAIG,WAAW,CAACG,QAAQ,CAACD,WAAWE,IAAI,GAAG;gBAC9C,OAAOF;YACT;YACA,OAAO;gBACL,GAAGA,UAAU;gBACbG,OAAO;oBACL,GAAGH,WAAWG,KAAK;oBACnBC,cAAc;2BAAKJ,WAAWG,KAAK,EAAEC,gBAAgB,EAAE;wBAAGP;qBAAK;gBACjE;YACF;QACF;IACF;AACF"}
-17
View File
@@ -1,17 +0,0 @@
import type { Config } from 'payload';
import type { SeoOption } from './types.js';
/**
* Wraps each SEO-enabled collection's fields into a tabbed UI: a "Content" tab
* holding the collection's own fields, and an "SEO" tab holding the `meta`
* group that @payloadcms/plugin-seo added.
*
* This replaces plugin-seo's own `tabbedUI`, which merges tabs by inspecting
* the first field and breaks when other plugins/fields (roles, slug) already
* sit in the collection — producing an empty SEO tab. Running this AFTER
* plugin-seo (so `meta` already exists) and after other field injections lets
* us build the tabs deterministically.
*
* If a collection's first field is already a tabs field, the SEO tab is
* appended to it instead of creating a new wrapper.
*/
export declare function injectSeoTabs(config: Config, seo: SeoOption): Config;
-79
View File
@@ -1,79 +0,0 @@
/**
* Wraps each SEO-enabled collection's fields into a tabbed UI: a "Content" tab
* holding the collection's own fields, and an "SEO" tab holding the `meta`
* group that @payloadcms/plugin-seo added.
*
* This replaces plugin-seo's own `tabbedUI`, which merges tabs by inspecting
* the first field and breaks when other plugins/fields (roles, slug) already
* sit in the collection — producing an empty SEO tab. Running this AFTER
* plugin-seo (so `meta` already exists) and after other field injections lets
* us build the tabs deterministically.
*
* If a collection's first field is already a tabs field, the SEO tab is
* appended to it instead of creating a new wrapper.
*/ export function injectSeoTabs(config, seo) {
return {
...config,
collections: (config.collections ?? []).map((collection)=>{
if (!seo.collections.includes(collection.slug)) {
return collection;
}
const fields = collection.fields ?? [];
// Separate the meta group (added by plugin-seo) from the rest.
const metaField = fields.find((f)=>'name' in f && f.name === 'meta');
const otherFields = fields.filter((f)=>!('name' in f && f.name === 'meta'));
// Nothing to do if plugin-seo hasn't added meta (shouldn't happen).
if (!metaField) {
return collection;
}
// If the collection already leads with a tabs field, append an SEO tab.
const firstField = otherFields[0];
if (firstField && firstField.type === 'tabs') {
const existingTabs = firstField;
const withSeoTab = {
...existingTabs,
tabs: [
...existingTabs.tabs,
{
fields: [
metaField
],
label: 'SEO'
}
]
};
return {
...collection,
fields: [
withSeoTab,
...otherFields.slice(1)
]
};
}
// Otherwise wrap everything: Content tab + SEO tab.
const tabs = {
type: 'tabs',
tabs: [
{
fields: otherFields,
label: collection.labels?.singular?.toString() || 'Content'
},
{
fields: [
metaField
],
label: 'SEO'
}
]
};
return {
...collection,
fields: [
tabs
]
};
})
};
}
//# sourceMappingURL=injectSeoTabs.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":["../../../src/modules/seo/injectSeoTabs.ts"],"sourcesContent":["import type { Config, Field, TabsField } from 'payload'\n\nimport type { SeoOption } from './types.js'\n\n/**\n * Wraps each SEO-enabled collection's fields into a tabbed UI: a \"Content\" tab\n * holding the collection's own fields, and an \"SEO\" tab holding the `meta`\n * group that @payloadcms/plugin-seo added.\n *\n * This replaces plugin-seo's own `tabbedUI`, which merges tabs by inspecting\n * the first field and breaks when other plugins/fields (roles, slug) already\n * sit in the collection — producing an empty SEO tab. Running this AFTER\n * plugin-seo (so `meta` already exists) and after other field injections lets\n * us build the tabs deterministically.\n *\n * If a collection's first field is already a tabs field, the SEO tab is\n * appended to it instead of creating a new wrapper.\n */\nexport function injectSeoTabs(config: Config, seo: SeoOption): Config {\n return {\n ...config,\n collections: (config.collections ?? []).map((collection) => {\n if (!seo.collections.includes(collection.slug)) {\n return collection\n }\n\n const fields = collection.fields ?? []\n\n // Separate the meta group (added by plugin-seo) from the rest.\n const metaField = fields.find(\n (f): f is { name: string } & Field => 'name' in f && f.name === 'meta',\n )\n const otherFields = fields.filter(\n (f) => !('name' in f && f.name === 'meta'),\n )\n\n // Nothing to do if plugin-seo hasn't added meta (shouldn't happen).\n if (!metaField) {return collection}\n\n // If the collection already leads with a tabs field, append an SEO tab.\n const firstField = otherFields[0]\n if (firstField && firstField.type === 'tabs') {\n const existingTabs = firstField\n const withSeoTab: TabsField = {\n ...existingTabs,\n tabs: [...existingTabs.tabs, { fields: [metaField], label: 'SEO' }],\n }\n return {\n ...collection,\n fields: [withSeoTab, ...otherFields.slice(1)],\n }\n }\n\n // Otherwise wrap everything: Content tab + SEO tab.\n const tabs: TabsField = {\n type: 'tabs',\n tabs: [\n { fields: otherFields, label: collection.labels?.singular?.toString() || 'Content' },\n { fields: [metaField], label: 'SEO' },\n ],\n }\n\n return {\n ...collection,\n fields: [tabs],\n }\n }),\n }\n}\n"],"names":["injectSeoTabs","config","seo","collections","map","collection","includes","slug","fields","metaField","find","f","name","otherFields","filter","firstField","type","existingTabs","withSeoTab","tabs","label","slice","labels","singular","toString"],"mappings":"AAIA;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASA,cAAcC,MAAc,EAAEC,GAAc;IAC1D,OAAO;QACL,GAAGD,MAAM;QACTE,aAAa,AAACF,CAAAA,OAAOE,WAAW,IAAI,EAAE,AAAD,EAAGC,GAAG,CAAC,CAACC;YAC3C,IAAI,CAACH,IAAIC,WAAW,CAACG,QAAQ,CAACD,WAAWE,IAAI,GAAG;gBAC9C,OAAOF;YACT;YAEA,MAAMG,SAASH,WAAWG,MAAM,IAAI,EAAE;YAEtC,+DAA+D;YAC/D,MAAMC,YAAYD,OAAOE,IAAI,CAC3B,CAACC,IAAqC,UAAUA,KAAKA,EAAEC,IAAI,KAAK;YAElE,MAAMC,cAAcL,OAAOM,MAAM,CAC/B,CAACH,IAAM,CAAE,CAAA,UAAUA,KAAKA,EAAEC,IAAI,KAAK,MAAK;YAG1C,oEAAoE;YACpE,IAAI,CAACH,WAAW;gBAAC,OAAOJ;YAAU;YAElC,wEAAwE;YACxE,MAAMU,aAAaF,WAAW,CAAC,EAAE;YACjC,IAAIE,cAAcA,WAAWC,IAAI,KAAK,QAAQ;gBAC5C,MAAMC,eAAeF;gBACrB,MAAMG,aAAwB;oBAC5B,GAAGD,YAAY;oBACfE,MAAM;2BAAIF,aAAaE,IAAI;wBAAE;4BAAEX,QAAQ;gCAACC;6BAAU;4BAAEW,OAAO;wBAAM;qBAAE;gBACrE;gBACA,OAAO;oBACL,GAAGf,UAAU;oBACbG,QAAQ;wBAACU;2BAAeL,YAAYQ,KAAK,CAAC;qBAAG;gBAC/C;YACF;YAEA,oDAAoD;YACpD,MAAMF,OAAkB;gBACtBH,MAAM;gBACNG,MAAM;oBACJ;wBAAEX,QAAQK;wBAAaO,OAAOf,WAAWiB,MAAM,EAAEC,UAAUC,cAAc;oBAAU;oBACnF;wBAAEhB,QAAQ;4BAACC;yBAAU;wBAAEW,OAAO;oBAAM;iBACrC;YACH;YAEA,OAAO;gBACL,GAAGf,UAAU;gBACbG,QAAQ;oBAACW;iBAAK;YAChB;QACF;IACF;AACF"}
-24
View File
@@ -1,24 +0,0 @@
import type { BasePayload } from 'payload';
import type { TitleOrder } from './composeTitle.js';
export type SiteMetaConfig = {
/** Slug of the page assigned as Homepage; collapses to the locale root. */
homeSlug?: string;
order?: TitleOrder;
/** Padded separator, e.g. ' | ' — the panel stores the bare character. */
separator?: string;
siteName: null | string;
};
/**
* Reads the site-wide inputs to title and URL composition from SiteSettings.
*
* Split out of metadata assembly because these are one concern with one source:
* whatever an editor set in the panel. Metadata generation shouldn't also know
* that the separator arrives unpadded, or that the home slug hides inside a
* relationship — it should receive a resolved config.
*/
export declare function readSiteMetaConfig({ locale, payload, settingsSlug, siteNameField, }: {
locale: string;
payload: BasePayload;
settingsSlug?: string;
siteNameField?: string;
}): Promise<SiteMetaConfig>;
-26
View File
@@ -1,26 +0,0 @@
/**
* Reads the site-wide inputs to title and URL composition from SiteSettings.
*
* Split out of metadata assembly because these are one concern with one source:
* whatever an editor set in the panel. Metadata generation shouldn't also know
* that the separator arrives unpadded, or that the home slug hides inside a
* relationship — it should receive a resolved config.
*/ export async function readSiteMetaConfig({ locale, payload, settingsSlug = 'site-settings', siteNameField = 'siteName' }) {
// depth 1 populates the homepage relationship, so its slug is available
// without a second query.
const settings = await payload.findGlobal({
slug: settingsSlug,
depth: 1,
locale: locale
});
const homepage = settings.homepage && typeof settings.homepage === 'object' ? settings.homepage : null;
return {
siteName: settings[siteNameField] ?? null,
// The panel stores '|'; titles need it padded.
homeSlug: typeof homepage?.slug === 'string' ? homepage.slug : undefined,
order: settings.titleOrder ?? undefined,
separator: settings.titleSeparator ? ` ${settings.titleSeparator} ` : undefined
};
}
//# sourceMappingURL=readSiteMetaConfig.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":["../../../src/modules/seo/readSiteMetaConfig.ts"],"sourcesContent":["import type { BasePayload } from 'payload'\n\nimport type { TitleOrder } from './composeTitle.js'\n\ntype SettingsShape = {\n [key: string]: unknown\n homepage?: { id: number | string; slug?: unknown } | null | number | string\n titleOrder?: null | TitleOrder\n titleSeparator?: null | string\n}\n\nexport type SiteMetaConfig = {\n /** Slug of the page assigned as Homepage; collapses to the locale root. */\n homeSlug?: string\n order?: TitleOrder\n /** Padded separator, e.g. ' | ' — the panel stores the bare character. */\n separator?: string\n siteName: null | string\n}\n\n/**\n * Reads the site-wide inputs to title and URL composition from SiteSettings.\n *\n * Split out of metadata assembly because these are one concern with one source:\n * whatever an editor set in the panel. Metadata generation shouldn't also know\n * that the separator arrives unpadded, or that the home slug hides inside a\n * relationship — it should receive a resolved config.\n */\nexport async function readSiteMetaConfig({\n locale,\n payload,\n settingsSlug = 'site-settings',\n siteNameField = 'siteName',\n}: {\n locale: string\n payload: BasePayload\n settingsSlug?: string\n siteNameField?: string\n}): Promise<SiteMetaConfig> {\n // depth 1 populates the homepage relationship, so its slug is available\n // without a second query.\n const settings = (await payload.findGlobal({\n slug: settingsSlug as never,\n depth: 1,\n locale: locale as never,\n })) as SettingsShape\n\n const homepage =\n settings.homepage && typeof settings.homepage === 'object' ? settings.homepage : null\n\n return {\n siteName: (settings[siteNameField] as string | undefined) ?? null,\n // The panel stores '|'; titles need it padded.\n homeSlug: typeof homepage?.slug === 'string' ? homepage.slug : undefined,\n order: settings.titleOrder ?? undefined,\n separator: settings.titleSeparator ? ` ${settings.titleSeparator} ` : undefined,\n }\n}\n"],"names":["readSiteMetaConfig","locale","payload","settingsSlug","siteNameField","settings","findGlobal","slug","depth","homepage","siteName","homeSlug","undefined","order","titleOrder","separator","titleSeparator"],"mappings":"AAoBA;;;;;;;CAOC,GACD,OAAO,eAAeA,mBAAmB,EACvCC,MAAM,EACNC,OAAO,EACPC,eAAe,eAAe,EAC9BC,gBAAgB,UAAU,EAM3B;IACC,wEAAwE;IACxE,0BAA0B;IAC1B,MAAMC,WAAY,MAAMH,QAAQI,UAAU,CAAC;QACzCC,MAAMJ;QACNK,OAAO;QACPP,QAAQA;IACV;IAEA,MAAMQ,WACJJ,SAASI,QAAQ,IAAI,OAAOJ,SAASI,QAAQ,KAAK,WAAWJ,SAASI,QAAQ,GAAG;IAEnF,OAAO;QACLC,UAAU,AAACL,QAAQ,CAACD,cAAc,IAA2B;QAC7D,+CAA+C;QAC/CO,UAAU,OAAOF,UAAUF,SAAS,WAAWE,SAASF,IAAI,GAAGK;QAC/DC,OAAOR,SAASS,UAAU,IAAIF;QAC9BG,WAAWV,SAASW,cAAc,GAAG,CAAC,CAAC,EAAEX,SAASW,cAAc,CAAC,CAAC,CAAC,GAAGJ;IACxE;AACF"}
-18
View File
@@ -1,18 +0,0 @@
import type { Plugin } from 'payload';
import type { SeoOption } from './types.js';
type BuildSeoPluginArgs = {
seo: SeoOption;
/** Upload collection slug for the meta image (typically 'media'). */
uploadsCollection?: string;
};
/**
* Configures @payloadcms/plugin-seo from IPAL's SeoOption.
*
* Adds the `meta` field group (title, description, image) to the client's
* chosen collections. tabbedUI is intentionally NOT used — it merges tabs by
* inspecting the first field, which breaks when other plugins/fields (roles,
* slug) already sit in the collection, leaving an empty SEO tab. Instead the
* SEO UI fields are injected into a controlled tab by injectSeoTabs.
*/
export declare function buildSeoPlugin({ seo, uploadsCollection }: BuildSeoPluginArgs): Plugin;
export {};
-40
View File
@@ -1,40 +0,0 @@
import { seoPlugin } from '@payloadcms/plugin-seo';
/**
* Escape hatch from automatic title composition: whatever is typed here becomes
* the entire title — no site name, no separator. Useful on a home page, where
* composing would produce "Intecion Software | Intecion Software".
*/ const titleOverrideField = {
name: 'titleOverride',
type: 'text',
admin: {
description: 'Use this exact text as the browser-tab title — no site name, no separator. Leave empty to compose the title automatically.'
},
localized: true
};
/**
* Configures @payloadcms/plugin-seo from IPAL's SeoOption.
*
* Adds the `meta` field group (title, description, image) to the client's
* chosen collections. tabbedUI is intentionally NOT used — it merges tabs by
* inspecting the first field, which breaks when other plugins/fields (roles,
* slug) already sit in the collection, leaving an empty SEO tab. Instead the
* SEO UI fields are injected into a controlled tab by injectSeoTabs.
*/ export function buildSeoPlugin({ seo, uploadsCollection = 'media' }) {
return seoPlugin({
collections: seo.collections,
fields: ({ defaultFields })=>[
...defaultFields,
titleOverrideField,
...seo.fields ?? []
],
uploadsCollection,
...seo.generateTitle && {
generateTitle: seo.generateTitle
},
...seo.generateDescription && {
generateDescription: seo.generateDescription
}
});
}
//# sourceMappingURL=seoPluginConfig.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":["../../../src/modules/seo/seoPluginConfig.ts"],"sourcesContent":["import type { Field, Plugin } from 'payload'\n\nimport { seoPlugin } from '@payloadcms/plugin-seo'\n\nimport type { SeoOption } from './types.js'\n\ntype BuildSeoPluginArgs = {\n seo: SeoOption\n /** Upload collection slug for the meta image (typically 'media'). */\n uploadsCollection?: string\n}\n\n/**\n * Escape hatch from automatic title composition: whatever is typed here becomes\n * the entire title — no site name, no separator. Useful on a home page, where\n * composing would produce \"Intecion Software | Intecion Software\".\n */\nconst titleOverrideField: Field = {\n name: 'titleOverride',\n type: 'text',\n admin: {\n description:\n 'Use this exact text as the browser-tab title — no site name, no separator. Leave empty to compose the title automatically.',\n },\n localized: true,\n}\n\n/**\n * Configures @payloadcms/plugin-seo from IPAL's SeoOption.\n *\n * Adds the `meta` field group (title, description, image) to the client's\n * chosen collections. tabbedUI is intentionally NOT used — it merges tabs by\n * inspecting the first field, which breaks when other plugins/fields (roles,\n * slug) already sit in the collection, leaving an empty SEO tab. Instead the\n * SEO UI fields are injected into a controlled tab by injectSeoTabs.\n */\nexport function buildSeoPlugin({ seo, uploadsCollection = 'media' }: BuildSeoPluginArgs): Plugin {\n return seoPlugin({\n collections: seo.collections,\n fields: ({ defaultFields }) => [...defaultFields, titleOverrideField, ...(seo.fields ?? [])],\n uploadsCollection,\n ...(seo.generateTitle && { generateTitle: seo.generateTitle }),\n ...(seo.generateDescription && { generateDescription: seo.generateDescription }),\n })\n}\n"],"names":["seoPlugin","titleOverrideField","name","type","admin","description","localized","buildSeoPlugin","seo","uploadsCollection","collections","fields","defaultFields","generateTitle","generateDescription"],"mappings":"AAEA,SAASA,SAAS,QAAQ,yBAAwB;AAUlD;;;;CAIC,GACD,MAAMC,qBAA4B;IAChCC,MAAM;IACNC,MAAM;IACNC,OAAO;QACLC,aACE;IACJ;IACAC,WAAW;AACb;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASC,eAAe,EAAEC,GAAG,EAAEC,oBAAoB,OAAO,EAAsB;IACrF,OAAOT,UAAU;QACfU,aAAaF,IAAIE,WAAW;QAC5BC,QAAQ,CAAC,EAAEC,aAAa,EAAE,GAAK;mBAAIA;gBAAeX;mBAAwBO,IAAIG,MAAM,IAAI,EAAE;aAAE;QAC5FF;QACA,GAAID,IAAIK,aAAa,IAAI;YAAEA,eAAeL,IAAIK,aAAa;QAAC,CAAC;QAC7D,GAAIL,IAAIM,mBAAmB,IAAI;YAAEA,qBAAqBN,IAAIM,mBAAmB;QAAC,CAAC;IACjF;AACF"}
-18
View File
@@ -1,18 +0,0 @@
import type { BasePayload } from 'payload';
import type { I18nConfig, LocalizedSlugs } from '../i18n/index.js';
/**
* Reads a document's slug in every locale — the map hreflang alternates are
* built from.
*
* Needs its own query because a document fetched in one locale returns `slug`
* as a plain string. Fetching with `locale: 'all'` turns *every* localized
* field into a locale→value map, which is right for the slug and wrong for
* everything else (a title map would break title composition), so this asks
* only for what it needs and at depth 0.
*/
export declare function slugsAcrossLocales({ id, collection, config, payload, }: {
collection: string;
config: I18nConfig;
id: number | string;
payload: BasePayload;
}): Promise<LocalizedSlugs>;
-24
View File
@@ -1,24 +0,0 @@
import { getLocalizedSlugs } from '../i18n/index.js';
/**
* Reads a document's slug in every locale — the map hreflang alternates are
* built from.
*
* Needs its own query because a document fetched in one locale returns `slug`
* as a plain string. Fetching with `locale: 'all'` turns *every* localized
* field into a locale→value map, which is right for the slug and wrong for
* everything else (a title map would break title composition), so this asks
* only for what it needs and at depth 0.
*/ export async function slugsAcrossLocales({ id, collection, config, payload }) {
const doc = await payload.findByID({
id,
collection: collection,
depth: 0,
locale: 'all'
});
return doc.slug && typeof doc.slug === 'object' ? getLocalizedSlugs({
config,
slugField: doc.slug
}) : {};
}
//# sourceMappingURL=slugsAcrossLocales.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":["../../../src/modules/seo/slugsAcrossLocales.ts"],"sourcesContent":["import type { BasePayload } from 'payload'\n\nimport type { I18nConfig, LocalizedSlugs } from '../i18n/index.js'\n\nimport { getLocalizedSlugs } from '../i18n/index.js'\n\n/**\n * Reads a document's slug in every locale — the map hreflang alternates are\n * built from.\n *\n * Needs its own query because a document fetched in one locale returns `slug`\n * as a plain string. Fetching with `locale: 'all'` turns *every* localized\n * field into a locale→value map, which is right for the slug and wrong for\n * everything else (a title map would break title composition), so this asks\n * only for what it needs and at depth 0.\n */\nexport async function slugsAcrossLocales({\n id,\n collection,\n config,\n payload,\n}: {\n collection: string\n config: I18nConfig\n id: number | string\n payload: BasePayload\n}): Promise<LocalizedSlugs> {\n const doc = (await payload.findByID({\n id,\n collection: collection as never,\n depth: 0,\n locale: 'all',\n })) as { slug?: unknown }\n\n return doc.slug && typeof doc.slug === 'object'\n ? getLocalizedSlugs({ config, slugField: doc.slug as Record<string, unknown> })\n : {}\n}\n"],"names":["getLocalizedSlugs","slugsAcrossLocales","id","collection","config","payload","doc","findByID","depth","locale","slug","slugField"],"mappings":"AAIA,SAASA,iBAAiB,QAAQ,mBAAkB;AAEpD;;;;;;;;;CASC,GACD,OAAO,eAAeC,mBAAmB,EACvCC,EAAE,EACFC,UAAU,EACVC,MAAM,EACNC,OAAO,EAMR;IACC,MAAMC,MAAO,MAAMD,QAAQE,QAAQ,CAAC;QAClCL;QACAC,YAAYA;QACZK,OAAO;QACPC,QAAQ;IACV;IAEA,OAAOH,IAAII,IAAI,IAAI,OAAOJ,IAAII,IAAI,KAAK,WACnCV,kBAAkB;QAAEI;QAAQO,WAAWL,IAAII,IAAI;IAA4B,KAC3E,CAAC;AACP"}
-40
View File
@@ -1,40 +0,0 @@
import type { Field } from 'payload';
import type { AutoFillMapping } from './autoFillMeta.js';
/**
* SEO configuration.
*
* The plugin wires @payloadcms/plugin-seo into the client's config and adds
* locale-aware metadata helpers on top. Collections come from options because
* the plugin doesn't own the client's content collections (e.g. Pages).
*/
export type SeoOption = {
/**
* Auto-fill empty meta from document fields on save. Defaults to the website
* template mapping (title → meta.title). Set to false to disable.
*/
autoFill?: AutoFillMapping | false;
/** Collection slugs that receive SEO meta fields, e.g. ['pages', 'posts']. */
collections: string[];
/** Extra fields appended to the SEO group in the admin. */
fields?: Field[];
/** Optional: customize how meta descriptions are generated. */
generateDescription?: (args: {
doc: Record<string, unknown>;
}) => string;
/** Optional: customize how meta titles are generated in the admin preview. */
generateTitle?: (args: {
doc: Record<string, unknown>;
}) => string;
};
/**
* Minimal shape of the SEO meta group as stored on a document by
* @payloadcms/plugin-seo. The client's generated types are richer; helpers
* depend only on this.
*/
export type SeoMeta = {
description?: null | string;
image?: unknown;
title?: null | string;
/** When set, used as the whole title — no site name, no separator. */
titleOverride?: null | string;
};
-7
View File
@@ -1,7 +0,0 @@
/**
* Minimal shape of the SEO meta group as stored on a document by
* @payloadcms/plugin-seo. The client's generated types are richer; helpers
* depend only on this.
*/ export { };
//# sourceMappingURL=types.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"sources":["../../../src/modules/seo/types.ts"],"sourcesContent":["import type { Field } from 'payload'\n\nimport type { AutoFillMapping } from './autoFillMeta.js'\n\n/**\n * SEO configuration.\n *\n * The plugin wires @payloadcms/plugin-seo into the client's config and adds\n * locale-aware metadata helpers on top. Collections come from options because\n * the plugin doesn't own the client's content collections (e.g. Pages).\n */\nexport type SeoOption = {\n /**\n * Auto-fill empty meta from document fields on save. Defaults to the website\n * template mapping (title → meta.title). Set to false to disable.\n */\n autoFill?: AutoFillMapping | false\n /** Collection slugs that receive SEO meta fields, e.g. ['pages', 'posts']. */\n collections: string[]\n /** Extra fields appended to the SEO group in the admin. */\n fields?: Field[]\n /** Optional: customize how meta descriptions are generated. */\n generateDescription?: (args: { doc: Record<string, unknown> }) => string\n /** Optional: customize how meta titles are generated in the admin preview. */\n generateTitle?: (args: { doc: Record<string, unknown> }) => string\n}\n\n/**\n * Minimal shape of the SEO meta group as stored on a document by\n * @payloadcms/plugin-seo. The client's generated types are richer; helpers\n * depend only on this.\n */\nexport type SeoMeta = {\n description?: null | string\n image?: unknown\n title?: null | string\n /** When set, used as the whole title — no site name, no separator. */\n titleOverride?: null | string\n}\n"],"names":[],"mappings":"AA2BA;;;;CAIC,GACD,WAMC"}