Files
vue-journal-layer/app/utils/routes.ts
Jakub Zych 6f6792f51c feat: scaffold Journal layer with configurable post route and git-install check
- journal module registers the Journal pages at journal.routes paths
- typed anonymous-GET client, html gate, SSR x-forwarded-for forwarding
- fixture API, built-server SSR smoke and git-tag install check
2026-10-09 17:31:45 +02:00

79 lines
2.9 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',
})
/** 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',
})
/** 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',
}
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('/')
})
}