Compare commits
2
Commits
e2a703fc72
...
4a46651839
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a46651839 | ||
|
|
e7f06548fb |
+27
-13
@@ -1,14 +1,25 @@
|
|||||||
import type { SitemapEntry } from './buildSitemapEntries.js';
|
import type { SitemapEntry } from './buildSitemapEntries.js';
|
||||||
|
type BuildSitemapXmlOptions = {
|
||||||
|
/**
|
||||||
|
* URL of a CSS stylesheet to make the sitemap readable in the browser, e.g.
|
||||||
|
* '/sitemap.css'. Uses `type="text/css"` — the W3C "Associating Style Sheets
|
||||||
|
* with XML" mechanism, which is NOT deprecated (unlike XSLT / type="text/xsl",
|
||||||
|
* which Chrome/WebKit are removing). CSS on XML shows no warning, styles the
|
||||||
|
* raw tags directly (e.g. `url { display: block }` turns the wall of text into
|
||||||
|
* cards), and crawlers ignore the directive entirely.
|
||||||
|
*/
|
||||||
|
cssUrl?: string;
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* Serializes sitemap entries to a clean, indented XML STRING — valid for crawlers
|
* Serializes sitemap entries to a clean, indented XML STRING — valid for crawlers
|
||||||
* and readable in the browser as the native XML tree (indented, collapsible,
|
* and (with `cssUrl`) styled in the browser via plain CSS.
|
||||||
* syntax-highlighted by the browser itself).
|
|
||||||
*
|
*
|
||||||
* NO XSLT stylesheet: browsers (Chrome et al.) are REMOVING XSLT support, so a
|
* Two viewing modes:
|
||||||
* <?xml-stylesheet?> approach is a dead end — it shows a deprecation warning now
|
* - No cssUrl → the browser's native formatted XML tree (indented, collapsible).
|
||||||
* and will break soon. Instead we serve plain XML with the correct Content-Type;
|
* - With cssUrl → a `<?xml-stylesheet type="text/css">` directive; the project's
|
||||||
* the browser renders its built-in formatted XML view (the "code tree" look) with
|
* CSS styles the XML tags (cards, labels via ::before). NOT XSLT — that's being
|
||||||
* no transformation needed.
|
* removed from browsers and shows a deprecation warning. CSS is safe and
|
||||||
|
* W3C-standard.
|
||||||
*
|
*
|
||||||
* // app/sitemap.xml/route.ts
|
* // app/sitemap.xml/route.ts
|
||||||
* import { buildSitemapXml } from '@intecion/ipal-kit'
|
* import { buildSitemapXml } from '@intecion/ipal-kit'
|
||||||
@@ -16,16 +27,19 @@ import type { SitemapEntry } from './buildSitemapEntries.js';
|
|||||||
* export const dynamic = 'force-dynamic'
|
* export const dynamic = 'force-dynamic'
|
||||||
* export async function GET() {
|
* export async function GET() {
|
||||||
* const entries = await sitemap()
|
* const entries = await sitemap()
|
||||||
* const xml = buildSitemapXml(entries)
|
* const xml = buildSitemapXml(entries, { cssUrl: '/sitemap.css' })
|
||||||
* return new Response(xml, {
|
* return new Response(xml, {
|
||||||
* headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
* headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
||||||
* })
|
* })
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* The indentation makes the raw XML pleasant to read; the browser's default XML
|
* Put sitemap.css in the project's /public and style the tags (see docs/seo.md
|
||||||
* viewer adds the tree/fold UI. Crawlers read the XML as usual.
|
* for a starter). Note: <loc> is an XML tag, not <a href> — CSS can't make it a
|
||||||
|
* clickable link (some browsers auto-detect URLs); the win is readability, not
|
||||||
|
* clickability.
|
||||||
*
|
*
|
||||||
* NOTE: if you use this custom route, DON'T also keep app/sitemap.ts — pick one
|
* NOTE: if you use this custom route, DON'T also keep app/sitemap.ts — pick one.
|
||||||
* (this route OR the Next MetadataRoute). Two sitemaps confuse crawlers.
|
* Two sitemaps confuse crawlers.
|
||||||
*/
|
*/
|
||||||
export declare function buildSitemapXml(entries: SitemapEntry[]): string;
|
export declare function buildSitemapXml(entries: SitemapEntry[], opts?: BuildSitemapXmlOptions): string;
|
||||||
|
export {};
|
||||||
|
|||||||
Vendored
+20
-20
@@ -1,13 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* Serializes sitemap entries to a clean, indented XML STRING — valid for crawlers
|
* Serializes sitemap entries to a clean, indented XML STRING — valid for crawlers
|
||||||
* and readable in the browser as the native XML tree (indented, collapsible,
|
* and (with `cssUrl`) styled in the browser via plain CSS.
|
||||||
* syntax-highlighted by the browser itself).
|
|
||||||
*
|
*
|
||||||
* NO XSLT stylesheet: browsers (Chrome et al.) are REMOVING XSLT support, so a
|
* Two viewing modes:
|
||||||
* <?xml-stylesheet?> approach is a dead end — it shows a deprecation warning now
|
* - No cssUrl → the browser's native formatted XML tree (indented, collapsible).
|
||||||
* and will break soon. Instead we serve plain XML with the correct Content-Type;
|
* - With cssUrl → a `<?xml-stylesheet type="text/css">` directive; the project's
|
||||||
* the browser renders its built-in formatted XML view (the "code tree" look) with
|
* CSS styles the XML tags (cards, labels via ::before). NOT XSLT — that's being
|
||||||
* no transformation needed.
|
* removed from browsers and shows a deprecation warning. CSS is safe and
|
||||||
|
* W3C-standard.
|
||||||
*
|
*
|
||||||
* // app/sitemap.xml/route.ts
|
* // app/sitemap.xml/route.ts
|
||||||
* import { buildSitemapXml } from '@intecion/ipal-kit'
|
* import { buildSitemapXml } from '@intecion/ipal-kit'
|
||||||
@@ -15,18 +15,21 @@
|
|||||||
* export const dynamic = 'force-dynamic'
|
* export const dynamic = 'force-dynamic'
|
||||||
* export async function GET() {
|
* export async function GET() {
|
||||||
* const entries = await sitemap()
|
* const entries = await sitemap()
|
||||||
* const xml = buildSitemapXml(entries)
|
* const xml = buildSitemapXml(entries, { cssUrl: '/sitemap.css' })
|
||||||
* return new Response(xml, {
|
* return new Response(xml, {
|
||||||
* headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
* headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
||||||
* })
|
* })
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* The indentation makes the raw XML pleasant to read; the browser's default XML
|
* Put sitemap.css in the project's /public and style the tags (see docs/seo.md
|
||||||
* viewer adds the tree/fold UI. Crawlers read the XML as usual.
|
* for a starter). Note: <loc> is an XML tag, not <a href> — CSS can't make it a
|
||||||
|
* clickable link (some browsers auto-detect URLs); the win is readability, not
|
||||||
|
* clickability.
|
||||||
*
|
*
|
||||||
* NOTE: if you use this custom route, DON'T also keep app/sitemap.ts — pick one
|
* NOTE: if you use this custom route, DON'T also keep app/sitemap.ts — pick one.
|
||||||
* (this route OR the Next MetadataRoute). Two sitemaps confuse crawlers.
|
* Two sitemaps confuse crawlers.
|
||||||
*/ export function buildSitemapXml(entries) {
|
*/ export function buildSitemapXml(entries, opts = {}) {
|
||||||
|
const { cssUrl } = opts;
|
||||||
const esc = (s)=>s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
const esc = (s)=>s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||||
const urls = entries.map((e)=>{
|
const urls = entries.map((e)=>{
|
||||||
const parts = [
|
const parts = [
|
||||||
@@ -36,12 +39,8 @@
|
|||||||
const iso = e.lastModified instanceof Date ? e.lastModified.toISOString() : String(e.lastModified);
|
const iso = e.lastModified instanceof Date ? e.lastModified.toISOString() : String(e.lastModified);
|
||||||
parts.push(` <lastmod>${esc(iso)}</lastmod>`);
|
parts.push(` <lastmod>${esc(iso)}</lastmod>`);
|
||||||
}
|
}
|
||||||
if (e.changeFrequency) {
|
if (e.changeFrequency) parts.push(` <changefreq>${e.changeFrequency}</changefreq>`);
|
||||||
parts.push(` <changefreq>${e.changeFrequency}</changefreq>`);
|
if (typeof e.priority === 'number') parts.push(` <priority>${e.priority}</priority>`);
|
||||||
}
|
|
||||||
if (typeof e.priority === 'number') {
|
|
||||||
parts.push(` <priority>${e.priority}</priority>`);
|
|
||||||
}
|
|
||||||
const alternates = e.alternates?.languages;
|
const alternates = e.alternates?.languages;
|
||||||
if (alternates) {
|
if (alternates) {
|
||||||
for (const [lang, href] of Object.entries(alternates)){
|
for (const [lang, href] of Object.entries(alternates)){
|
||||||
@@ -52,7 +51,8 @@
|
|||||||
}
|
}
|
||||||
return ` <url>\n${parts.join('\n')}\n </url>`;
|
return ` <url>\n${parts.join('\n')}\n </url>`;
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
return `<?xml version="1.0" encoding="UTF-8"?>\n` + `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" ` + `xmlns:xhtml="http://www.w3.org/1999/xhtml">\n` + urls + `\n</urlset>`;
|
const stylesheet = cssUrl ? `<?xml-stylesheet type="text/css" href="${esc(cssUrl)}"?>\n` : '';
|
||||||
|
return `<?xml version="1.0" encoding="UTF-8"?>\n` + stylesheet + `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" ` + `xmlns:xhtml="http://www.w3.org/1999/xhtml">\n` + urls + `\n</urlset>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
//# sourceMappingURL=buildSitemapXml.js.map
|
//# sourceMappingURL=buildSitemapXml.js.map
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
+174
-123
@@ -19,14 +19,14 @@ admina i bez importMap się nie wyrenderują.
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
ipalKit({
|
ipalKit({
|
||||||
seo: {
|
seo: {
|
||||||
collections: ['pages', 'posts'], // które kolekcje dostają meta
|
collections: ['pages', 'posts'], // które kolekcje dostają meta
|
||||||
// generateTitle: ({ doc }) => `${doc.title}`, // opcjonalne
|
// generateTitle: ({ doc }) => `${doc.title}`, // opcjonalne
|
||||||
// generateDescription: ({ doc }) => doc.excerpt ?? '',
|
// generateDescription: ({ doc }) => doc.excerpt ?? '',
|
||||||
// fields: [...], // extra pola w grupie SEO
|
// fields: [...], // extra pola w grupie SEO
|
||||||
// autoFill: { title: 'title', description: 'excerpt' }, // mapowanie auto-fill
|
// autoFill: { title: 'title', description: 'excerpt' }, // mapowanie auto-fill
|
||||||
// autoFill: false, // wyłącz auto-fill
|
// autoFill: false, // wyłącz auto-fill
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -89,16 +89,16 @@ import { createPageMetadata } from '@intecion/ipal-kit'
|
|||||||
import { i18nConfig } from '@/i18n.config'
|
import { i18nConfig } from '@/i18n.config'
|
||||||
|
|
||||||
const pageMetadata = createPageMetadata({
|
const pageMetadata = createPageMetadata({
|
||||||
config: i18nConfig,
|
config: i18nConfig,
|
||||||
baseUrl: process.env.NEXT_PUBLIC_SERVER_URL,
|
baseUrl: process.env.NEXT_PUBLIC_SERVER_URL,
|
||||||
// collection: 'pages', // domyślne
|
// collection: 'pages', // domyślne
|
||||||
// settingsSlug: 'site-settings', // domyślne
|
// settingsSlug: 'site-settings', // domyślne
|
||||||
// siteNameField: 'siteName', // domyślne
|
// siteNameField: 'siteName', // domyślne
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function generateMetadata({ params }): Promise<Metadata> {
|
export async function generateMetadata({ params }): Promise<Metadata> {
|
||||||
const { locale, slug } = await params
|
const { locale, slug } = await params
|
||||||
return pageMetadata({ payload: await getPayload({ config }), locale, slug })
|
return pageMetadata({ payload: await getPayload({ config }), locale, slug })
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -121,43 +121,43 @@ global. Klient dostarcza resolvery.
|
|||||||
import { createMetadataGenerator } from '@intecion/ipal-kit'
|
import { createMetadataGenerator } from '@intecion/ipal-kit'
|
||||||
|
|
||||||
const generate = createMetadataGenerator({
|
const generate = createMetadataGenerator({
|
||||||
config: i18nConfig,
|
config: i18nConfig,
|
||||||
baseUrl: process.env.NEXT_PUBLIC_SERVER_URL,
|
baseUrl: process.env.NEXT_PUBLIC_SERVER_URL,
|
||||||
homeSlug: 'homepage',
|
homeSlug: 'homepage',
|
||||||
|
|
||||||
resolveDocument: async ({ payload, params, locale }) => {
|
resolveDocument: async ({ payload, params, locale }) => {
|
||||||
const slug = (params.slug as string[])?.join('/')
|
const slug = (params.slug as string[])?.join('/')
|
||||||
|
|
||||||
// meta MUSI przyjść w konkretnym locale (stringi), a slug jako mapa
|
// meta MUSI przyjść w konkretnym locale (stringi), a slug jako mapa
|
||||||
// locale→wartość (hreflang) — to dwa różne odczyty.
|
// locale→wartość (hreflang) — to dwa różne odczyty.
|
||||||
const found = await payload.find({
|
const found = await payload.find({
|
||||||
collection: 'posts',
|
collection: 'posts',
|
||||||
where: { slug: { equals: slug } },
|
where: { slug: { equals: slug } },
|
||||||
locale,
|
locale,
|
||||||
depth: 1,
|
depth: 1,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
})
|
})
|
||||||
const doc = found.docs[0]
|
const doc = found.docs[0]
|
||||||
if (!doc) return null
|
if (!doc) return null
|
||||||
|
|
||||||
const allLocales = await payload.findByID({
|
const allLocales = await payload.findByID({
|
||||||
collection: 'posts',
|
collection: 'posts',
|
||||||
id: doc.id,
|
id: doc.id,
|
||||||
locale: 'all',
|
locale: 'all',
|
||||||
depth: 0,
|
depth: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
return { ...doc, slug: allLocales.slug }
|
return { ...doc, slug: allLocales.slug }
|
||||||
},
|
},
|
||||||
|
|
||||||
resolveSiteName: async ({ payload, locale }) =>
|
resolveSiteName: async ({ payload, locale }) =>
|
||||||
(await getSiteSettings(payload, { locale })).siteName ?? null,
|
(await getSiteSettings(payload, { locale })).siteName ?? null,
|
||||||
resolveImageUrl: async ({ doc }) => doc.meta?.image?.url ?? null,
|
resolveImageUrl: async ({ doc }) => doc.meta?.image?.url ?? null,
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function generateMetadata({ params }) {
|
export async function generateMetadata({ params }) {
|
||||||
const { locale, slug } = await params
|
const { locale, slug } = await params
|
||||||
return generate({ payload: await getPayload({ config }), params: { slug }, locale })
|
return generate({ payload: await getPayload({ config }), params: { slug }, locale })
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -176,16 +176,16 @@ Gdy chcesz pełną kontrolę:
|
|||||||
import { buildMetadata, getLocalizedSlugs } from '@intecion/ipal-kit'
|
import { buildMetadata, getLocalizedSlugs } from '@intecion/ipal-kit'
|
||||||
|
|
||||||
return buildMetadata({
|
return buildMetadata({
|
||||||
meta: doc.meta, // z plugin-seo
|
meta: doc.meta, // z plugin-seo
|
||||||
siteName: settings.siteName,
|
siteName: settings.siteName,
|
||||||
imageUrl: '/og.png',
|
imageUrl: '/og.png',
|
||||||
locale: 'pl',
|
locale: 'pl',
|
||||||
slugs: getLocalizedSlugs({ slugField: docAllLocales.slug, config }),
|
slugs: getLocalizedSlugs({ slugField: docAllLocales.slug, config }),
|
||||||
config,
|
config,
|
||||||
baseUrl: 'https://example.com',
|
baseUrl: 'https://example.com',
|
||||||
separator: ' – ', // opcjonalne
|
separator: ' – ', // opcjonalne
|
||||||
order: 'site-first', // opcjonalne
|
order: 'site-first', // opcjonalne
|
||||||
homeSlug: 'homepage',
|
homeSlug: 'homepage',
|
||||||
})
|
})
|
||||||
// → { title, description, openGraph, alternates: { canonical, languages } }
|
// → { title, description, openGraph, alternates: { canonical, languages } }
|
||||||
```
|
```
|
||||||
@@ -232,10 +232,10 @@ nie rozjedzie się z tym, co strony serwują. Handlery są gotowe w
|
|||||||
```ts
|
```ts
|
||||||
// src/lib/content.ts
|
// src/lib/content.ts
|
||||||
export const { /* ... */, sitemap, robots } = createContentHelpers({
|
export const { /* ... */, sitemap, robots } = createContentHelpers({
|
||||||
config,
|
config,
|
||||||
content: contentConfig,
|
content: contentConfig,
|
||||||
i18n: i18nConfig, // wymagane dla sitemap (hreflang)
|
i18n: i18nConfig, // wymagane dla sitemap (hreflang)
|
||||||
baseUrl: process.env.NEXT_PUBLIC_SERVER_URL,
|
baseUrl: process.env.NEXT_PUBLIC_SERVER_URL,
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -278,7 +278,7 @@ Niskopoziomowo (własna trasa zamiast handlera z fabryki):
|
|||||||
import { buildSitemapEntries, buildRobots } from '@intecion/ipal-kit'
|
import { buildSitemapEntries, buildRobots } from '@intecion/ipal-kit'
|
||||||
|
|
||||||
const entries = await buildSitemapEntries({
|
const entries = await buildSitemapEntries({
|
||||||
payload, config: i18nConfig, baseUrl, content: contentConfig,
|
payload, config: i18nConfig, baseUrl, content: contentConfig,
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -321,9 +321,9 @@ import { buildIconsMetadata } from '@intecion/ipal-kit'
|
|||||||
import { getSettings } from '@/lib/payload'
|
import { getSettings } from '@/lib/payload'
|
||||||
|
|
||||||
export async function generateMetadata({ params }): Promise<Metadata> {
|
export async function generateMetadata({ params }): Promise<Metadata> {
|
||||||
const { locale } = await params
|
const { locale } = await params
|
||||||
const settings = await getSettings(locale)
|
const settings = await getSettings(locale)
|
||||||
return buildIconsMetadata(settings.favicon) // z pola favicon (panel)
|
return buildIconsMetadata(settings.favicon) // z pola favicon (panel)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -346,16 +346,16 @@ wynikach, logo w knowledge panel.
|
|||||||
import { buildOrganizationJsonLd } from '@intecion/ipal-kit'
|
import { buildOrganizationJsonLd } from '@intecion/ipal-kit'
|
||||||
|
|
||||||
const jsonLd = buildOrganizationJsonLd({
|
const jsonLd = buildOrganizationJsonLd({
|
||||||
name: settings.siteName,
|
name: settings.siteName,
|
||||||
url: process.env.NEXT_PUBLIC_SERVER_URL!,
|
url: process.env.NEXT_PUBLIC_SERVER_URL!,
|
||||||
logo: settings.logo,
|
logo: settings.logo,
|
||||||
sameAs: settings.socialLinks, // opcjonalne: profile społecznościowe
|
sameAs: settings.socialLinks, // opcjonalne: profile społecznościowe
|
||||||
})
|
})
|
||||||
|
|
||||||
// w JSX layoutu:
|
// w JSX layoutu:
|
||||||
<script
|
<script
|
||||||
type="application/ld+json"
|
type="application/ld+json"
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -403,42 +403,42 @@ import { i18nConfig } from '@/i18n.config'
|
|||||||
import type { SiteSetting } from '@/payload-types'
|
import type { SiteSetting } from '@/payload-types'
|
||||||
|
|
||||||
export default async function manifest(): Promise<MetadataRoute.Manifest> {
|
export default async function manifest(): Promise<MetadataRoute.Manifest> {
|
||||||
const payload = await getCachedPayload()
|
const payload = await getCachedPayload()
|
||||||
const settings = await getSiteSettings<SiteSetting>(payload, {
|
const settings = await getSiteSettings<SiteSetting>(payload, {
|
||||||
locale: i18nConfig.defaultLocale as never,
|
locale: i18nConfig.defaultLocale as never,
|
||||||
})
|
})
|
||||||
|
|
||||||
const siteName = settings?.siteName?.trim()
|
const siteName = settings?.siteName?.trim()
|
||||||
|
|
||||||
// Ikona z panelu (favicon → logo). Dla PNG podaj KONKRETNY rozmiar z media
|
// Ikona z panelu (favicon → logo). Dla PNG podaj KONKRETNY rozmiar z media
|
||||||
// (nie 'any' — 'any' jest tylko dla SVG). Bez ikony → pomiń pole icons.
|
// (nie 'any' — 'any' jest tylko dla SVG). Bez ikony → pomiń pole icons.
|
||||||
const icon = settings?.favicon ?? settings?.logo
|
const icon = settings?.favicon ?? settings?.logo
|
||||||
const iconEntry =
|
const iconEntry =
|
||||||
typeof icon === 'object' && icon?.url
|
typeof icon === 'object' && icon?.url
|
||||||
? (() => {
|
? (() => {
|
||||||
const isSvg = icon.mimeType === 'image/svg+xml' || icon.url.endsWith('.svg')
|
const isSvg = icon.mimeType === 'image/svg+xml' || icon.url.endsWith('.svg')
|
||||||
const size =
|
const size =
|
||||||
typeof icon.width === 'number' && typeof icon.height === 'number'
|
typeof icon.width === 'number' && typeof icon.height === 'number'
|
||||||
? `${Math.min(icon.width, icon.height)}x${Math.min(icon.width, icon.height)}`
|
? `${Math.min(icon.width, icon.height)}x${Math.min(icon.width, icon.height)}`
|
||||||
: '512x512'
|
: '512x512'
|
||||||
return {
|
return {
|
||||||
src: icon.url,
|
src: icon.url,
|
||||||
type: icon.mimeType ?? 'image/png',
|
type: icon.mimeType ?? 'image/png',
|
||||||
sizes: isSvg ? 'any' : size, // 'any' tylko dla SVG
|
sizes: isSvg ? 'any' : size, // 'any' tylko dla SVG
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
// Buduj TYLKO z tego, co jest. Brak pola → nie ma go w manifeście (zamiast
|
// Buduj TYLKO z tego, co jest. Brak pola → nie ma go w manifeście (zamiast
|
||||||
// zaszytego fallbacku). start_url z configu, nie zaszyte '/pl'.
|
// zaszytego fallbacku). start_url z configu, nie zaszyte '/pl'.
|
||||||
return {
|
return {
|
||||||
...(siteName ? { name: siteName, short_name: siteName } : {}),
|
...(siteName ? { name: siteName, short_name: siteName } : {}),
|
||||||
start_url: `/${i18nConfig.defaultLocale}`,
|
start_url: `/${i18nConfig.defaultLocale}`,
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
...(iconEntry ? { icons: [iconEntry] } : {}),
|
...(iconEntry ? { icons: [iconEntry] } : {}),
|
||||||
// theme_color / background_color / description — TYLKO jeśli dodasz pola w
|
// theme_color / background_color / description — TYLKO jeśli dodasz pola w
|
||||||
// panelu i je odczytasz. NIE zaszywaj '#0e1e24' ani opisu klienta.
|
// panelu i je odczytasz. NIE zaszywaj '#0e1e24' ani opisu klienta.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -522,16 +522,16 @@ zanim strona wygeneruje metadane. To pcha metadata do body. NIE deklaruj `<head>
|
|||||||
```tsx
|
```tsx
|
||||||
// ŹLE — jawny <head> zamyka head za wcześnie
|
// ŹLE — jawny <head> zamyka head za wcześnie
|
||||||
<html lang={locale}>
|
<html lang={locale}>
|
||||||
<head><MediaPreconnect /></head>
|
<head><MediaPreconnect /></head>
|
||||||
<body>{children}</body>
|
<body>{children}</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
// DOBRZE — MediaPreconnect w body, React 19 hoistuje link do head
|
// DOBRZE — MediaPreconnect w body, React 19 hoistuje link do head
|
||||||
<html lang={locale}>
|
<html lang={locale}>
|
||||||
<body>
|
<body>
|
||||||
<MediaPreconnect />
|
<MediaPreconnect />
|
||||||
{children}
|
{children}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -546,8 +546,8 @@ z jakiegoś powodu nie może być ISR):
|
|||||||
```ts
|
```ts
|
||||||
// next.config.ts
|
// next.config.ts
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
htmlLimitedBots:
|
htmlLimitedBots:
|
||||||
/Googlebot|Google-InspectionTool|Storebot-Google|Bingbot|Yandex|DuckDuckBot|Baiduspider|Screaming Frog|AhrefsBot|SemrushBot/i,
|
/Googlebot|Google-InspectionTool|Storebot-Google|Bingbot|Yandex|DuckDuckBot|Baiduspider|Screaming Frog|AhrefsBot|SemrushBot/i,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -652,10 +652,10 @@ searchbox (pole wyszukiwania pod wynikiem marki). RAZ w root layout:
|
|||||||
```tsx
|
```tsx
|
||||||
import { buildWebSiteJsonLd } from '@intecion/ipal-kit'
|
import { buildWebSiteJsonLd } from '@intecion/ipal-kit'
|
||||||
const jsonLd = buildWebSiteJsonLd({
|
const jsonLd = buildWebSiteJsonLd({
|
||||||
name: settings.siteName,
|
name: settings.siteName,
|
||||||
url: baseUrl,
|
url: baseUrl,
|
||||||
// TYLKO jeśli masz działającą stronę wyszukiwania:
|
// TYLKO jeśli masz działającą stronę wyszukiwania:
|
||||||
search: { target: `${baseUrl}/szukaj?q={search_term_string}` },
|
search: { target: `${baseUrl}/szukaj?q={search_term_string}` },
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
Pomiń `search`, jeśli nie ma realnej wyszukiwarki — SearchAction wskazujący na
|
Pomiń `search`, jeśli nie ma realnej wyszukiwarki — SearchAction wskazujący na
|
||||||
@@ -666,9 +666,9 @@ Detailing) + Google rozumie hierarchię. PER STRONA, z pozycji strony:
|
|||||||
```tsx
|
```tsx
|
||||||
import { buildBreadcrumbJsonLd } from '@intecion/ipal-kit'
|
import { buildBreadcrumbJsonLd } from '@intecion/ipal-kit'
|
||||||
const jsonLd = buildBreadcrumbJsonLd([
|
const jsonLd = buildBreadcrumbJsonLd([
|
||||||
{ name: 'Strona główna', url: `${base}/pl` },
|
{ name: 'Strona główna', url: `${base}/pl` },
|
||||||
{ name: 'Usługi', url: `${base}/pl/uslugi` },
|
{ name: 'Usługi', url: `${base}/pl/uslugi` },
|
||||||
{ name: 'Detailing', url: `${base}/pl/uslugi/detailing` },
|
{ name: 'Detailing', url: `${base}/pl/uslugi/detailing` },
|
||||||
])
|
])
|
||||||
```
|
```
|
||||||
Okruszki buduj z RZECZYWISTEJ pozycji strony (resolveRoute / ścieżka URL), NIE z
|
Okruszki buduj z RZECZYWISTEJ pozycji strony (resolveRoute / ścieżka URL), NIE z
|
||||||
@@ -679,7 +679,7 @@ samych pozycji co menu w headerze:
|
|||||||
```tsx
|
```tsx
|
||||||
import { buildSiteNavigationJsonLd } from '@intecion/ipal-kit'
|
import { buildSiteNavigationJsonLd } from '@intecion/ipal-kit'
|
||||||
const jsonLd = buildSiteNavigationJsonLd(
|
const jsonLd = buildSiteNavigationJsonLd(
|
||||||
navItems.map(i => ({ name: i.label, url: `${base}${i.href}` }))
|
navItems.map(i => ({ name: i.label, url: `${base}${i.href}` }))
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
Dane z tego samego źródła co widoczne menu — nie osobna zaszyta lista.
|
Dane z tego samego źródła co widoczne menu — nie osobna zaszyta lista.
|
||||||
@@ -914,9 +914,60 @@ export async function GET() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`buildSitemapXml` zwraca wcięty XML (czytelny w surowej postaci), a przeglądarka
|
`buildSitemapXml` zwraca wcięty XML. Dwa tryby wyświetlania:
|
||||||
dokłada widok drzewa. Zawiera hreflang jako `<xhtml:link>`. Crawlery czytają XML
|
|
||||||
normalnie.
|
**Bez `cssUrl`** → przeglądarka pokazuje natywny widok drzewa XML (wcięcia, zwijanie).
|
||||||
|
|
||||||
|
**Z `cssUrl`** → stylujesz XML własnym CSS (kafelki, etykiety). To W3C standard
|
||||||
|
„Associating Style Sheets with XML" — `type="text/css"`, NIE wycofywane (w
|
||||||
|
przeciwieństwie do XSLT/`text/xsl`). Zero ostrzeżenia, ładny wygląd, bezpieczne
|
||||||
|
dla Google (crawlery ignorują dyrektywę).
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const xml = buildSitemapXml(entries, { cssUrl: '/sitemap.css' })
|
||||||
|
```
|
||||||
|
|
||||||
|
### Starter CSS (public/sitemap.css)
|
||||||
|
|
||||||
|
Skopiuj do `public/sitemap.css` w projekcie, dostosuj do designu. Selektory to
|
||||||
|
bezpośrednio nazwy tagów XML:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* public/sitemap.css */
|
||||||
|
urlset {
|
||||||
|
display: block;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #090d16; color: #f1f5f9;
|
||||||
|
padding: 2rem 1.5rem; max-width: 1200px; margin: 0 auto;
|
||||||
|
}
|
||||||
|
url { /* każdy adres jako kafelek */
|
||||||
|
display: block;
|
||||||
|
background: #111827; border: 1px solid #1e293b; border-radius: 8px;
|
||||||
|
padding: 1rem 1.25rem; margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
loc { /* adres URL */
|
||||||
|
display: block; font-size: 0.95rem; font-weight: 600;
|
||||||
|
color: #f97316; margin-bottom: 0.5rem; word-break: break-all;
|
||||||
|
}
|
||||||
|
lastmod, changefreq, priority {
|
||||||
|
display: inline-block; font-size: 0.8rem; color: #94a3b8; margin-right: 1.5rem;
|
||||||
|
}
|
||||||
|
lastmod::before { content: 'Ostatnia modyfikacja: '; color: #64748b; }
|
||||||
|
changefreq::before { content: 'Częstotliwość: '; color: #64748b; }
|
||||||
|
priority::before { content: 'Priorytet: '; color: #64748b; }
|
||||||
|
link { /* tagi hreflang */
|
||||||
|
display: inline-block; font-size: 0.75rem;
|
||||||
|
background: #1e293b; color: #38bdf8; border: 1px solid #334155;
|
||||||
|
padding: 0.15rem 0.45rem; border-radius: 4px; margin: 0.4rem 0.35rem 0 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Kluczowe: `display: block` na `<url>`/`<loc>` zamienia „ścianę tekstu" w kafelki.
|
||||||
|
Etykiety („Ostatnia modyfikacja:") przez `::before`. Dostosuj kolory do projektu.
|
||||||
|
|
||||||
|
**Ograniczenie:** `<loc>` to tag XML, nie `<a href>` — CSS nie zrobi z niego
|
||||||
|
klikalnego linku (niektóre przeglądarki autodetektują URL). Zysk to czytelność
|
||||||
|
i organizacja, nie klikalność. Dla sitemap (głównie dla robotów) to akceptowalne.
|
||||||
|
|
||||||
### WAŻNE — jeden sitemap, nie dwa
|
### WAŻNE — jeden sitemap, nie dwa
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@intecion/ipal-kit",
|
"name": "@intecion/ipal-kit",
|
||||||
"version": "1.4.1",
|
"version": "1.4.2",
|
||||||
"description": "Intecion Payload Advanced Library — a Payload CMS 3 plugin: i18n, SEO, forms, consent, analytics, blog/archives.",
|
"description": "Intecion Payload Advanced Library — a Payload CMS 3 plugin: i18n, SEO, forms, consent, analytics, blog/archives.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,15 +1,27 @@
|
|||||||
import type { SitemapEntry } from './buildSitemapEntries.js'
|
import type { SitemapEntry } from './buildSitemapEntries.js'
|
||||||
|
|
||||||
|
type BuildSitemapXmlOptions = {
|
||||||
|
/**
|
||||||
|
* URL of a CSS stylesheet to make the sitemap readable in the browser, e.g.
|
||||||
|
* '/sitemap.css'. Uses `type="text/css"` — the W3C "Associating Style Sheets
|
||||||
|
* with XML" mechanism, which is NOT deprecated (unlike XSLT / type="text/xsl",
|
||||||
|
* which Chrome/WebKit are removing). CSS on XML shows no warning, styles the
|
||||||
|
* raw tags directly (e.g. `url { display: block }` turns the wall of text into
|
||||||
|
* cards), and crawlers ignore the directive entirely.
|
||||||
|
*/
|
||||||
|
cssUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializes sitemap entries to a clean, indented XML STRING — valid for crawlers
|
* Serializes sitemap entries to a clean, indented XML STRING — valid for crawlers
|
||||||
* and readable in the browser as the native XML tree (indented, collapsible,
|
* and (with `cssUrl`) styled in the browser via plain CSS.
|
||||||
* syntax-highlighted by the browser itself).
|
|
||||||
*
|
*
|
||||||
* NO XSLT stylesheet: browsers (Chrome et al.) are REMOVING XSLT support, so a
|
* Two viewing modes:
|
||||||
* <?xml-stylesheet?> approach is a dead end — it shows a deprecation warning now
|
* - No cssUrl → the browser's native formatted XML tree (indented, collapsible).
|
||||||
* and will break soon. Instead we serve plain XML with the correct Content-Type;
|
* - With cssUrl → a `<?xml-stylesheet type="text/css">` directive; the project's
|
||||||
* the browser renders its built-in formatted XML view (the "code tree" look) with
|
* CSS styles the XML tags (cards, labels via ::before). NOT XSLT — that's being
|
||||||
* no transformation needed.
|
* removed from browsers and shows a deprecation warning. CSS is safe and
|
||||||
|
* W3C-standard.
|
||||||
*
|
*
|
||||||
* // app/sitemap.xml/route.ts
|
* // app/sitemap.xml/route.ts
|
||||||
* import { buildSitemapXml } from '@intecion/ipal-kit'
|
* import { buildSitemapXml } from '@intecion/ipal-kit'
|
||||||
@@ -17,19 +29,26 @@ import type { SitemapEntry } from './buildSitemapEntries.js'
|
|||||||
* export const dynamic = 'force-dynamic'
|
* export const dynamic = 'force-dynamic'
|
||||||
* export async function GET() {
|
* export async function GET() {
|
||||||
* const entries = await sitemap()
|
* const entries = await sitemap()
|
||||||
* const xml = buildSitemapXml(entries)
|
* const xml = buildSitemapXml(entries, { cssUrl: '/sitemap.css' })
|
||||||
* return new Response(xml, {
|
* return new Response(xml, {
|
||||||
* headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
* headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
||||||
* })
|
* })
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* The indentation makes the raw XML pleasant to read; the browser's default XML
|
* Put sitemap.css in the project's /public and style the tags (see docs/seo.md
|
||||||
* viewer adds the tree/fold UI. Crawlers read the XML as usual.
|
* for a starter). Note: <loc> is an XML tag, not <a href> — CSS can't make it a
|
||||||
|
* clickable link (some browsers auto-detect URLs); the win is readability, not
|
||||||
|
* clickability.
|
||||||
*
|
*
|
||||||
* NOTE: if you use this custom route, DON'T also keep app/sitemap.ts — pick one
|
* NOTE: if you use this custom route, DON'T also keep app/sitemap.ts — pick one.
|
||||||
* (this route OR the Next MetadataRoute). Two sitemaps confuse crawlers.
|
* Two sitemaps confuse crawlers.
|
||||||
*/
|
*/
|
||||||
export function buildSitemapXml(entries: SitemapEntry[]): string {
|
export function buildSitemapXml(
|
||||||
|
entries: SitemapEntry[],
|
||||||
|
opts: BuildSitemapXmlOptions = {},
|
||||||
|
): string {
|
||||||
|
const { cssUrl } = opts
|
||||||
|
|
||||||
const esc = (s: string): string =>
|
const esc = (s: string): string =>
|
||||||
s
|
s
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
@@ -46,8 +65,8 @@ export function buildSitemapXml(entries: SitemapEntry[]): string {
|
|||||||
e.lastModified instanceof Date ? e.lastModified.toISOString() : String(e.lastModified)
|
e.lastModified instanceof Date ? e.lastModified.toISOString() : String(e.lastModified)
|
||||||
parts.push(` <lastmod>${esc(iso)}</lastmod>`)
|
parts.push(` <lastmod>${esc(iso)}</lastmod>`)
|
||||||
}
|
}
|
||||||
if (e.changeFrequency) {parts.push(` <changefreq>${e.changeFrequency}</changefreq>`)}
|
if (e.changeFrequency) parts.push(` <changefreq>${e.changeFrequency}</changefreq>`)
|
||||||
if (typeof e.priority === 'number') {parts.push(` <priority>${e.priority}</priority>`)}
|
if (typeof e.priority === 'number') parts.push(` <priority>${e.priority}</priority>`)
|
||||||
const alternates = e.alternates?.languages
|
const alternates = e.alternates?.languages
|
||||||
if (alternates) {
|
if (alternates) {
|
||||||
for (const [lang, href] of Object.entries(alternates)) {
|
for (const [lang, href] of Object.entries(alternates)) {
|
||||||
@@ -62,8 +81,11 @@ export function buildSitemapXml(entries: SitemapEntry[]): string {
|
|||||||
})
|
})
|
||||||
.join('\n')
|
.join('\n')
|
||||||
|
|
||||||
|
const stylesheet = cssUrl ? `<?xml-stylesheet type="text/css" href="${esc(cssUrl)}"?>\n` : ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
||||||
|
stylesheet +
|
||||||
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" ` +
|
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" ` +
|
||||||
`xmlns:xhtml="http://www.w3.org/1999/xhtml">\n` +
|
`xmlns:xhtml="http://www.w3.org/1999/xhtml">\n` +
|
||||||
urls +
|
urls +
|
||||||
|
|||||||
Reference in New Issue
Block a user