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.
75 lines
2.2 KiB
TypeScript
75 lines
2.2 KiB
TypeScript
import type { Article, Category } from '#shared/types/blog'
|
|
|
|
interface RawCategory extends Category {
|
|
nameEn?: string | null
|
|
}
|
|
|
|
interface RawArticle {
|
|
id: string
|
|
title: string
|
|
titleEn: string | null
|
|
slug: string
|
|
publishedAt: string
|
|
content: string | null
|
|
contentEn: string | null
|
|
cover: string
|
|
coverAlt: string | null
|
|
coverAltEn: string | null
|
|
authorName: string | null
|
|
expand?: { category?: RawCategory }
|
|
}
|
|
|
|
export default defineEventHandler(async (event): Promise<Article> => {
|
|
const slug = getRouterParam(event, 'slug')
|
|
|
|
if (!slug) {
|
|
throw createError({ statusCode: 400, statusMessage: 'Missing article slug' })
|
|
}
|
|
|
|
const lang = langParam(getQuery(event).lang)
|
|
|
|
const response = await pbFetch<PbList<RawArticle>>(
|
|
`/api/collections/articles/records?fields=${ARTICLE_DETAIL_FIELDS}&expand=category&filter=${encodeURIComponent(`slug = ${quote(slug)}`)}&${pagination(1, 1)}`
|
|
)
|
|
|
|
const article = response.items[0]
|
|
|
|
if (!article) {
|
|
throw createError({ statusCode: 404, statusMessage: 'Article not found' })
|
|
}
|
|
|
|
const hasTranslation = Boolean(article.titleEn && article.contentEn)
|
|
|
|
// An /en/ request for an article with no translation yet 404s cleanly,
|
|
// rather than silently falling back to Italian text on an English URL —
|
|
// see docs/content-model.md.
|
|
if (lang === 'en' && !hasTranslation) {
|
|
throw createError({ statusCode: 404, statusMessage: 'Article not found' })
|
|
}
|
|
|
|
const title = lang === 'en' ? article.titleEn! : article.title
|
|
const content = lang === 'en' ? article.contentEn : article.content
|
|
const coverAlt = lang === 'en' ? article.coverAltEn || article.coverAlt : article.coverAlt
|
|
const category = article.expand?.category
|
|
? {
|
|
name:
|
|
lang === 'en'
|
|
? article.expand.category.nameEn || article.expand.category.name
|
|
: article.expand.category.name,
|
|
slug: article.expand.category.slug,
|
|
}
|
|
: null
|
|
|
|
return {
|
|
title,
|
|
slug: article.slug,
|
|
publishedAt: article.publishedAt,
|
|
cover: toMediaImage('articles', article.id, article.cover, coverAlt),
|
|
category,
|
|
html: renderMarkdown(content),
|
|
summary: summarise(content),
|
|
author: article.authorName || null,
|
|
hasTranslation,
|
|
}
|
|
})
|