Files
vue-seo-layer/app/composables/usePagination.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

72 lines
3.6 KiB
TypeScript

// Page links of a listing in the CMS-chosen URL form (D-40, D-42, UI-SPEC
// L2). The form comes from the resolve answer's pagination (a request with
// a page number), else the site answer's (when useSeoSite() ran on this
// page), else the layer's seo.pagination option; the path segment is the
// one of the page's locale. Page 1 is the unnumbered URL; the other query
// parameters stay.
//
// It also adds <link rel="prev"> and <link rel="next"> to the head and
// hands the number of pages to the CMS head (useCmsSeo), which replaces
// %pages% in the CMS title and description (D-41). Robots, canonical and
// the title and description suffixes come from the resolver.
//
// const { items, href } = usePagination({ current: page, last: lastPage })
import type { MaybeRefOrGetter } from 'vue'
import { paginationOption } from '../../lib/options.ts'
import { seoPageHref, type SeoPaginationForm } from '../utils/pageHref.ts'
import { seoPaginationWindow } from '../utils/paginationWindow.ts'
import type { SeoSiteAnswer } from '../utils/types.ts'
import { SEO_SITE_KEY } from './useSeoSite.ts'
/** The number of pages of the current listing, for the CMS head's %pages%. */
export function useSeoPages() {
return useState<number | null>('seo-layer:pages', () => null)
}
const primary = (code: string | undefined | null) => (code ?? '').toLowerCase().split(/[-_]/)[0] ?? ''
export interface UsePaginationOptions {
current: MaybeRefOrGetter<number>
last: MaybeRefOrGetter<number>
/** The listing's locale, for the path segment (default: the page's content locale). */
locale?: MaybeRefOrGetter<string | undefined>
}
export function usePagination(opts: UsePaginationOptions) {
const route = useRoute()
const answer = useSeoAnswer()
const config = useRuntimeConfig()
const { data: site } = useNuxtData<SeoSiteAnswer | null>(SEO_SITE_KEY)
const option = paginationOption((config.public.seo as { pagination?: unknown } | undefined)?.pagination)
const current = computed(() => Math.max(1, Math.trunc(Number(toValue(opts.current)) || 1)))
const last = computed(() => Math.max(0, Math.trunc(Number(toValue(opts.last)) || 0)))
const form = computed((): { form: SeoPaginationForm; param: string; segment: string } => {
const locale = toValue(opts.locale) || answer.value?.locale || primary(answer.value?.html_lang) || site.value?.default_locale || ''
const a = answer.value?.pagination
if (a && (a.form === 'query' || a.form === 'path')) return { form: a.form, param: a.param || option.param, segment: a.segment || '' }
const s = site.value?.pagination
if (s && (s.form === 'query' || s.form === 'path')) return { form: s.form, param: s.param || option.param, segment: s.segments?.[locale] || s.segments?.[primary(locale)] || '' }
return { form: option.form, param: option.param, segment: option.segments[locale] || option.segments[primary(locale)] || '' }
})
const href = (page: number) => seoPageHref(route.fullPath, page, form.value.form, form.value.segment, form.value.param)
const items = computed(() => seoPaginationWindow(current.value, last.value))
const pages = useSeoPages()
watch(last, (n) => (pages.value = n > 0 ? n : null), { immediate: true })
useHead(
computed(() => {
const link: Array<{ rel: string; href: string; key: string }> = []
if (last.value > 1 && current.value > 1 && current.value <= last.value) link.push({ rel: 'prev', href: href(current.value - 1), key: 'seo-pagination-prev' })
if (last.value > 1 && current.value < last.value) link.push({ rel: 'next', href: href(current.value + 1), key: 'seo-pagination-next' })
return { link }
}),
)
return { current, last, items, href, form }
}