Files
vue-seo-layer/app/utils/pageHref.ts
Jakub Zych f2d13d6264 feat: pagination component and composable
- <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
2026-11-04 11:59:51 +01:00

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
}