- journalPaginationSource answers runtimeConfig.public.seo.pagination (the SEO module's option with its defaults), else the raw seo.pagination - the module computes the paged routes inside its extendPages callback, so an app with only form: 'path' gets /blog/strona/:page and /blog/page/:page (L4 WR-03)
131 lines
5.7 KiB
TypeScript
131 lines
5.7 KiB
TypeScript
// Journal route configuration: defaults, stable route names, validation and
|
|
// path filling. Pure TypeScript, so `node --test` and the layer module both
|
|
// import it directly.
|
|
|
|
import type { JournalRoutes } from './types.ts'
|
|
|
|
/** Neutral default paths. An app moves or disables any of them in journal.routes. */
|
|
export const DEFAULT_JOURNAL_ROUTES: Readonly<JournalRoutes> = Object.freeze({
|
|
index: '/blog',
|
|
post: '/blog/:slug',
|
|
category: '/blog/category/:category',
|
|
tag: '/blog/tag/:tag',
|
|
author: '/blog/author/:author',
|
|
search: false,
|
|
})
|
|
|
|
/** Route names stay stable whatever the paths are, so links never hard-code a path. */
|
|
export const JOURNAL_ROUTE_NAMES: Readonly<Record<keyof JournalRoutes, string>> = Object.freeze({
|
|
index: 'journal-index',
|
|
post: 'journal-post',
|
|
category: 'journal-category',
|
|
tag: 'journal-tag',
|
|
author: 'journal-author',
|
|
search: 'journal-search',
|
|
})
|
|
|
|
/** The route param each path must carry. */
|
|
const REQUIRED_PARAM: Readonly<Record<keyof JournalRoutes, string | null>> = {
|
|
index: null,
|
|
post: 'slug',
|
|
category: 'category',
|
|
tag: 'tag',
|
|
author: 'author',
|
|
search: null,
|
|
}
|
|
|
|
const ROUTE_KEYS = Object.keys(DEFAULT_JOURNAL_ROUTES) as Array<keyof JournalRoutes>
|
|
|
|
/**
|
|
* Merges `input` over the defaults and validates every value: each is `false`
|
|
* or a string starting with "/", and post, category, tag and author carry
|
|
* their `:slug`, `:category`, `:tag` or `:author` param. Throws an Error that
|
|
* names `journal.routes.<key>` otherwise.
|
|
*/
|
|
export function normalizeJournalRoutes(input?: Partial<Record<string, unknown>> | null): JournalRoutes {
|
|
const out = { ...DEFAULT_JOURNAL_ROUTES } as JournalRoutes
|
|
for (const key of ROUTE_KEYS) {
|
|
const raw = input && Object.prototype.hasOwnProperty.call(input, key) ? input[key] : undefined
|
|
if (raw === undefined || raw === null) continue
|
|
if (raw === false) {
|
|
out[key] = false
|
|
continue
|
|
}
|
|
if (typeof raw !== 'string' || !raw.startsWith('/')) {
|
|
throw new Error(`journal.routes.${key} must be false or a path starting with "/", got ${JSON.stringify(raw)}`)
|
|
}
|
|
const param = REQUIRED_PARAM[key]
|
|
if (param && !new RegExp(`:${param}(?![A-Za-z0-9_])`).test(raw)) {
|
|
throw new Error(`journal.routes.${key} must contain the :${param} param, got "${raw}"`)
|
|
}
|
|
out[key] = raw
|
|
}
|
|
return out
|
|
}
|
|
|
|
/**
|
|
* Fills the `:name` params of a route pattern, encoding each value per path
|
|
* segment (a "/" inside a value stays a separator). A custom matcher such as
|
|
* `:slug(.*)` and a trailing modifier are dropped with the param. Unknown
|
|
* params become empty.
|
|
*/
|
|
export function fillJournalPath(pattern: string, params: Record<string, string | number>): string {
|
|
return pattern.replace(/:([A-Za-z_][A-Za-z0-9_]*)(\([^)]*\))?[?*+]?/g, (_m, name: string) => {
|
|
const value = params[name]
|
|
if (value === undefined || value === null) return ''
|
|
return String(value)
|
|
.split('/')
|
|
.map((part) => encodeURIComponent(part))
|
|
.join('/')
|
|
})
|
|
}
|
|
|
|
/** The listing views that page (the search page keeps ?page with its ?q). */
|
|
export const JOURNAL_PAGED_VIEWS: ReadonlyArray<keyof JournalRoutes> = Object.freeze(['index', 'category', 'tag', 'author'])
|
|
|
|
/** The route name of a listing's path-form page route for one segment. */
|
|
export function journalPagedRouteName(key: keyof JournalRoutes, segment: string): 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
|
|
* `<listing>/<segment>/:page` for every distinct locale segment
|
|
* (seo.pagination.segments, else the param name), rendering the same page
|
|
* as the listing. The query form, or no option, adds none.
|
|
*/
|
|
export function journalPagedRoutes(routes: JournalRoutes, pagination: unknown): Array<{ key: keyof JournalRoutes; name: string; path: string }> {
|
|
const p = pagination !== null && typeof pagination === 'object' ? (pagination as Record<string, unknown>) : {}
|
|
if (p.form !== 'path') return []
|
|
const segment = /^[\p{L}\p{N}_-]+$/u
|
|
const raw = p.segments !== null && typeof p.segments === 'object' ? Object.values(p.segments as Record<string, unknown>) : []
|
|
const segments = [...new Set(raw.filter((v): v is string => typeof v === 'string' && segment.test(v)))]
|
|
if (segments.length === 0) segments.push(typeof p.param === 'string' && segment.test(p.param) ? p.param : 'page')
|
|
const out: Array<{ key: keyof JournalRoutes; name: string; path: string }> = []
|
|
for (const key of JOURNAL_PAGED_VIEWS) {
|
|
const base = routes[key]
|
|
if (base === false) continue
|
|
for (const seg of segments) out.push({ key, name: journalPagedRouteName(key, seg), path: `${base.replace(/\/+$/, '')}/${seg}/:page(\\d+)` })
|
|
}
|
|
return out
|
|
}
|