Compare commits

..
2 Commits
Author SHA1 Message Date
radoslaw.smolinski 610ab6fdf5 1.4.3 2026-09-28 14:54:30 +02:00
radoslaw.smolinski 81275c0395 Added css asset for sitemap 2026-09-28 14:54:21 +02:00
4 changed files with 278 additions and 122 deletions
+76
View File
@@ -0,0 +1,76 @@
/*
* Starter stylesheet for a sitemap served via buildSitemapXml({ cssUrl }).
* Uses type="text/css" on XML (W3C "Associating Style Sheets with XML") — NOT
* XSLT, so no browser deprecation warning. Selectors are the XML tag names.
*
* Copy to your project's /public/sitemap.css and adjust colors/spacing to taste.
* Crawlers ignore this; it only affects the human-readable browser view.
*/
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;
line-height: 1.5;
}
/* Each URL entry as a card. */
url {
display: block;
background: #111827;
border: 1px solid #1e293b;
border-radius: 8px;
padding: 1rem 1.25rem;
margin-bottom: 0.75rem;
}
/* The URL itself. */
loc {
display: block;
font-size: 0.95rem;
font-weight: 600;
color: #f97316;
margin-bottom: 0.5rem;
word-break: break-all;
}
/* Metadata line: lastmod / changefreq / priority, each with a label. */
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;
}
/* hreflang alternates as small pills. */
link {
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;
}
+125 -121
View File
@@ -19,14 +19,14 @@ admina i bez importMap się nie wyrenderują.
```ts
ipalKit({
seo: {
collections: ['pages', 'posts'], // które kolekcje dostają meta
// generateTitle: ({ doc }) => `${doc.title}`, // opcjonalne
// generateDescription: ({ doc }) => doc.excerpt ?? '',
// fields: [...], // extra pola w grupie SEO
// autoFill: { title: 'title', description: 'excerpt' }, // mapowanie auto-fill
// autoFill: false, // wyłącz auto-fill
},
seo: {
collections: ['pages', 'posts'], // które kolekcje dostają meta
// generateTitle: ({ doc }) => `${doc.title}`, // opcjonalne
// generateDescription: ({ doc }) => doc.excerpt ?? '',
// fields: [...], // extra pola w grupie SEO
// autoFill: { title: 'title', description: 'excerpt' }, // mapowanie auto-fill
// autoFill: false, // wyłącz auto-fill
},
})
```
@@ -89,16 +89,16 @@ import { createPageMetadata } from '@intecion/ipal-kit'
import { i18nConfig } from '@/i18n.config'
const pageMetadata = createPageMetadata({
config: i18nConfig,
baseUrl: process.env.NEXT_PUBLIC_SERVER_URL,
// collection: 'pages', // domyślne
// settingsSlug: 'site-settings', // domyślne
// siteNameField: 'siteName', // domyślne
config: i18nConfig,
baseUrl: process.env.NEXT_PUBLIC_SERVER_URL,
// collection: 'pages', // domyślne
// settingsSlug: 'site-settings', // domyślne
// siteNameField: 'siteName', // domyślne
})
export async function generateMetadata({ params }): Promise<Metadata> {
const { locale, slug } = await params
return pageMetadata({ payload: await getPayload({ config }), locale, slug })
const { locale, slug } = await params
return pageMetadata({ payload: await getPayload({ config }), locale, slug })
}
```
@@ -121,43 +121,43 @@ global. Klient dostarcza resolvery.
import { createMetadataGenerator } from '@intecion/ipal-kit'
const generate = createMetadataGenerator({
config: i18nConfig,
baseUrl: process.env.NEXT_PUBLIC_SERVER_URL,
homeSlug: 'homepage',
config: i18nConfig,
baseUrl: process.env.NEXT_PUBLIC_SERVER_URL,
homeSlug: 'homepage',
resolveDocument: async ({ payload, params, locale }) => {
const slug = (params.slug as string[])?.join('/')
resolveDocument: async ({ payload, params, locale }) => {
const slug = (params.slug as string[])?.join('/')
// meta MUSI przyjść w konkretnym locale (stringi), a slug jako mapa
// locale→wartość (hreflang) — to dwa różne odczyty.
const found = await payload.find({
collection: 'posts',
where: { slug: { equals: slug } },
locale,
depth: 1,
limit: 1,
})
const doc = found.docs[0]
if (!doc) return null
// meta MUSI przyjść w konkretnym locale (stringi), a slug jako mapa
// locale→wartość (hreflang) — to dwa różne odczyty.
const found = await payload.find({
collection: 'posts',
where: { slug: { equals: slug } },
locale,
depth: 1,
limit: 1,
})
const doc = found.docs[0]
if (!doc) return null
const allLocales = await payload.findByID({
collection: 'posts',
id: doc.id,
locale: 'all',
depth: 0,
})
const allLocales = await payload.findByID({
collection: 'posts',
id: doc.id,
locale: 'all',
depth: 0,
})
return { ...doc, slug: allLocales.slug }
},
return { ...doc, slug: allLocales.slug }
},
resolveSiteName: async ({ payload, locale }) =>
(await getSiteSettings(payload, { locale })).siteName ?? null,
resolveImageUrl: async ({ doc }) => doc.meta?.image?.url ?? null,
resolveSiteName: async ({ payload, locale }) =>
(await getSiteSettings(payload, { locale })).siteName ?? null,
resolveImageUrl: async ({ doc }) => doc.meta?.image?.url ?? null,
})
export async function generateMetadata({ params }) {
const { locale, slug } = await params
return generate({ payload: await getPayload({ config }), params: { slug }, locale })
const { locale, slug } = await params
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'
return buildMetadata({
meta: doc.meta, // z plugin-seo
siteName: settings.siteName,
imageUrl: '/og.png',
locale: 'pl',
slugs: getLocalizedSlugs({ slugField: docAllLocales.slug, config }),
config,
baseUrl: 'https://example.com',
separator: ' – ', // opcjonalne
order: 'site-first', // opcjonalne
homeSlug: 'homepage',
meta: doc.meta, // z plugin-seo
siteName: settings.siteName,
imageUrl: '/og.png',
locale: 'pl',
slugs: getLocalizedSlugs({ slugField: docAllLocales.slug, config }),
config,
baseUrl: 'https://example.com',
separator: ' – ', // opcjonalne
order: 'site-first', // opcjonalne
homeSlug: 'homepage',
})
// → { title, description, openGraph, alternates: { canonical, languages } }
```
@@ -232,10 +232,10 @@ nie rozjedzie się z tym, co strony serwują. Handlery są gotowe w
```ts
// src/lib/content.ts
export const { /* ... */, sitemap, robots } = createContentHelpers({
config,
content: contentConfig,
i18n: i18nConfig, // wymagane dla sitemap (hreflang)
baseUrl: process.env.NEXT_PUBLIC_SERVER_URL,
config,
content: contentConfig,
i18n: i18nConfig, // wymagane dla sitemap (hreflang)
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'
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'
export async function generateMetadata({ params }): Promise<Metadata> {
const { locale } = await params
const settings = await getSettings(locale)
return buildIconsMetadata(settings.favicon) // z pola favicon (panel)
const { locale } = await params
const settings = await getSettings(locale)
return buildIconsMetadata(settings.favicon) // z pola favicon (panel)
}
```
@@ -346,16 +346,16 @@ wynikach, logo w knowledge panel.
import { buildOrganizationJsonLd } from '@intecion/ipal-kit'
const jsonLd = buildOrganizationJsonLd({
name: settings.siteName,
url: process.env.NEXT_PUBLIC_SERVER_URL!,
logo: settings.logo,
sameAs: settings.socialLinks, // opcjonalne: profile społecznościowe
})
name: settings.siteName,
url: process.env.NEXT_PUBLIC_SERVER_URL!,
logo: settings.logo,
sameAs: settings.socialLinks, // opcjonalne: profile społecznościowe
})
// w JSX layoutu:
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
// w JSX layoutu:
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
```
@@ -403,42 +403,42 @@ import { i18nConfig } from '@/i18n.config'
import type { SiteSetting } from '@/payload-types'
export default async function manifest(): Promise<MetadataRoute.Manifest> {
const payload = await getCachedPayload()
const settings = await getSiteSettings<SiteSetting>(payload, {
locale: i18nConfig.defaultLocale as never,
})
const payload = await getCachedPayload()
const settings = await getSiteSettings<SiteSetting>(payload, {
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
// (nie 'any' — 'any' jest tylko dla SVG). Bez ikony → pomiń pole icons.
const icon = settings?.favicon ?? settings?.logo
const iconEntry =
typeof icon === 'object' && icon?.url
? (() => {
const isSvg = icon.mimeType === 'image/svg+xml' || icon.url.endsWith('.svg')
const size =
typeof icon.width === 'number' && typeof icon.height === 'number'
? `${Math.min(icon.width, icon.height)}x${Math.min(icon.width, icon.height)}`
: '512x512'
return {
src: icon.url,
type: icon.mimeType ?? 'image/png',
sizes: isSvg ? 'any' : size, // 'any' tylko dla SVG
}
})()
: undefined
// Ikona z panelu (favicon → logo). Dla PNG podaj KONKRETNY rozmiar z media
// (nie 'any' — 'any' jest tylko dla SVG). Bez ikony → pomiń pole icons.
const icon = settings?.favicon ?? settings?.logo
const iconEntry =
typeof icon === 'object' && icon?.url
? (() => {
const isSvg = icon.mimeType === 'image/svg+xml' || icon.url.endsWith('.svg')
const size =
typeof icon.width === 'number' && typeof icon.height === 'number'
? `${Math.min(icon.width, icon.height)}x${Math.min(icon.width, icon.height)}`
: '512x512'
return {
src: icon.url,
type: icon.mimeType ?? 'image/png',
sizes: isSvg ? 'any' : size, // 'any' tylko dla SVG
}
})()
: undefined
// Buduj TYLKO z tego, co jest. Brak pola → nie ma go w manifeście (zamiast
// zaszytego fallbacku). start_url z configu, nie zaszyte '/pl'.
return {
...(siteName ? { name: siteName, short_name: siteName } : {}),
start_url: `/${i18nConfig.defaultLocale}`,
display: 'standalone',
...(iconEntry ? { icons: [iconEntry] } : {}),
// theme_color / background_color / description — TYLKO jeśli dodasz pola w
// panelu i je odczytasz. NIE zaszywaj '#0e1e24' ani opisu klienta.
}
// Buduj TYLKO z tego, co jest. Brak pola → nie ma go w manifeście (zamiast
// zaszytego fallbacku). start_url z configu, nie zaszyte '/pl'.
return {
...(siteName ? { name: siteName, short_name: siteName } : {}),
start_url: `/${i18nConfig.defaultLocale}`,
display: 'standalone',
...(iconEntry ? { icons: [iconEntry] } : {}),
// theme_color / background_color / description — TYLKO jeśli dodasz pola w
// 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
// ŹLE — jawny <head> zamyka head za wcześnie
<html lang={locale}>
<head><MediaPreconnect /></head>
<body>{children}</body>
<head><MediaPreconnect /></head>
<body>{children}</body>
</html>
// DOBRZE — MediaPreconnect w body, React 19 hoistuje link do head
<html lang={locale}>
<body>
<MediaPreconnect />
{children}
</body>
<body>
<MediaPreconnect />
{children}
</body>
</html>
```
@@ -546,8 +546,8 @@ z jakiegoś powodu nie może być ISR):
```ts
// next.config.ts
const nextConfig: NextConfig = {
htmlLimitedBots:
/Googlebot|Google-InspectionTool|Storebot-Google|Bingbot|Yandex|DuckDuckBot|Baiduspider|Screaming Frog|AhrefsBot|SemrushBot/i,
htmlLimitedBots:
/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
import { buildWebSiteJsonLd } from '@intecion/ipal-kit'
const jsonLd = buildWebSiteJsonLd({
name: settings.siteName,
url: baseUrl,
// TYLKO jeśli masz działającą stronę wyszukiwania:
search: { target: `${baseUrl}/szukaj?q={search_term_string}` },
name: settings.siteName,
url: baseUrl,
// TYLKO jeśli masz działającą stronę wyszukiwania:
search: { target: `${baseUrl}/szukaj?q={search_term_string}` },
})
```
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
import { buildBreadcrumbJsonLd } from '@intecion/ipal-kit'
const jsonLd = buildBreadcrumbJsonLd([
{ name: 'Strona główna', url: `${base}/pl` },
{ name: 'Usługi', url: `${base}/pl/uslugi` },
{ name: 'Detailing', url: `${base}/pl/uslugi/detailing` },
{ name: 'Strona główna', url: `${base}/pl` },
{ name: 'Usługi', url: `${base}/pl/uslugi` },
{ name: 'Detailing', url: `${base}/pl/uslugi/detailing` },
])
```
Okruszki buduj z RZECZYWISTEJ pozycji strony (resolveRoute / ścieżka URL), NIE z
@@ -679,7 +679,7 @@ samych pozycji co menu w headerze:
```tsx
import { buildSiteNavigationJsonLd } from '@intecion/ipal-kit'
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.
@@ -929,7 +929,11 @@ const xml = buildSitemapXml(entries, { cssUrl: '/sitemap.css' })
### Starter CSS (public/sitemap.css)
Skopiuj do `public/sitemap.css` w projekcie, dostosuj do designu. Selektory to
Plugin dostarcza gotowy plik — skopiuj do projektu:
```bash
cp node_modules/@intecion/ipal-kit/dist/modules/seo/assets/sitemap.css public/sitemap.css
```
Albo skopiuj poniższy starter i dostosuj do designu. Selektory to
bezpośrednio nazwy tagów XML:
```css
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@intecion/ipal-kit",
"version": "1.4.2",
"version": "1.4.3",
"description": "Intecion Payload Advanced Library — a Payload CMS 3 plugin: i18n, SEO, forms, consent, analytics, blog/archives.",
"license": "MIT",
"repository": {
+76
View File
@@ -0,0 +1,76 @@
/*
* Starter stylesheet for a sitemap served via buildSitemapXml({ cssUrl }).
* Uses type="text/css" on XML (W3C "Associating Style Sheets with XML") — NOT
* XSLT, so no browser deprecation warning. Selectors are the XML tag names.
*
* Copy to your project's /public/sitemap.css and adjust colors/spacing to taste.
* Crawlers ignore this; it only affects the human-readable browser view.
*/
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;
line-height: 1.5;
}
/* Each URL entry as a card. */
url {
display: block;
background: #111827;
border: 1px solid #1e293b;
border-radius: 8px;
padding: 1rem 1.25rem;
margin-bottom: 0.75rem;
}
/* The URL itself. */
loc {
display: block;
font-size: 0.95rem;
font-weight: 600;
color: #f97316;
margin-bottom: 0.5rem;
word-break: break-all;
}
/* Metadata line: lastmod / changefreq / priority, each with a label. */
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;
}
/* hreflang alternates as small pills. */
link {
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;
}