diff --git a/README.md b/README.md index dff0373..d3c87c8 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,7 @@ The page head comes from the CMS: vue-seo-layer resolves every page through the - **ItemList.** The index, category, tag and author pages add an `ItemList` node (`@id` `#itemlist`, one `ListItem` with the absolute URL of each post shown, positions continuing across pages) through vue-seo-layer's `useSchemaNode` (`useJournalItemList`), so the page keeps one JSON-LD script. - **Body links.** `JournalPostBody` renders the body through `renderJournalBody` in the UI-SPEC L7 order: the sanitizer (with H1 demotion), vue-seo-layer's `autolink` with the resolve answer's `autolinks` and `autolink_max` (first occurrence, whole words, never in headings, links, code, figure captions or buttons, never to the page itself), `applyOutbound` with the answer's `outbound` (the editor's `rel` kept, `nofollow` or `sponsored` from the domain lists; `target="_blank"`, `rel="noopener"` and a `` hint only while `new_tab` is on, the hint while vue-seo-layer's `seo.links.newTabHint` is on, its default) and `imageAttrs` (`loading="lazy"`, `decoding="async"`, stored width, height and `alt=""` kept). Internal links are untouched. - **Breadcrumbs.** Post, index, category, tag and author pages render `` at the top of their content while `journal.breadcrumbs` is on. A site that places the trail in its layout sets it to `false` and renders one `` there; vue-seo-layer renders one trail per page either way, and the `BreadcrumbList` node is the CMS's. -- **Pagination.** `JournalPagination` renders `` with the journal's labels: the classes `journal-pagination`, `__prev`, `__pages`, `__current` and `__next` stay, plus page 1, the last page and `__gap` items, `aria-label`s and head `rel="prev"`/`rel="next"` links. Links follow the CMS's pagination form; with `seo.pagination.form: 'path'` in the app's config the module registers `//:page` routes for the index, category, tag and author pages. +- **Pagination.** `JournalPagination` renders `` with the journal's labels: the classes `journal-pagination`, `__prev`, `__pages`, `__current` and `__next` stay, plus page 1, the last page and `__gap` items, `aria-label`s and head `rel="prev"`/`rel="next"` links. Links follow the CMS's pagination form; with `seo.pagination.form: 'path'` in the app's config the module registers `//:page` routes for the index, category, tag and author pages. The segments follow vue-seo-layer's resolved `seo.pagination` option, defaults included (`pl` `strona`, `en` `page`), the same ones its pagination links use, so an app that sets only `form: 'path'` gets `/blog/strona/:page` and `/blog/page/:page`. ### Headings diff --git a/app/utils/routes.ts b/app/utils/routes.ts index 7e13c12..5ae44ba 100644 --- a/app/utils/routes.ts +++ b/app/utils/routes.ts @@ -88,6 +88,24 @@ export function journalPagedRouteName(key: keyof JournalRoutes, segment: string) return `${JOURNAL_ROUTE_NAMES[key]}-page-${segment}` } +/** + * The pagination option the journal's path-form routes follow (P33-71, L4 + * WR-03): vue-seo-layer's resolved option, which its module's setup writes + * into runtimeConfig.public.seo.pagination with its defaults (segments pl + * strona, en page), so the routes match the links usePagination builds; + * else the raw seo.pagination of the app's config. Read inside the + * extendPages callback, which runs after every module's setup. No import + * from vue-seo-layer: a git-installed layer's .ts is not type-stripped by + * Node under node_modules. + */ +export function journalPaginationSource(nuxtOptions: unknown): unknown { + if (nuxtOptions === null || typeof nuxtOptions !== 'object') return undefined + const o = nuxtOptions as { runtimeConfig?: { public?: { seo?: { pagination?: unknown } } }; seo?: { pagination?: unknown } } + const resolved = o.runtimeConfig?.public?.seo?.pagination + if (resolved !== null && typeof resolved === 'object') return resolved + return o.seo?.pagination +} + /** * The extra listing routes of the path pagination form (D-40): with the * vue-seo-layer build option seo.pagination.form 'path', each listing gets diff --git a/lib/journal-module.ts b/lib/journal-module.ts index 2ca36d6..8f131f8 100644 --- a/lib/journal-module.ts +++ b/lib/journal-module.ts @@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url' import { defineNuxtModule, extendPages } from 'nuxt/kit' -import { DEFAULT_JOURNAL_ROUTES, JOURNAL_ROUTE_NAMES, journalPagedRoutes, normalizeJournalRoutes } from '../app/utils/routes.ts' +import { DEFAULT_JOURNAL_ROUTES, JOURNAL_ROUTE_NAMES, journalPagedRoutes, journalPaginationSource, normalizeJournalRoutes } from '../app/utils/routes.ts' import type { JournalModuleOptions, JournalPostTitleTag, JournalRoutes } from '../app/utils/types.ts' const PAGE_FILES: Record = { @@ -55,12 +55,13 @@ export default defineNuxtModule({ setup(options, nuxt) { const routes = normalizeJournalRoutes(options.routes as unknown as Record) - // The path pagination form (vue-seo-layer's seo.pagination.form 'path', - // D-40) adds //:page routes rendering the same pages, - // which read the page from the :page param. - const paged = journalPagedRoutes(routes, (nuxt.options as { seo?: { pagination?: unknown } }).seo?.pagination) - extendPages((pages) => { + // The path pagination form (vue-seo-layer's seo.pagination.form 'path', + // D-40) adds //:page routes rendering the same pages, + // which read the page from the :page param. The segments come from the + // SEO module's resolved option (its defaults included), written in its + // setup, which has run by now (L4 WR-03). + const paged = journalPagedRoutes(routes, journalPaginationSource(nuxt.options)) for (const key of Object.keys(PAGE_FILES) as Array) { const path = routes[key] if (path === false) continue diff --git a/tests/routes.test.ts b/tests/routes.test.ts index efae349..ce984ce 100644 --- a/tests/routes.test.ts +++ b/tests/routes.test.ts @@ -6,6 +6,7 @@ import { JOURNAL_ROUTE_NAMES, fillJournalPath, journalPagedRoutes, + journalPaginationSource, normalizeJournalRoutes, } from '../app/utils/routes.ts' @@ -144,3 +145,19 @@ test('journalPagedRoutes: the query form, no option and disabled views add nothi path: '/news/p/:page(\\d+)', }) }) + +test('journalPaginationSource: the SEO module\'s resolved option, else the raw seo.pagination (L4 WR-03)', () => { + const resolved = { form: 'path', param: 'page', segments: { pl: 'strona', en: 'page' } } + assert.deepEqual(journalPaginationSource({ runtimeConfig: { public: { seo: { pagination: resolved } } }, seo: { pagination: { form: 'path' } } }), resolved) + assert.deepEqual(journalPaginationSource({ runtimeConfig: { public: {} }, seo: { pagination: { form: 'path' } } }), { form: 'path' }) + assert.deepEqual(journalPaginationSource({ runtimeConfig: { public: { seo: { cms: true } } }, seo: { pagination: { form: 'query' } } }), { form: 'query' }) + assert.equal(journalPaginationSource({}), undefined) + assert.equal(journalPaginationSource(null), undefined) +}) + +test('journalPagedRoutes: an app that set only form path gets the default pl and en segments through the resolved option', () => { + // vue-seo-layer's setup writes runtimeConfig.public.seo.pagination with its defaults + const nuxtOptions = { seo: { pagination: { form: 'path' } }, runtimeConfig: { public: { seo: { pagination: { form: 'path', param: 'page', segments: { pl: 'strona', en: 'page' } } } } } } + const paths = journalPagedRoutes(normalizeJournalRoutes({ category: false, tag: false, author: false }), journalPaginationSource(nuxtOptions)).map((r) => r.path) + assert.deepEqual(paths, ['/blog/strona/:page(\\d+)', '/blog/page/:page(\\d+)']) +})