SEO: multilingual homepage (homeSlug per locale in path/sitemap), Article + llms.txt generators, siteDescription fallback, composeTitle dedup
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import type { Field } from 'payload';
|
import type { Field } from 'payload';
|
||||||
/**
|
/**
|
||||||
* General site identity fields.
|
* General site identity fields.
|
||||||
* Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo, favicon).
|
* Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo).
|
||||||
*/
|
*/
|
||||||
export declare const generalFields: Field[];
|
export declare const generalFields: Field[];
|
||||||
|
|||||||
+11
-4
@@ -1,7 +1,7 @@
|
|||||||
import { validateFaviconField } from '../../../modules/seo/index.js';
|
import { validateFaviconField } from '../../../modules/seo/index.js';
|
||||||
/**
|
/**
|
||||||
* General site identity fields.
|
* General site identity fields.
|
||||||
* Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo, favicon).
|
* Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo).
|
||||||
*/ export const generalFields = [
|
*/ export const generalFields = [
|
||||||
{
|
{
|
||||||
name: 'siteName',
|
name: 'siteName',
|
||||||
@@ -12,6 +12,14 @@ import { validateFaviconField } from '../../../modules/seo/index.js';
|
|||||||
localized: true,
|
localized: true,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'siteDescription',
|
||||||
|
type: 'textarea',
|
||||||
|
admin: {
|
||||||
|
description: 'Fallback meta description when a page has none. Also used for llms.txt and Open Graph.'
|
||||||
|
},
|
||||||
|
localized: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'titleOrder',
|
name: 'titleOrder',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
@@ -64,7 +72,7 @@ import { validateFaviconField } from '../../../modules/seo/index.js';
|
|||||||
name: 'logo',
|
name: 'logo',
|
||||||
type: 'upload',
|
type: 'upload',
|
||||||
admin: {
|
admin: {
|
||||||
description: 'Primary site logo. Also used for Organization structured data (buildOrganizationJsonLd).'
|
description: 'Primary site logo.'
|
||||||
},
|
},
|
||||||
relationTo: 'media'
|
relationTo: 'media'
|
||||||
},
|
},
|
||||||
@@ -83,8 +91,7 @@ import { validateFaviconField } from '../../../modules/seo/index.js';
|
|||||||
description: 'Square icon, PNG or SVG, min. 48×48px (Google requires this to show it in search). Rendered via buildIconsMetadata.'
|
description: 'Square icon, PNG or SVG, min. 48×48px (Google requires this to show it in search). Rendered via buildIconsMetadata.'
|
||||||
},
|
},
|
||||||
hooks: {
|
hooks: {
|
||||||
// Warns the editor at save time if the favicon is too small (<48×48) or
|
// Warn the editor if the favicon is too small / not square for Google.
|
||||||
// not square — Google won't display such favicons in search results.
|
|
||||||
beforeValidate: [
|
beforeValidate: [
|
||||||
validateFaviconField
|
validateFaviconField
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"sources":["../../../../src/globals/SiteSettings/fields/general.ts"],"sourcesContent":["import type { Field } from 'payload'\n\nimport { validateFaviconField } from '../../../modules/seo/index.js'\n\n/**\n * General site identity fields.\n * Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo, favicon).\n */\nexport const generalFields: Field[] = [\n {\n name: 'siteName',\n type: 'text',\n admin: {\n description: 'Used in page titles and Open Graph metadata.',\n },\n localized: true,\n required: true,\n },\n {\n name: 'titleOrder',\n type: 'select',\n admin: {\n description: 'Which comes first in browser tabs.',\n },\n defaultValue: 'page-first',\n options: [\n { label: 'Page first — About Us | Acme', value: 'page-first' },\n { label: 'Site first — Acme | About Us', value: 'site-first' },\n ],\n },\n {\n name: 'titleSeparator',\n type: 'select',\n admin: {\n description: 'Separates the page title from the site name in browser tabs.',\n },\n defaultValue: '|',\n options: [\n { label: 'Pipe — Page | Site', value: '|' },\n { label: 'Dash — Page – Site', value: '–' },\n { label: 'Hyphen — Page - Site', value: '-' },\n { label: 'Bullet — Page · Site', value: '·' },\n { label: 'Slash — Page / Site', value: '/' },\n ],\n },\n {\n name: 'logo',\n type: 'upload',\n admin: {\n description:\n 'Primary site logo. Also used for Organization structured data (buildOrganizationJsonLd).',\n },\n relationTo: 'media',\n },\n {\n name: 'defaultShareImage',\n type: 'upload',\n admin: {\n description: 'Fallback Open Graph image when a page has none.',\n },\n relationTo: 'media',\n },\n {\n name: 'favicon',\n type: 'upload',\n admin: {\n description:\n 'Square icon, PNG or SVG, min. 48×48px (Google requires this to show it in search). Rendered via buildIconsMetadata.',\n },\n hooks: {\n // Warns the editor at save time if the favicon is too small (<48×48) or\n // not square — Google won't display such favicons in search results.\n beforeValidate: [validateFaviconField],\n },\n relationTo: 'media',\n },\n]\n"],"names":["validateFaviconField","generalFields","name","type","admin","description","localized","required","defaultValue","options","label","value","relationTo","hooks","beforeValidate"],"mappings":"AAEA,SAASA,oBAAoB,QAAQ,gCAA+B;AAEpE;;;CAGC,GACD,OAAO,MAAMC,gBAAyB;IACpC;QACEC,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAC,WAAW;QACXC,UAAU;IACZ;IACA;QACEL,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAG,cAAc;QACdC,SAAS;YACP;gBAAEC,OAAO;gBAAkCC,OAAO;YAAa;YAC/D;gBAAED,OAAO;gBAAkCC,OAAO;YAAa;SAChE;IACH;IACA;QACET,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAG,cAAc;QACdC,SAAS;YACP;gBAAEC,OAAO;gBAAwBC,OAAO;YAAI;YAC5C;gBAAED,OAAO;gBAAwBC,OAAO;YAAI;YAC5C;gBAAED,OAAO;gBAA0BC,OAAO;YAAI;YAC9C;gBAAED,OAAO;gBAA0BC,OAAO;YAAI;YAC9C;gBAAED,OAAO;gBAAyBC,OAAO;YAAI;SAC9C;IACH;IACA;QACET,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aACE;QACJ;QACAO,YAAY;IACd;IACA;QACEV,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAO,YAAY;IACd;IACA;QACEV,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aACE;QACJ;QACAQ,OAAO;YACL,wEAAwE;YACxE,qEAAqE;YACrEC,gBAAgB;gBAACd;aAAqB;QACxC;QACAY,YAAY;IACd;CACD,CAAA"}
|
{"version":3,"sources":["../../../../src/globals/SiteSettings/fields/general.ts"],"sourcesContent":["import type { Field } from 'payload'\n\nimport { validateFaviconField } from '../../../modules/seo/index.js'\n\n/**\n * General site identity fields.\n * Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo).\n */\nexport const generalFields: Field[] = [\n {\n name: 'siteName',\n type: 'text',\n admin: {\n description: 'Used in page titles and Open Graph metadata.',\n },\n localized: true,\n required: true,\n },\n {\n name: 'siteDescription',\n type: 'textarea',\n admin: {\n description:\n 'Fallback meta description when a page has none. Also used for llms.txt and Open Graph.',\n },\n localized: true,\n },\n {\n name: 'titleOrder',\n type: 'select',\n admin: {\n description: 'Which comes first in browser tabs.',\n },\n defaultValue: 'page-first',\n options: [\n { label: 'Page first — About Us | Acme', value: 'page-first' },\n { label: 'Site first — Acme | About Us', value: 'site-first' },\n ],\n },\n {\n name: 'titleSeparator',\n type: 'select',\n admin: {\n description: 'Separates the page title from the site name in browser tabs.',\n },\n defaultValue: '|',\n options: [\n { label: 'Pipe — Page | Site', value: '|' },\n { label: 'Dash — Page – Site', value: '–' },\n { label: 'Hyphen — Page - Site', value: '-' },\n { label: 'Bullet — Page · Site', value: '·' },\n { label: 'Slash — Page / Site', value: '/' },\n ],\n },\n {\n name: 'logo',\n type: 'upload',\n admin: {\n description: 'Primary site logo.',\n },\n relationTo: 'media',\n },\n {\n name: 'defaultShareImage',\n type: 'upload',\n admin: {\n description: 'Fallback Open Graph image when a page has none.',\n },\n relationTo: 'media',\n },\n {\n name: 'favicon',\n type: 'upload',\n admin: {\n description:\n 'Square icon, PNG or SVG, min. 48×48px (Google requires this to show it in search). Rendered via buildIconsMetadata.',\n },\n hooks: {\n // Warn the editor if the favicon is too small / not square for Google.\n beforeValidate: [validateFaviconField],\n },\n relationTo: 'media',\n },\n]\n"],"names":["validateFaviconField","generalFields","name","type","admin","description","localized","required","defaultValue","options","label","value","relationTo","hooks","beforeValidate"],"mappings":"AAEA,SAASA,oBAAoB,QAAQ,gCAA+B;AAEpE;;;CAGC,GACD,OAAO,MAAMC,gBAAyB;IACpC;QACEC,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAC,WAAW;QACXC,UAAU;IACZ;IACA;QACEL,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aACE;QACJ;QACAC,WAAW;IACb;IACA;QACEJ,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAG,cAAc;QACdC,SAAS;YACP;gBAAEC,OAAO;gBAAkCC,OAAO;YAAa;YAC/D;gBAAED,OAAO;gBAAkCC,OAAO;YAAa;SAChE;IACH;IACA;QACET,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAG,cAAc;QACdC,SAAS;YACP;gBAAEC,OAAO;gBAAwBC,OAAO;YAAI;YAC5C;gBAAED,OAAO;gBAAwBC,OAAO;YAAI;YAC5C;gBAAED,OAAO;gBAA0BC,OAAO;YAAI;YAC9C;gBAAED,OAAO;gBAA0BC,OAAO;YAAI;YAC9C;gBAAED,OAAO;gBAAyBC,OAAO;YAAI;SAC9C;IACH;IACA;QACET,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAO,YAAY;IACd;IACA;QACEV,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aAAa;QACf;QACAO,YAAY;IACd;IACA;QACEV,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,aACE;QACJ;QACAQ,OAAO;YACL,uEAAuE;YACvEC,gBAAgB;gBAACd;aAAqB;QACxC;QACAY,YAAY;IACd;CACD,CAAA"}
|
||||||
Vendored
+1
-1
@@ -29,7 +29,7 @@ export type { GlobalQueryOptions } from './modules/payload/index.js';
|
|||||||
export { getGlobal, getSiteIntegrations, getSiteSettings, SITE_INTEGRATIONS_SLUG, SITE_SETTINGS_SLUG, } from './modules/payload/index.js';
|
export { getGlobal, getSiteIntegrations, getSiteSettings, SITE_INTEGRATIONS_SLUG, SITE_SETTINGS_SLUG, } from './modules/payload/index.js';
|
||||||
export { buildSecurityHeaders } from './modules/security/index.js';
|
export { buildSecurityHeaders } from './modules/security/index.js';
|
||||||
export type { BuildSecurityHeadersArgs, SecurityHeader } from './modules/security/index.js';
|
export type { BuildSecurityHeadersArgs, SecurityHeader } from './modules/security/index.js';
|
||||||
export { buildArticleJsonLd, buildFaqJsonLd, buildIconsMetadata, buildLocalBusinessJsonLd, buildOrganizationJsonLd, buildServiceJsonLd, validateFaviconField, } from './modules/seo/index.js';
|
export { buildArticleJsonLd, buildFaqJsonLd, buildIconsMetadata, buildLlmsTxt, buildLocalBusinessJsonLd, buildOrganizationJsonLd, buildServiceJsonLd, validateFaviconField, } from './modules/seo/index.js';
|
||||||
export { buildBreadcrumbJsonLd, buildSiteNavigationJsonLd, buildWebSiteJsonLd, } from './modules/seo/index.js';
|
export { buildBreadcrumbJsonLd, buildSiteNavigationJsonLd, buildWebSiteJsonLd, } from './modules/seo/index.js';
|
||||||
export type { PageMetadata, SeoMeta, SeoOption } from './modules/seo/index.js';
|
export type { PageMetadata, SeoMeta, SeoOption } from './modules/seo/index.js';
|
||||||
export { buildHreflangAlternates, buildMetadata, composeTitle } from './modules/seo/index.js';
|
export { buildHreflangAlternates, buildMetadata, composeTitle } from './modules/seo/index.js';
|
||||||
|
|||||||
Vendored
+1
-1
@@ -18,7 +18,7 @@ export { getNotificationTexts, NOTIFICATION_FALLBACK, resolveFormMessage } from
|
|||||||
export { ALL_SYSTEM_PAGE_ROLES, getSystemPagePath } from './modules/pages/index.js';
|
export { ALL_SYSTEM_PAGE_ROLES, getSystemPagePath } from './modules/pages/index.js';
|
||||||
export { getGlobal, getSiteIntegrations, getSiteSettings, SITE_INTEGRATIONS_SLUG, SITE_SETTINGS_SLUG } from './modules/payload/index.js';
|
export { getGlobal, getSiteIntegrations, getSiteSettings, SITE_INTEGRATIONS_SLUG, SITE_SETTINGS_SLUG } from './modules/payload/index.js';
|
||||||
export { buildSecurityHeaders } from './modules/security/index.js';
|
export { buildSecurityHeaders } from './modules/security/index.js';
|
||||||
export { buildArticleJsonLd, buildFaqJsonLd, buildIconsMetadata, buildLocalBusinessJsonLd, buildOrganizationJsonLd, buildServiceJsonLd, validateFaviconField } from './modules/seo/index.js';
|
export { buildArticleJsonLd, buildFaqJsonLd, buildIconsMetadata, buildLlmsTxt, buildLocalBusinessJsonLd, buildOrganizationJsonLd, buildServiceJsonLd, validateFaviconField } from './modules/seo/index.js';
|
||||||
// Structured data (schema.org JSON-LD) — brand/sitelink signals for Google.
|
// Structured data (schema.org JSON-LD) — brand/sitelink signals for Google.
|
||||||
// WebSite (+ optional SearchAction), BreadcrumbList (per page), SiteNavigation.
|
// WebSite (+ optional SearchAction), BreadcrumbList (per page), SiteNavigation.
|
||||||
export { buildBreadcrumbJsonLd, buildSiteNavigationJsonLd, buildWebSiteJsonLd } from './modules/seo/index.js';
|
export { buildBreadcrumbJsonLd, buildSiteNavigationJsonLd, buildWebSiteJsonLd } from './modules/seo/index.js';
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+45
@@ -0,0 +1,45 @@
|
|||||||
|
import type { BasePayload } from 'payload';
|
||||||
|
import type { I18nConfig } from '../i18n/index.js';
|
||||||
|
type BuildLlmsTxtArgs = {
|
||||||
|
/** Absolute site URL (https://…). */
|
||||||
|
baseUrl: string;
|
||||||
|
config: I18nConfig;
|
||||||
|
/** Locale for names/descriptions. Defaults to config.defaultLocale. */
|
||||||
|
locale?: string;
|
||||||
|
/** Pages collection slug. Defaults to 'pages'. */
|
||||||
|
pagesSlug?: string;
|
||||||
|
payload: BasePayload;
|
||||||
|
/** Site settings global slug. Defaults to 'site-settings'. */
|
||||||
|
settingsSlug?: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Builds the body of /llms.txt — a Markdown file describing the site for AI
|
||||||
|
* agents / LLM crawlers, per the llmstxt.org convention. Mirrors buildRobots /
|
||||||
|
* buildSitemapEntries: the plugin already knows the site's name, description,
|
||||||
|
* and pages, so it can generate this automatically.
|
||||||
|
*
|
||||||
|
* Structure (llmstxt.org): H1 site name, a blockquote/summary, then a list of
|
||||||
|
* key pages as Markdown links with short descriptions. Agents read this to
|
||||||
|
* understand the site quickly without crawling everything.
|
||||||
|
*
|
||||||
|
* Wire it as a route that returns text/plain:
|
||||||
|
*
|
||||||
|
* // app/llms.txt/route.ts
|
||||||
|
* import { buildLlmsTxt } from '@intecion/ipal-kit'
|
||||||
|
* import { getCachedPayload } from '@/lib/content'
|
||||||
|
* import { i18nConfig } from '@/i18n.config'
|
||||||
|
* export const dynamic = 'force-dynamic'
|
||||||
|
* export async function GET() {
|
||||||
|
* const body = await buildLlmsTxt({
|
||||||
|
* payload: await getCachedPayload(),
|
||||||
|
* config: i18nConfig,
|
||||||
|
* baseUrl: process.env.NEXT_PUBLIC_SERVER_URL!,
|
||||||
|
* })
|
||||||
|
* return new Response(body, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } })
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Data comes from the panel (siteName, siteDescription, pages) — nothing
|
||||||
|
* hardcoded. Skips drafts and noindex pages (same as the sitemap).
|
||||||
|
*/
|
||||||
|
export declare function buildLlmsTxt({ baseUrl, config, locale, pagesSlug, payload, settingsSlug, }: BuildLlmsTxtArgs): Promise<string>;
|
||||||
|
export {};
|
||||||
Vendored
+90
@@ -0,0 +1,90 @@
|
|||||||
|
import { buildLocalizedPath } from '../i18n/index.js';
|
||||||
|
/**
|
||||||
|
* Builds the body of /llms.txt — a Markdown file describing the site for AI
|
||||||
|
* agents / LLM crawlers, per the llmstxt.org convention. Mirrors buildRobots /
|
||||||
|
* buildSitemapEntries: the plugin already knows the site's name, description,
|
||||||
|
* and pages, so it can generate this automatically.
|
||||||
|
*
|
||||||
|
* Structure (llmstxt.org): H1 site name, a blockquote/summary, then a list of
|
||||||
|
* key pages as Markdown links with short descriptions. Agents read this to
|
||||||
|
* understand the site quickly without crawling everything.
|
||||||
|
*
|
||||||
|
* Wire it as a route that returns text/plain:
|
||||||
|
*
|
||||||
|
* // app/llms.txt/route.ts
|
||||||
|
* import { buildLlmsTxt } from '@intecion/ipal-kit'
|
||||||
|
* import { getCachedPayload } from '@/lib/content'
|
||||||
|
* import { i18nConfig } from '@/i18n.config'
|
||||||
|
* export const dynamic = 'force-dynamic'
|
||||||
|
* export async function GET() {
|
||||||
|
* const body = await buildLlmsTxt({
|
||||||
|
* payload: await getCachedPayload(),
|
||||||
|
* config: i18nConfig,
|
||||||
|
* baseUrl: process.env.NEXT_PUBLIC_SERVER_URL!,
|
||||||
|
* })
|
||||||
|
* return new Response(body, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } })
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Data comes from the panel (siteName, siteDescription, pages) — nothing
|
||||||
|
* hardcoded. Skips drafts and noindex pages (same as the sitemap).
|
||||||
|
*/ export async function buildLlmsTxt({ baseUrl, config, locale, pagesSlug = 'pages', payload, settingsSlug = 'site-settings' }) {
|
||||||
|
const loc = locale ?? config.defaultLocale;
|
||||||
|
const origin = baseUrl.replace(/\/$/, '');
|
||||||
|
const settings = await payload.findGlobal({
|
||||||
|
slug: settingsSlug,
|
||||||
|
depth: 0,
|
||||||
|
locale: loc
|
||||||
|
});
|
||||||
|
const name = settings.siteName?.trim() || 'Website';
|
||||||
|
const description = settings.siteDescription?.trim();
|
||||||
|
const result = await payload.find({
|
||||||
|
collection: pagesSlug,
|
||||||
|
depth: 0,
|
||||||
|
limit: 1000,
|
||||||
|
locale: loc,
|
||||||
|
where: {
|
||||||
|
_status: {
|
||||||
|
not_equals: 'draft'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const lines = [
|
||||||
|
`# ${name}`,
|
||||||
|
''
|
||||||
|
];
|
||||||
|
if (description) {
|
||||||
|
lines.push(`> ${description}`, '');
|
||||||
|
}
|
||||||
|
const pageLinks = [];
|
||||||
|
for (const raw of result.docs){
|
||||||
|
if (raw._status && raw._status !== 'published') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (raw.meta?.noindex) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const title = typeof raw.title === 'string' ? raw.title : undefined;
|
||||||
|
const slug = typeof raw.slug === 'string' ? raw.slug : undefined;
|
||||||
|
if (!title || !slug) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const path = buildLocalizedPath({
|
||||||
|
config,
|
||||||
|
locale: loc,
|
||||||
|
slugs: {
|
||||||
|
[loc]: slug
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const pageDesc = typeof raw.meta?.description === 'string' ? ` — ${raw.meta.description}` : '';
|
||||||
|
pageLinks.push(`- [${title}](${origin}${path})${pageDesc}`);
|
||||||
|
}
|
||||||
|
if (pageLinks.length > 0) {
|
||||||
|
lines.push('## Strony', '', ...pageLinks, '');
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
//# sourceMappingURL=buildLlmsTxt.js.map
|
||||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+8
-2
@@ -31,7 +31,7 @@ type BuildMetadataArgs = {
|
|||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
config: I18nConfig;
|
config: I18nConfig;
|
||||||
/** Home slug that collapses to the locale root. Defaults to 'home'. */
|
/** Home slug that collapses to the locale root. Defaults to 'home'. */
|
||||||
homeSlug?: string | Record<string, string>;
|
homeSlug?: Record<string, string> | string;
|
||||||
/** Resolved OG image URL (page image or site defaultShareImage). */
|
/** Resolved OG image URL (page image or site defaultShareImage). */
|
||||||
imageUrl?: null | string;
|
imageUrl?: null | string;
|
||||||
/** Current locale being rendered. */
|
/** Current locale being rendered. */
|
||||||
@@ -63,6 +63,12 @@ type BuildMetadataArgs = {
|
|||||||
query?: string;
|
query?: string;
|
||||||
/** Separator between page title and site name. Defaults to ' | '. */
|
/** Separator between page title and site name. Defaults to ' | '. */
|
||||||
separator?: string;
|
separator?: string;
|
||||||
|
/**
|
||||||
|
* Global fallback description (from SiteSettings.siteDescription). Used when a
|
||||||
|
* page has no meta.description — better than emitting no description at all
|
||||||
|
* (Google would then scrape random on-page text for the snippet).
|
||||||
|
*/
|
||||||
|
siteDescription?: null | string;
|
||||||
/** Site name for title composition and OG. */
|
/** Site name for title composition and OG. */
|
||||||
siteName?: null | string;
|
siteName?: null | string;
|
||||||
/** slug per locale for this document — drives canonical + hreflang. */
|
/** slug per locale for this document — drives canonical + hreflang. */
|
||||||
@@ -77,5 +83,5 @@ type BuildMetadataArgs = {
|
|||||||
* pieces (meta group, site name, image URL, localized slugs) and passes them
|
* pieces (meta group, site name, image URL, localized slugs) and passes them
|
||||||
* in — the plugin composes, it doesn't fetch.
|
* in — the plugin composes, it doesn't fetch.
|
||||||
*/
|
*/
|
||||||
export declare function buildMetadata({ baseUrl, config, homeSlug, imageUrl, locale, meta, order, pageTitle, prefix, query, separator, siteName, slugs, }: BuildMetadataArgs): PageMetadata;
|
export declare function buildMetadata({ baseUrl, config, homeSlug, imageUrl, locale, meta, order, pageTitle, prefix, query, separator, siteDescription, siteName, slugs, }: BuildMetadataArgs): PageMetadata;
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
Vendored
+2
-2
@@ -9,7 +9,7 @@ import { buildHreflangAlternates } from './hreflang.js';
|
|||||||
* Designed for use inside Next.js `generateMetadata`. The caller resolves the
|
* Designed for use inside Next.js `generateMetadata`. The caller resolves the
|
||||||
* pieces (meta group, site name, image URL, localized slugs) and passes them
|
* pieces (meta group, site name, image URL, localized slugs) and passes them
|
||||||
* in — the plugin composes, it doesn't fetch.
|
* in — the plugin composes, it doesn't fetch.
|
||||||
*/ export function buildMetadata({ baseUrl, config, homeSlug = 'home', imageUrl, locale, meta, order, pageTitle, prefix, query, separator, siteName, slugs }) {
|
*/ export function buildMetadata({ baseUrl, config, homeSlug = 'home', imageUrl, locale, meta, order, pageTitle, prefix, query, separator, siteDescription, siteName, slugs }) {
|
||||||
// Title source priority: titleOverride (exact, wins outright) > meta.title
|
// Title source priority: titleOverride (exact, wins outright) > meta.title
|
||||||
// (SEO title an editor set) > pageTitle (the document's own name). This means
|
// (SEO title an editor set) > pageTitle (the document's own name). This means
|
||||||
// a page with no SEO title still shows its name (e.g. 'Sprzątanie biur')
|
// a page with no SEO title still shows its name (e.g. 'Sprzątanie biur')
|
||||||
@@ -22,7 +22,7 @@ import { buildHreflangAlternates } from './hreflang.js';
|
|||||||
separator,
|
separator,
|
||||||
siteName
|
siteName
|
||||||
});
|
});
|
||||||
const description = meta?.description?.trim() || undefined;
|
const description = meta?.description?.trim() || siteDescription?.trim() || undefined;
|
||||||
const origin = baseUrl?.replace(/\/$/, '') ?? '';
|
const origin = baseUrl?.replace(/\/$/, '') ?? '';
|
||||||
const suffix = query ?? '';
|
const suffix = query ?? '';
|
||||||
const currentPath = buildLocalizedPath({
|
const currentPath = buildLocalizedPath({
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -4,6 +4,7 @@ export { buildArticleJsonLd } from './buildArticleJsonLd.js';
|
|||||||
export { buildBreadcrumbJsonLd } from './buildBreadcrumbJsonLd.js';
|
export { buildBreadcrumbJsonLd } from './buildBreadcrumbJsonLd.js';
|
||||||
export { buildFaqJsonLd } from './buildFaqJsonLd.js';
|
export { buildFaqJsonLd } from './buildFaqJsonLd.js';
|
||||||
export { buildIconsMetadata } from './buildIconsMetadata.js';
|
export { buildIconsMetadata } from './buildIconsMetadata.js';
|
||||||
|
export { buildLlmsTxt } from './buildLlmsTxt.js';
|
||||||
export { buildLocalBusinessJsonLd } from './buildLocalBusinessJsonLd.js';
|
export { buildLocalBusinessJsonLd } from './buildLocalBusinessJsonLd.js';
|
||||||
export { buildMetadata } from './buildMetadata.js';
|
export { buildMetadata } from './buildMetadata.js';
|
||||||
export type { PageMetadata } from './buildMetadata.js';
|
export type { PageMetadata } from './buildMetadata.js';
|
||||||
|
|||||||
Vendored
+1
@@ -3,6 +3,7 @@ export { buildArticleJsonLd } from './buildArticleJsonLd.js';
|
|||||||
export { buildBreadcrumbJsonLd } from './buildBreadcrumbJsonLd.js';
|
export { buildBreadcrumbJsonLd } from './buildBreadcrumbJsonLd.js';
|
||||||
export { buildFaqJsonLd } from './buildFaqJsonLd.js';
|
export { buildFaqJsonLd } from './buildFaqJsonLd.js';
|
||||||
export { buildIconsMetadata } from './buildIconsMetadata.js';
|
export { buildIconsMetadata } from './buildIconsMetadata.js';
|
||||||
|
export { buildLlmsTxt } from './buildLlmsTxt.js';
|
||||||
export { buildLocalBusinessJsonLd } from './buildLocalBusinessJsonLd.js';
|
export { buildLocalBusinessJsonLd } from './buildLocalBusinessJsonLd.js';
|
||||||
export { buildMetadata } from './buildMetadata.js';
|
export { buildMetadata } from './buildMetadata.js';
|
||||||
export { buildOrganizationJsonLd } from './buildOrganizationJsonLd.js';
|
export { buildOrganizationJsonLd } from './buildOrganizationJsonLd.js';
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"sources":["../../../src/modules/seo/index.ts"],"sourcesContent":["export { buildAutoFillMetaHook } from './autoFillMeta.js'\nexport type { AutoFillMapping } from './autoFillMeta.js'\nexport { buildArticleJsonLd } from './buildArticleJsonLd.js'\nexport { buildBreadcrumbJsonLd } from './buildBreadcrumbJsonLd.js'\nexport { buildFaqJsonLd } from './buildFaqJsonLd.js'\nexport { buildIconsMetadata } from './buildIconsMetadata.js'\nexport { buildLocalBusinessJsonLd } from './buildLocalBusinessJsonLd.js'\nexport { buildMetadata } from './buildMetadata.js'\nexport type { PageMetadata } from './buildMetadata.js'\nexport { buildOrganizationJsonLd } from './buildOrganizationJsonLd.js'\nexport { buildRobots } from './buildRobots.js'\nexport type { RobotsRules } from './buildRobots.js'\nexport { buildServiceJsonLd } from './buildServiceJsonLd.js'\nexport { buildSitemapEntries } from './buildSitemapEntries.js'\nexport type { SitemapEntry } from './buildSitemapEntries.js'\nexport { buildSiteNavigationJsonLd } from './buildSiteNavigationJsonLd.js'\nexport { buildWebSiteJsonLd } from './buildWebSiteJsonLd.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'\nexport { validateFaviconField } from './validateFavicon.js'\n"],"names":["buildAutoFillMetaHook","buildArticleJsonLd","buildBreadcrumbJsonLd","buildFaqJsonLd","buildIconsMetadata","buildLocalBusinessJsonLd","buildMetadata","buildOrganizationJsonLd","buildRobots","buildServiceJsonLd","buildSitemapEntries","buildSiteNavigationJsonLd","buildWebSiteJsonLd","composeTitle","createMetadataGenerator","createPageMetadata","buildHreflangAlternates","injectAutoFillMeta","injectSeoTabs","readSiteMetaConfig","buildSeoPlugin","slugsAcrossLocales","validateFaviconField"],"mappings":"AAAA,SAASA,qBAAqB,QAAQ,oBAAmB;AAEzD,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,qBAAqB,QAAQ,6BAA4B;AAClE,SAASC,cAAc,QAAQ,sBAAqB;AACpD,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,wBAAwB,QAAQ,gCAA+B;AACxE,SAASC,aAAa,QAAQ,qBAAoB;AAElD,SAASC,uBAAuB,QAAQ,+BAA8B;AACtE,SAASC,WAAW,QAAQ,mBAAkB;AAE9C,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,mBAAmB,QAAQ,2BAA0B;AAE9D,SAASC,yBAAyB,QAAQ,iCAAgC;AAC1E,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,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;AAE5D,SAASC,oBAAoB,QAAQ,uBAAsB"}
|
{"version":3,"sources":["../../../src/modules/seo/index.ts"],"sourcesContent":["export { buildAutoFillMetaHook } from './autoFillMeta.js'\nexport type { AutoFillMapping } from './autoFillMeta.js'\nexport { buildArticleJsonLd } from './buildArticleJsonLd.js'\nexport { buildBreadcrumbJsonLd } from './buildBreadcrumbJsonLd.js'\nexport { buildFaqJsonLd } from './buildFaqJsonLd.js'\nexport { buildIconsMetadata } from './buildIconsMetadata.js'\nexport { buildLlmsTxt } from './buildLlmsTxt.js'\nexport { buildLocalBusinessJsonLd } from './buildLocalBusinessJsonLd.js'\nexport { buildMetadata } from './buildMetadata.js'\nexport type { PageMetadata } from './buildMetadata.js'\nexport { buildOrganizationJsonLd } from './buildOrganizationJsonLd.js'\nexport { buildRobots } from './buildRobots.js'\nexport type { RobotsRules } from './buildRobots.js'\nexport { buildServiceJsonLd } from './buildServiceJsonLd.js'\nexport { buildSitemapEntries } from './buildSitemapEntries.js'\nexport type { SitemapEntry } from './buildSitemapEntries.js'\nexport { buildSiteNavigationJsonLd } from './buildSiteNavigationJsonLd.js'\nexport { buildWebSiteJsonLd } from './buildWebSiteJsonLd.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'\nexport { validateFaviconField } from './validateFavicon.js'\n"],"names":["buildAutoFillMetaHook","buildArticleJsonLd","buildBreadcrumbJsonLd","buildFaqJsonLd","buildIconsMetadata","buildLlmsTxt","buildLocalBusinessJsonLd","buildMetadata","buildOrganizationJsonLd","buildRobots","buildServiceJsonLd","buildSitemapEntries","buildSiteNavigationJsonLd","buildWebSiteJsonLd","composeTitle","createMetadataGenerator","createPageMetadata","buildHreflangAlternates","injectAutoFillMeta","injectSeoTabs","readSiteMetaConfig","buildSeoPlugin","slugsAcrossLocales","validateFaviconField"],"mappings":"AAAA,SAASA,qBAAqB,QAAQ,oBAAmB;AAEzD,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,qBAAqB,QAAQ,6BAA4B;AAClE,SAASC,cAAc,QAAQ,sBAAqB;AACpD,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,YAAY,QAAQ,oBAAmB;AAChD,SAASC,wBAAwB,QAAQ,gCAA+B;AACxE,SAASC,aAAa,QAAQ,qBAAoB;AAElD,SAASC,uBAAuB,QAAQ,+BAA8B;AACtE,SAASC,WAAW,QAAQ,mBAAkB;AAE9C,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,SAASC,mBAAmB,QAAQ,2BAA0B;AAE9D,SAASC,yBAAyB,QAAQ,iCAAgC;AAC1E,SAASC,kBAAkB,QAAQ,0BAAyB;AAC5D,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;AAE5D,SAASC,oBAAoB,QAAQ,uBAAsB"}
|
||||||
+8
-6
@@ -1,10 +1,6 @@
|
|||||||
import type { BasePayload } from 'payload';
|
import type { BasePayload } from 'payload';
|
||||||
import type { TitleOrder } from './composeTitle.js';
|
import type { TitleOrder } from './composeTitle.js';
|
||||||
export type SiteMetaConfig = {
|
export type SiteMetaConfig = {
|
||||||
siteName: string | null;
|
|
||||||
/** Padded separator, e.g. ' | ' — the panel stores the bare character. */
|
|
||||||
separator?: string;
|
|
||||||
order?: TitleOrder;
|
|
||||||
/**
|
/**
|
||||||
* Home slug per locale, e.g. { pl: 'strona-glowna', de: 'startseite' }. A map
|
* Home slug per locale, e.g. { pl: 'strona-glowna', de: 'startseite' }. A map
|
||||||
* (not a single string) so multilingual homepages collapse to root in EVERY
|
* (not a single string) so multilingual homepages collapse to root in EVERY
|
||||||
@@ -12,6 +8,12 @@ export type SiteMetaConfig = {
|
|||||||
* instead of '/de', breaking return tags (a real GSC error).
|
* instead of '/de', breaking return tags (a real GSC error).
|
||||||
*/
|
*/
|
||||||
homeSlug?: Record<string, string>;
|
homeSlug?: Record<string, string>;
|
||||||
|
order?: TitleOrder;
|
||||||
|
/** Padded separator, e.g. ' | ' — the panel stores the bare character. */
|
||||||
|
separator?: string;
|
||||||
|
/** Global fallback meta description. */
|
||||||
|
siteDescription?: null | string;
|
||||||
|
siteName: null | string;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Reads the site-wide inputs to title and URL composition from SiteSettings.
|
* Reads the site-wide inputs to title and URL composition from SiteSettings.
|
||||||
@@ -21,9 +23,9 @@ export type SiteMetaConfig = {
|
|||||||
* that the separator arrives unpadded, or that the home slug hides inside a
|
* that the separator arrives unpadded, or that the home slug hides inside a
|
||||||
* relationship — it should receive a resolved config.
|
* relationship — it should receive a resolved config.
|
||||||
*/
|
*/
|
||||||
export declare function readSiteMetaConfig({ payload, locale, settingsSlug, siteNameField, }: {
|
export declare function readSiteMetaConfig({ locale, payload, settingsSlug, siteNameField, }: {
|
||||||
payload: BasePayload;
|
|
||||||
locale: string;
|
locale: string;
|
||||||
|
payload: BasePayload;
|
||||||
settingsSlug?: string;
|
settingsSlug?: string;
|
||||||
siteNameField?: string;
|
siteNameField?: string;
|
||||||
}): Promise<SiteMetaConfig>;
|
}): Promise<SiteMetaConfig>;
|
||||||
|
|||||||
+15
-10
@@ -5,13 +5,13 @@
|
|||||||
* whatever an editor set in the panel. Metadata generation shouldn't also know
|
* 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
|
* that the separator arrives unpadded, or that the home slug hides inside a
|
||||||
* relationship — it should receive a resolved config.
|
* relationship — it should receive a resolved config.
|
||||||
*/ export async function readSiteMetaConfig({ payload, locale, settingsSlug = 'site-settings', siteNameField = 'siteName' }) {
|
*/ export async function readSiteMetaConfig({ locale, payload, settingsSlug = 'site-settings', siteNameField = 'siteName' }) {
|
||||||
// depth 1 populates the homepage relationship, so its slug is available
|
// depth 1 populates the homepage relationship, so its slug is available
|
||||||
// without a second query.
|
// without a second query.
|
||||||
const settings = await payload.findGlobal({
|
const settings = await payload.findGlobal({
|
||||||
slug: settingsSlug,
|
slug: settingsSlug,
|
||||||
locale: locale,
|
depth: 1,
|
||||||
depth: 1
|
locale: locale
|
||||||
});
|
});
|
||||||
const homepage = settings.homepage && typeof settings.homepage === 'object' ? settings.homepage : null;
|
const homepage = settings.homepage && typeof settings.homepage === 'object' ? settings.homepage : null;
|
||||||
// Build the per-locale home slug map. The homepage's slug is localized, so a
|
// Build the per-locale home slug map. The homepage's slug is localized, so a
|
||||||
@@ -22,18 +22,22 @@
|
|||||||
if (homepage?.id) {
|
if (homepage?.id) {
|
||||||
try {
|
try {
|
||||||
const homeDoc = await payload.findByID({
|
const homeDoc = await payload.findByID({
|
||||||
collection: 'pages',
|
|
||||||
id: homepage.id,
|
id: homepage.id,
|
||||||
locale: 'all',
|
collection: 'pages',
|
||||||
depth: 0
|
depth: 0,
|
||||||
|
locale: 'all'
|
||||||
});
|
});
|
||||||
const slugField = homeDoc.slug;
|
const slugField = homeDoc.slug;
|
||||||
if (slugField && typeof slugField === 'object') {
|
if (slugField && typeof slugField === 'object') {
|
||||||
const map = {};
|
const map = {};
|
||||||
for (const [loc, value] of Object.entries(slugField)){
|
for (const [loc, value] of Object.entries(slugField)){
|
||||||
if (typeof value === 'string') map[loc] = value;
|
if (typeof value === 'string') {
|
||||||
|
map[loc] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Object.keys(map).length > 0) {
|
||||||
|
homeSlug = map;
|
||||||
}
|
}
|
||||||
if (Object.keys(map).length > 0) homeSlug = map;
|
|
||||||
} else if (typeof slugField === 'string') {
|
} else if (typeof slugField === 'string') {
|
||||||
// Non-localized slug (same in every language) — key it by current locale;
|
// Non-localized slug (same in every language) — key it by current locale;
|
||||||
// buildLocalizedPath tolerates a partial map. Non-localized homepage
|
// buildLocalizedPath tolerates a partial map. Non-localized homepage
|
||||||
@@ -54,11 +58,12 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
siteDescription: settings.siteDescription ?? null,
|
||||||
siteName: settings[siteNameField] ?? null,
|
siteName: settings[siteNameField] ?? null,
|
||||||
// The panel stores '|'; titles need it padded.
|
// The panel stores '|'; titles need it padded.
|
||||||
separator: settings.titleSeparator ? ` ${settings.titleSeparator} ` : undefined,
|
homeSlug,
|
||||||
order: settings.titleOrder ?? undefined,
|
order: settings.titleOrder ?? undefined,
|
||||||
homeSlug
|
separator: settings.titleSeparator ? ` ${settings.titleSeparator} ` : undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+44
-1
@@ -841,4 +841,47 @@ const jsonLd = buildArticleJsonLd({
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
Dane z dokumentu/panelu. Google wymaga headline + dat dla rich result.
|
Dane z dokumentu/panelu. Google wymaga headline + dat dla rich result.
|
||||||
|
|
||||||
|
## Fallback meta description (siteDescription)
|
||||||
|
|
||||||
|
Strona bez `meta.description` → plugin używa globalnego `siteDescription` z
|
||||||
|
SiteSettings. Lepsze niż brak description (Google losowo wyciąga tekst ze strony).
|
||||||
|
|
||||||
|
- Pole **siteDescription** w SiteSettings → General (localized, textarea)
|
||||||
|
- buildMetadata: `meta.description || siteDescription`
|
||||||
|
- Redaktor wypełnia raz globalnie; strony bez własnego opisu dziedziczą
|
||||||
|
|
||||||
|
## llms.txt — opis strony dla agentów AI (GEO)
|
||||||
|
|
||||||
|
`buildLlmsTxt` generuje `/llms.txt` (standard llmstxt.org) — plik Markdown
|
||||||
|
opisujący stronę dla agentów AI / crawlerów LLM. Na wzór buildRobots/sitemap:
|
||||||
|
plugin zna nazwę, opis i strony, więc generuje automatycznie.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// app/llms.txt/route.ts
|
||||||
|
import { buildLlmsTxt } from '@intecion/ipal-kit'
|
||||||
|
import { getCachedPayload } from '@/lib/content'
|
||||||
|
import { i18nConfig } from '@/i18n.config'
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const body = await buildLlmsTxt({
|
||||||
|
payload: await getCachedPayload(),
|
||||||
|
config: i18nConfig,
|
||||||
|
baseUrl: process.env.NEXT_PUBLIC_SERVER_URL!,
|
||||||
|
})
|
||||||
|
return new Response(body, {
|
||||||
|
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Struktura: H1 (nazwa firmy), blockquote (siteDescription), lista stron jako
|
||||||
|
linki Markdown z opisami. Dane z panelu (siteName, siteDescription, strony),
|
||||||
|
pomija drafty i noindex. Poprawia widoczność w wyszukiwaniach AI (GEO —
|
||||||
|
Generative Engine Optimization) i audytach „Agentic Browsing”.
|
||||||
|
|
||||||
|
Wymaga wypełnionego siteDescription i sensownych meta.description stron
|
||||||
|
(inaczej llms.txt będzie ubogi).
|
||||||
@@ -4,7 +4,7 @@ import { validateFaviconField } from '../../../modules/seo/index.js'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* General site identity fields.
|
* General site identity fields.
|
||||||
* Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo, favicon).
|
* Consumed by SEO/OG (siteName, defaultShareImage) and frontend (logo).
|
||||||
*/
|
*/
|
||||||
export const generalFields: Field[] = [
|
export const generalFields: Field[] = [
|
||||||
{
|
{
|
||||||
@@ -16,6 +16,15 @@ export const generalFields: Field[] = [
|
|||||||
localized: true,
|
localized: true,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'siteDescription',
|
||||||
|
type: 'textarea',
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
'Fallback meta description when a page has none. Also used for llms.txt and Open Graph.',
|
||||||
|
},
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'titleOrder',
|
name: 'titleOrder',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
@@ -47,8 +56,7 @@ export const generalFields: Field[] = [
|
|||||||
name: 'logo',
|
name: 'logo',
|
||||||
type: 'upload',
|
type: 'upload',
|
||||||
admin: {
|
admin: {
|
||||||
description:
|
description: 'Primary site logo.',
|
||||||
'Primary site logo. Also used for Organization structured data (buildOrganizationJsonLd).',
|
|
||||||
},
|
},
|
||||||
relationTo: 'media',
|
relationTo: 'media',
|
||||||
},
|
},
|
||||||
@@ -68,8 +76,7 @@ export const generalFields: Field[] = [
|
|||||||
'Square icon, PNG or SVG, min. 48×48px (Google requires this to show it in search). Rendered via buildIconsMetadata.',
|
'Square icon, PNG or SVG, min. 48×48px (Google requires this to show it in search). Rendered via buildIconsMetadata.',
|
||||||
},
|
},
|
||||||
hooks: {
|
hooks: {
|
||||||
// Warns the editor at save time if the favicon is too small (<48×48) or
|
// Warn the editor if the favicon is too small / not square for Google.
|
||||||
// not square — Google won't display such favicons in search results.
|
|
||||||
beforeValidate: [validateFaviconField],
|
beforeValidate: [validateFaviconField],
|
||||||
},
|
},
|
||||||
relationTo: 'media',
|
relationTo: 'media',
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ export {
|
|||||||
buildArticleJsonLd,
|
buildArticleJsonLd,
|
||||||
buildFaqJsonLd,
|
buildFaqJsonLd,
|
||||||
buildIconsMetadata,
|
buildIconsMetadata,
|
||||||
|
buildLlmsTxt,
|
||||||
buildLocalBusinessJsonLd,
|
buildLocalBusinessJsonLd,
|
||||||
buildOrganizationJsonLd,
|
buildOrganizationJsonLd,
|
||||||
buildServiceJsonLd,
|
buildServiceJsonLd,
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import type { BasePayload } from 'payload'
|
||||||
|
|
||||||
|
import type { I18nConfig } from '../i18n/index.js'
|
||||||
|
|
||||||
|
import { buildLocalizedPath } from '../i18n/index.js'
|
||||||
|
|
||||||
|
type BuildLlmsTxtArgs = {
|
||||||
|
/** Absolute site URL (https://…). */
|
||||||
|
baseUrl: string
|
||||||
|
config: I18nConfig
|
||||||
|
/** Locale for names/descriptions. Defaults to config.defaultLocale. */
|
||||||
|
locale?: string
|
||||||
|
/** Pages collection slug. Defaults to 'pages'. */
|
||||||
|
pagesSlug?: string
|
||||||
|
payload: BasePayload
|
||||||
|
/** Site settings global slug. Defaults to 'site-settings'. */
|
||||||
|
settingsSlug?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SettingsShape = {
|
||||||
|
siteDescription?: null | string
|
||||||
|
siteName?: null | string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PageRow = {
|
||||||
|
_status?: string
|
||||||
|
id: number | string
|
||||||
|
meta?: { description?: unknown; noindex?: boolean } | null
|
||||||
|
slug?: unknown
|
||||||
|
title?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the body of /llms.txt — a Markdown file describing the site for AI
|
||||||
|
* agents / LLM crawlers, per the llmstxt.org convention. Mirrors buildRobots /
|
||||||
|
* buildSitemapEntries: the plugin already knows the site's name, description,
|
||||||
|
* and pages, so it can generate this automatically.
|
||||||
|
*
|
||||||
|
* Structure (llmstxt.org): H1 site name, a blockquote/summary, then a list of
|
||||||
|
* key pages as Markdown links with short descriptions. Agents read this to
|
||||||
|
* understand the site quickly without crawling everything.
|
||||||
|
*
|
||||||
|
* Wire it as a route that returns text/plain:
|
||||||
|
*
|
||||||
|
* // app/llms.txt/route.ts
|
||||||
|
* import { buildLlmsTxt } from '@intecion/ipal-kit'
|
||||||
|
* import { getCachedPayload } from '@/lib/content'
|
||||||
|
* import { i18nConfig } from '@/i18n.config'
|
||||||
|
* export const dynamic = 'force-dynamic'
|
||||||
|
* export async function GET() {
|
||||||
|
* const body = await buildLlmsTxt({
|
||||||
|
* payload: await getCachedPayload(),
|
||||||
|
* config: i18nConfig,
|
||||||
|
* baseUrl: process.env.NEXT_PUBLIC_SERVER_URL!,
|
||||||
|
* })
|
||||||
|
* return new Response(body, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } })
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Data comes from the panel (siteName, siteDescription, pages) — nothing
|
||||||
|
* hardcoded. Skips drafts and noindex pages (same as the sitemap).
|
||||||
|
*/
|
||||||
|
export async function buildLlmsTxt({
|
||||||
|
baseUrl,
|
||||||
|
config,
|
||||||
|
locale,
|
||||||
|
pagesSlug = 'pages',
|
||||||
|
payload,
|
||||||
|
settingsSlug = 'site-settings',
|
||||||
|
}: BuildLlmsTxtArgs): Promise<string> {
|
||||||
|
const loc = locale ?? config.defaultLocale
|
||||||
|
const origin = baseUrl.replace(/\/$/, '')
|
||||||
|
|
||||||
|
const settings = (await payload.findGlobal({
|
||||||
|
slug: settingsSlug as never,
|
||||||
|
depth: 0,
|
||||||
|
locale: loc as never,
|
||||||
|
})) as SettingsShape
|
||||||
|
|
||||||
|
const name = settings.siteName?.trim() || 'Website'
|
||||||
|
const description = settings.siteDescription?.trim()
|
||||||
|
|
||||||
|
const result = await payload.find({
|
||||||
|
collection: pagesSlug as never,
|
||||||
|
depth: 0,
|
||||||
|
limit: 1000,
|
||||||
|
locale: loc as never,
|
||||||
|
where: { _status: { not_equals: 'draft' } } as never,
|
||||||
|
})
|
||||||
|
|
||||||
|
const lines: string[] = [`# ${name}`, '']
|
||||||
|
if (description) {
|
||||||
|
lines.push(`> ${description}`, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageLinks: string[] = []
|
||||||
|
for (const raw of result.docs as PageRow[]) {
|
||||||
|
if (raw._status && raw._status !== 'published') {continue}
|
||||||
|
if (raw.meta?.noindex) {continue}
|
||||||
|
|
||||||
|
const title = typeof raw.title === 'string' ? raw.title : undefined
|
||||||
|
const slug = typeof raw.slug === 'string' ? raw.slug : undefined
|
||||||
|
if (!title || !slug) {continue}
|
||||||
|
|
||||||
|
const path = buildLocalizedPath({ config, locale: loc, slugs: { [loc]: slug } })
|
||||||
|
if (!path) {continue}
|
||||||
|
|
||||||
|
const pageDesc = typeof raw.meta?.description === 'string' ? ` — ${raw.meta.description}` : ''
|
||||||
|
pageLinks.push(`- [${title}](${origin}${path})${pageDesc}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageLinks.length > 0) {
|
||||||
|
lines.push('## Strony', '', ...pageLinks, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@ type BuildMetadataArgs = {
|
|||||||
baseUrl?: string
|
baseUrl?: string
|
||||||
config: I18nConfig
|
config: I18nConfig
|
||||||
/** Home slug that collapses to the locale root. Defaults to 'home'. */
|
/** Home slug that collapses to the locale root. Defaults to 'home'. */
|
||||||
homeSlug?: string | Record<string, string>
|
homeSlug?: Record<string, string> | string
|
||||||
/** Resolved OG image URL (page image or site defaultShareImage). */
|
/** Resolved OG image URL (page image or site defaultShareImage). */
|
||||||
imageUrl?: null | string
|
imageUrl?: null | string
|
||||||
/** Current locale being rendered. */
|
/** Current locale being rendered. */
|
||||||
@@ -67,6 +67,12 @@ type BuildMetadataArgs = {
|
|||||||
query?: string
|
query?: string
|
||||||
/** Separator between page title and site name. Defaults to ' | '. */
|
/** Separator between page title and site name. Defaults to ' | '. */
|
||||||
separator?: string
|
separator?: string
|
||||||
|
/**
|
||||||
|
* Global fallback description (from SiteSettings.siteDescription). Used when a
|
||||||
|
* page has no meta.description — better than emitting no description at all
|
||||||
|
* (Google would then scrape random on-page text for the snippet).
|
||||||
|
*/
|
||||||
|
siteDescription?: null | string
|
||||||
/** Site name for title composition and OG. */
|
/** Site name for title composition and OG. */
|
||||||
siteName?: null | string
|
siteName?: null | string
|
||||||
/** slug per locale for this document — drives canonical + hreflang. */
|
/** slug per locale for this document — drives canonical + hreflang. */
|
||||||
@@ -94,6 +100,7 @@ export function buildMetadata({
|
|||||||
prefix,
|
prefix,
|
||||||
query,
|
query,
|
||||||
separator,
|
separator,
|
||||||
|
siteDescription,
|
||||||
siteName,
|
siteName,
|
||||||
slugs,
|
slugs,
|
||||||
}: BuildMetadataArgs): PageMetadata {
|
}: BuildMetadataArgs): PageMetadata {
|
||||||
@@ -105,7 +112,7 @@ export function buildMetadata({
|
|||||||
const resolvedPageTitle = meta?.title?.trim() || pageTitle?.trim() || undefined
|
const resolvedPageTitle = meta?.title?.trim() || pageTitle?.trim() || undefined
|
||||||
const title =
|
const title =
|
||||||
override || composeTitle({ order, pageTitle: resolvedPageTitle, separator, siteName })
|
override || composeTitle({ order, pageTitle: resolvedPageTitle, separator, siteName })
|
||||||
const description = meta?.description?.trim() || undefined
|
const description = meta?.description?.trim() || siteDescription?.trim() || undefined
|
||||||
const origin = baseUrl?.replace(/\/$/, '') ?? ''
|
const origin = baseUrl?.replace(/\/$/, '') ?? ''
|
||||||
|
|
||||||
const suffix = query ?? ''
|
const suffix = query ?? ''
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export { buildArticleJsonLd } from './buildArticleJsonLd.js'
|
|||||||
export { buildBreadcrumbJsonLd } from './buildBreadcrumbJsonLd.js'
|
export { buildBreadcrumbJsonLd } from './buildBreadcrumbJsonLd.js'
|
||||||
export { buildFaqJsonLd } from './buildFaqJsonLd.js'
|
export { buildFaqJsonLd } from './buildFaqJsonLd.js'
|
||||||
export { buildIconsMetadata } from './buildIconsMetadata.js'
|
export { buildIconsMetadata } from './buildIconsMetadata.js'
|
||||||
|
export { buildLlmsTxt } from './buildLlmsTxt.js'
|
||||||
export { buildLocalBusinessJsonLd } from './buildLocalBusinessJsonLd.js'
|
export { buildLocalBusinessJsonLd } from './buildLocalBusinessJsonLd.js'
|
||||||
export { buildMetadata } from './buildMetadata.js'
|
export { buildMetadata } from './buildMetadata.js'
|
||||||
export type { PageMetadata } from './buildMetadata.js'
|
export type { PageMetadata } from './buildMetadata.js'
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
import type { BasePayload } from 'payload'
|
import type { BasePayload } from 'payload'
|
||||||
|
|
||||||
import type { TitleOrder } from './composeTitle.js'
|
import type { TitleOrder } from './composeTitle.js'
|
||||||
|
|
||||||
type SettingsShape = {
|
type SettingsShape = {
|
||||||
homepage?: { id: string | number; slug?: unknown } | string | number | null
|
|
||||||
titleSeparator?: string | null
|
|
||||||
titleOrder?: TitleOrder | null
|
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
|
homepage?: { id: number | string; slug?: unknown } | null | number | string
|
||||||
|
titleOrder?: null | TitleOrder
|
||||||
|
titleSeparator?: null | string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SiteMetaConfig = {
|
export type SiteMetaConfig = {
|
||||||
siteName: string | null
|
|
||||||
/** Padded separator, e.g. ' | ' — the panel stores the bare character. */
|
|
||||||
separator?: string
|
|
||||||
order?: TitleOrder
|
|
||||||
/**
|
/**
|
||||||
* Home slug per locale, e.g. { pl: 'strona-glowna', de: 'startseite' }. A map
|
* Home slug per locale, e.g. { pl: 'strona-glowna', de: 'startseite' }. A map
|
||||||
* (not a single string) so multilingual homepages collapse to root in EVERY
|
* (not a single string) so multilingual homepages collapse to root in EVERY
|
||||||
@@ -20,6 +17,12 @@ export type SiteMetaConfig = {
|
|||||||
* instead of '/de', breaking return tags (a real GSC error).
|
* instead of '/de', breaking return tags (a real GSC error).
|
||||||
*/
|
*/
|
||||||
homeSlug?: Record<string, string>
|
homeSlug?: Record<string, string>
|
||||||
|
order?: TitleOrder
|
||||||
|
/** Padded separator, e.g. ' | ' — the panel stores the bare character. */
|
||||||
|
separator?: string
|
||||||
|
/** Global fallback meta description. */
|
||||||
|
siteDescription?: null | string
|
||||||
|
siteName: null | string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,13 +34,13 @@ export type SiteMetaConfig = {
|
|||||||
* relationship — it should receive a resolved config.
|
* relationship — it should receive a resolved config.
|
||||||
*/
|
*/
|
||||||
export async function readSiteMetaConfig({
|
export async function readSiteMetaConfig({
|
||||||
payload,
|
locale,
|
||||||
locale,
|
payload,
|
||||||
settingsSlug = 'site-settings',
|
settingsSlug = 'site-settings',
|
||||||
siteNameField = 'siteName',
|
siteNameField = 'siteName',
|
||||||
}: {
|
}: {
|
||||||
payload: BasePayload
|
|
||||||
locale: string
|
locale: string
|
||||||
|
payload: BasePayload
|
||||||
settingsSlug?: string
|
settingsSlug?: string
|
||||||
siteNameField?: string
|
siteNameField?: string
|
||||||
}): Promise<SiteMetaConfig> {
|
}): Promise<SiteMetaConfig> {
|
||||||
@@ -45,8 +48,8 @@ export async function readSiteMetaConfig({
|
|||||||
// without a second query.
|
// without a second query.
|
||||||
const settings = (await payload.findGlobal({
|
const settings = (await payload.findGlobal({
|
||||||
slug: settingsSlug as never,
|
slug: settingsSlug as never,
|
||||||
locale: locale as never,
|
|
||||||
depth: 1,
|
depth: 1,
|
||||||
|
locale: locale as never,
|
||||||
})) as SettingsShape
|
})) as SettingsShape
|
||||||
|
|
||||||
const homepage =
|
const homepage =
|
||||||
@@ -60,18 +63,18 @@ export async function readSiteMetaConfig({
|
|||||||
if (homepage?.id) {
|
if (homepage?.id) {
|
||||||
try {
|
try {
|
||||||
const homeDoc = (await payload.findByID({
|
const homeDoc = (await payload.findByID({
|
||||||
collection: 'pages' as never,
|
|
||||||
id: homepage.id,
|
id: homepage.id,
|
||||||
locale: 'all' as never,
|
collection: 'pages' as never,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
|
locale: 'all' as never,
|
||||||
})) as { slug?: unknown }
|
})) as { slug?: unknown }
|
||||||
const slugField = homeDoc.slug
|
const slugField = homeDoc.slug
|
||||||
if (slugField && typeof slugField === 'object') {
|
if (slugField && typeof slugField === 'object') {
|
||||||
const map: Record<string, string> = {}
|
const map: Record<string, string> = {}
|
||||||
for (const [loc, value] of Object.entries(slugField as Record<string, unknown>)) {
|
for (const [loc, value] of Object.entries(slugField as Record<string, unknown>)) {
|
||||||
if (typeof value === 'string') map[loc] = value
|
if (typeof value === 'string') {map[loc] = value}
|
||||||
}
|
}
|
||||||
if (Object.keys(map).length > 0) homeSlug = map
|
if (Object.keys(map).length > 0) {homeSlug = map}
|
||||||
} else if (typeof slugField === 'string') {
|
} else if (typeof slugField === 'string') {
|
||||||
// Non-localized slug (same in every language) — key it by current locale;
|
// Non-localized slug (same in every language) — key it by current locale;
|
||||||
// buildLocalizedPath tolerates a partial map. Non-localized homepage
|
// buildLocalizedPath tolerates a partial map. Non-localized homepage
|
||||||
@@ -90,10 +93,11 @@ export async function readSiteMetaConfig({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
siteDescription: (settings.siteDescription as string | undefined) ?? null,
|
||||||
siteName: (settings[siteNameField] as string | undefined) ?? null,
|
siteName: (settings[siteNameField] as string | undefined) ?? null,
|
||||||
// The panel stores '|'; titles need it padded.
|
// The panel stores '|'; titles need it padded.
|
||||||
separator: settings.titleSeparator ? ` ${settings.titleSeparator} ` : undefined,
|
|
||||||
order: settings.titleOrder ?? undefined,
|
|
||||||
homeSlug,
|
homeSlug,
|
||||||
|
order: settings.titleOrder ?? undefined,
|
||||||
|
separator: settings.titleSeparator ? ` ${settings.titleSeparator} ` : undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user