- <SeoPagination> (UI-SPEC L2): previous and next (left out at the ends), page 1, the last page and five pages around the current one with gap items; nothing when last <= 1; props current, last, labels, classPrefix, locale - usePagination(): hrefs in the CMS form (resolve answer, then site answer, then seo.pagination), other query parameters kept, page 1 unnumbered, <link rel=prev/next> in the head - the CMS head fills %pages% in the title and description once the listing's page count is known (seoFillPages) - seoPaginationWindow, seoPageHref, seoPageNumber; L2 default styles
52 lines
2.2 KiB
TypeScript
52 lines
2.2 KiB
TypeScript
// 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
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
}
|