Files
blog/frontend/server/routes/content/articles/index.get.ts
T
davide 33b49c5dbb Add hand-rolled English support (/en/*) alongside the Italian site
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.
2026-09-11 13:53:17 +02:00

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,
}
})