// Page URLs in the CMS-chosen pagination form (D-40, UI-SPEC L2): query // (/blog?page=3) or path (/blog/strona/3, the segment per locale). Page 1 // is always the unnumbered URL; other query parameters stay. Built with // URLSearchParams, never by string concatenation of request input, and // rendered through Vue bindings (T-33-35). Pure, so `node --test` drives // it; names start with `seo` because app/utils is auto-imported. export type SeoPaginationForm = 'query' | 'path' const PAGE_PATH = /^(.*?)\/([^/]+)\/(\d+)$/ /** * The URL of page `page` for the listing at `url` (a path with an optional * query; a page number already in it, in either form, is replaced). `param` * is the query parameter, `segment` the path segment of the page's locale * (the param when empty). */ export function seoPageHref(url: string, page: number, form: SeoPaginationForm = 'query', segment = '', param = 'page'): string { const [rawPath = '/', rawQuery = ''] = (url.split('#')[0] ?? '').split('?') const query = new URLSearchParams(rawQuery) query.delete(param) const seg = segment || param let path = rawPath || '/' const numbered = PAGE_PATH.exec(path) if (form === 'path' && numbered && numbered[2] === seg) path = numbered[1] || '/' if (form === 'path' && page > 1) path = `${path.replace(/\/+$/, '')}/${encodeURIComponent(seg)}/${page}` if (form === 'query' && page > 1) query.set(param, String(page)) const qs = query.toString() return qs ? `${path}?${qs}` : path } /** * Where an explicit page 1 (/mapa-strony/posty/strona/1, /blog?page=1) * redirects: the unnumbered URL, built with the locale's segment, or null * when that target is the URL itself (a segment the URL does not carry), so * a page never answers a 301 to its own address (L4 WR-01). */ export function seoFirstPageRedirect(fullPath: string, form: SeoPaginationForm, segment = '', param = 'page'): string | null { const target = seoPageHref(fullPath, 1, form, segment, param) return target === fullPath ? null : target } /** * The page number a URL carries in the given form: 1 without one, the * number for a positive integer, null for anything else (a 404). */ export function seoPageNumber(url: string, form: SeoPaginationForm = 'query', segment = '', param = 'page'): number | null { const [rawPath = '/', rawQuery = ''] = (url.split('#')[0] ?? '').split('?') let raw: string | null = null if (form === 'path') { const m = PAGE_PATH.exec(rawPath) if (m && m[2] === (segment || param)) raw = m[3] ?? null } else { const values = new URLSearchParams(rawQuery).getAll(param) if (values.length > 1) return null raw = values[0] ?? null } if (raw === null) return 1 if (!/^\d+$/.test(raw)) return null const n = Number(raw) return Number.isSafeInteger(n) && n >= 1 ? n : null }