- <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
57 lines
2.8 KiB
Vue
57 lines
2.8 KiB
Vue
<script setup lang="ts">
|
|
// Numbered, server-rendered page links (UI-SPEC L2, D-40, D-42): previous
|
|
// and next (left out at the ends), page 1, the last page and five pages
|
|
// around the current one with gap items, in the CMS-chosen URL form
|
|
// (usePagination, which also adds <link rel="prev/next"> to the head and
|
|
// fills %pages% in the CMS title). Renders nothing when last ≤ 1.
|
|
// classPrefix renames every class, so a layer keeps its own markup classes
|
|
// (vue-journal-layer renders it with class-prefix="journal-pagination").
|
|
// Links render through NuxtLink's custom slot: client navigation without
|
|
// the router's active classes and aria-current, which would otherwise mark
|
|
// every page of the same path (the router ignores the query).
|
|
import { formatSeoLabel, type SeoLabels } from '../../utils/labels.ts'
|
|
import { SEO_PAGINATION_GAP } from '../../utils/paginationWindow.ts'
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
current: number
|
|
last: number
|
|
/** Overrides the pagination.* labels; page takes {page}. */
|
|
labels?: Partial<SeoLabels['pagination']>
|
|
classPrefix?: string
|
|
/** The listing's locale (labels and the path segment; default: the page's content locale). */
|
|
locale?: string
|
|
}>(),
|
|
{ labels: () => ({}), classPrefix: 'seo-pagination', locale: '' },
|
|
)
|
|
|
|
const dictionary = useSeoLabels(() => props.locale)
|
|
const l = computed(() => ({ ...dictionary.value.pagination, ...Object.fromEntries(Object.entries(props.labels ?? {}).filter(([, v]) => typeof v === 'string' && v !== '')) }))
|
|
const { current, last, items, href } = usePagination({ current: () => props.current, last: () => props.last, locale: () => props.locale })
|
|
const c = (part: string) => `${props.classPrefix}__${part}`
|
|
</script>
|
|
|
|
<template>
|
|
<nav v-if="last > 1" :class="classPrefix" :aria-label="l.label">
|
|
<NuxtLink v-if="current > 1" v-slot="link" :to="href(current - 1)" custom>
|
|
<a :class="c('prev')" :href="link.href" rel="prev" @click="link.navigate">{{ l.previous }}</a>
|
|
</NuxtLink>
|
|
<ol :class="c('pages')">
|
|
<template v-for="(item, i) in items" :key="item === SEO_PAGINATION_GAP ? `gap-${i}` : item">
|
|
<li v-if="item === SEO_PAGINATION_GAP" :class="c('gap')" aria-hidden="true">…</li>
|
|
<li v-else-if="item === current">
|
|
<span :class="c('current')" aria-current="page">{{ item }}</span>
|
|
</li>
|
|
<li v-else>
|
|
<NuxtLink v-slot="link" :to="href(item)" custom>
|
|
<a :href="link.href" :aria-label="formatSeoLabel(l.page, { page: item })" @click="link.navigate">{{ item }}</a>
|
|
</NuxtLink>
|
|
</li>
|
|
</template>
|
|
</ol>
|
|
<NuxtLink v-if="current < last" v-slot="link" :to="href(current + 1)" custom>
|
|
<a :class="c('next')" :href="link.href" rel="next" @click="link.navigate">{{ l.next }}</a>
|
|
</NuxtLink>
|
|
</nav>
|
|
</template>
|