PocketBase gains optional, manually-authored English fields on articles and categories (same record, same slug), and the frontend serves an /en counterpart of every dynamic route via thin page wrappers around shared view components — no i18n library, consistent with the project's existing minimalism stance for a two-locale site with ~20 UI strings. An article/category with no translation 404s cleanly on its own /en detail page and is filtered out of /en listings, and the header's language-switch link falls back to the English blog index rather than a dead link; resolving that requires a global route middleware, since the layout's header renders before the page content in document order and so can't react to state a page component sets during its own async setup.
63 lines
2.0 KiB
TypeScript
63 lines
2.0 KiB
TypeScript
import type { ArticleSummary, Category, Paginated } from '#shared/types/blog'
|
|
import type { Locale } from '#shared/utils/locale'
|
|
|
|
interface RawCategory extends Category {
|
|
nameEn?: string | null
|
|
}
|
|
|
|
interface RawArticleSummary {
|
|
id: string
|
|
title: string
|
|
titleEn: string | null
|
|
slug: string
|
|
publishedAt: string
|
|
cover: string
|
|
coverAlt: string | null
|
|
coverAltEn: string | null
|
|
expand?: { category?: RawCategory }
|
|
}
|
|
|
|
function toSummary(raw: RawArticleSummary, lang: Locale): ArticleSummary {
|
|
const title = lang === 'en' ? raw.titleEn || raw.title : raw.title
|
|
const coverAlt = lang === 'en' ? raw.coverAltEn || raw.coverAlt : raw.coverAlt
|
|
const category = raw.expand?.category
|
|
? {
|
|
name: lang === 'en' ? raw.expand.category.nameEn || raw.expand.category.name : raw.expand.category.name,
|
|
slug: raw.expand.category.slug,
|
|
}
|
|
: null
|
|
|
|
return {
|
|
title,
|
|
slug: raw.slug,
|
|
publishedAt: raw.publishedAt,
|
|
cover: toMediaImage('articles', raw.id, raw.cover, coverAlt),
|
|
category,
|
|
}
|
|
}
|
|
|
|
export default defineEventHandler(async (event): Promise<Paginated<ArticleSummary>> => {
|
|
const query = getQuery(event)
|
|
const page = pageParam(query.page)
|
|
const lang = langParam(query.lang)
|
|
const category = typeof query.category === 'string' ? query.category : null
|
|
|
|
const filters = []
|
|
if (category) filters.push(`category.slug = ${quote(category)}`)
|
|
// An English listing only ever shows articles that have actually been
|
|
// translated — never a partial/fallback entry, see docs/content-model.md.
|
|
if (lang === 'en') filters.push(`titleEn != ''`)
|
|
const filter = filters.length ? `&filter=${encodeURIComponent(filters.join(' && '))}` : ''
|
|
|
|
const response = await pbFetch<PbList<RawArticleSummary>>(
|
|
`/api/collections/articles/records?fields=${ARTICLE_SUMMARY_FIELDS}&expand=category&sort=-publishedAt&${pagination(page)}${filter}`
|
|
)
|
|
|
|
return {
|
|
items: response.items.map((raw) => toSummary(raw, lang)),
|
|
page: response.page,
|
|
pageCount: response.totalPages,
|
|
total: response.totalItems,
|
|
}
|
|
})
|