2026-09-11 11:25:14 +02:00
|
|
|
import type { Article, Category } from '#shared/types/blog'
|
|
|
|
|
|
2026-09-11 13:52:13 +02:00
|
|
|
interface RawCategory extends Category {
|
|
|
|
|
nameEn?: string | null
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-11 11:25:14 +02:00
|
|
|
interface RawArticle {
|
|
|
|
|
id: string
|
|
|
|
|
title: string
|
2026-09-11 13:52:13 +02:00
|
|
|
titleEn: string | null
|
2026-09-11 11:25:14 +02:00
|
|
|
slug: string
|
|
|
|
|
publishedAt: string
|
|
|
|
|
content: string | null
|
2026-09-11 13:52:13 +02:00
|
|
|
contentEn: string | null
|
2026-09-11 11:25:14 +02:00
|
|
|
cover: string
|
|
|
|
|
coverAlt: string | null
|
2026-09-11 13:52:13 +02:00
|
|
|
coverAltEn: string | null
|
2026-09-11 11:25:14 +02:00
|
|
|
authorName: string | null
|
2026-09-11 13:52:13 +02:00
|
|
|
expand?: { category?: RawCategory }
|
2026-09-11 11:25:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default defineEventHandler(async (event): Promise<Article> => {
|
|
|
|
|
const slug = getRouterParam(event, 'slug')
|
|
|
|
|
|
|
|
|
|
if (!slug) {
|
|
|
|
|
throw createError({ statusCode: 400, statusMessage: 'Missing article slug' })
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-11 13:52:13 +02:00
|
|
|
const lang = langParam(getQuery(event).lang)
|
|
|
|
|
|
2026-09-11 11:25:14 +02:00
|
|
|
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' })
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-11 13:52:13 +02:00
|
|
|
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
|
|
|
|
|
|
2026-09-11 11:25:14 +02:00
|
|
|
return {
|
2026-09-11 13:52:13 +02:00
|
|
|
title,
|
2026-09-11 11:25:14 +02:00
|
|
|
slug: article.slug,
|
|
|
|
|
publishedAt: article.publishedAt,
|
2026-09-11 13:52:13 +02:00
|
|
|
cover: toMediaImage('articles', article.id, article.cover, coverAlt),
|
|
|
|
|
category,
|
|
|
|
|
html: renderMarkdown(content),
|
|
|
|
|
summary: summarise(content),
|
2026-09-11 11:25:14 +02:00
|
|
|
author: article.authorName || null,
|
2026-09-11 13:52:13 +02:00
|
|
|
hasTranslation,
|
2026-09-11 11:25:14 +02:00
|
|
|
}
|
|
|
|
|
})
|