diff --git a/cms/src/api/article/content-types/article/schema.json b/cms/src/api/article/content-types/article/schema.json
index 247355f..488a18e 100644
--- a/cms/src/api/article/content-types/article/schema.json
+++ b/cms/src/api/article/content-types/article/schema.json
@@ -8,7 +8,8 @@
"description": "Blog article"
},
"options": {
- "draftAndPublish": true
+ "draftAndPublish": true,
+ "populateCreatorFields": true
},
"attributes": {
"title": {
diff --git a/frontend/app/pages/blog/[slug].vue b/frontend/app/pages/blog/[slug].vue
index 9e4638c..07380e0 100644
--- a/frontend/app/pages/blog/[slug].vue
+++ b/frontend/app/pages/blog/[slug].vue
@@ -39,6 +39,9 @@ useHead({
headline: article.value.title,
description: article.value.summary,
datePublished: isoDate(article.value.publishedAt),
+ author: article.value.author
+ ? { '@type': 'Person', name: article.value.author }
+ : undefined,
image: cover.value ? [cover.value] : undefined,
mainEntityOfPage: { '@type': 'WebPage', '@id': canonical },
}),
@@ -64,8 +67,12 @@ useHead({
+
+ ·
+ {{ $t('article.by', { name: article.author }) }}
+
@@ -90,7 +97,9 @@ useHead({
diff --git a/frontend/i18n/locales/en.json b/frontend/i18n/locales/en.json
index 39dea65..536d359 100644
--- a/frontend/i18n/locales/en.json
+++ b/frontend/i18n/locales/en.json
@@ -38,7 +38,8 @@
},
"article": {
"breadcrumb": "Breadcrumb",
- "back": "Back to all articles"
+ "back": "Back to all articles",
+ "by": "By {name}"
},
"error": {
"notFoundTitle": "Page not found",
diff --git a/frontend/i18n/locales/es.json b/frontend/i18n/locales/es.json
index 7968097..5ce291b 100644
--- a/frontend/i18n/locales/es.json
+++ b/frontend/i18n/locales/es.json
@@ -38,7 +38,8 @@
},
"article": {
"breadcrumb": "Ruta de navegación",
- "back": "Volver a todos los artículos"
+ "back": "Volver a todos los artículos",
+ "by": "Por {name}"
},
"error": {
"notFoundTitle": "Página no encontrada",
diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json
index 36dd80f..c81150a 100644
--- a/frontend/i18n/locales/fr.json
+++ b/frontend/i18n/locales/fr.json
@@ -38,7 +38,8 @@
},
"article": {
"breadcrumb": "Fil d'Ariane",
- "back": "Retour à tous les articles"
+ "back": "Retour à tous les articles",
+ "by": "Par {name}"
},
"error": {
"notFoundTitle": "Page introuvable",
diff --git a/frontend/i18n/locales/it.json b/frontend/i18n/locales/it.json
index 435ad74..0b46717 100644
--- a/frontend/i18n/locales/it.json
+++ b/frontend/i18n/locales/it.json
@@ -38,7 +38,8 @@
},
"article": {
"breadcrumb": "Percorso di navigazione",
- "back": "Torna a tutti gli articoli"
+ "back": "Torna a tutti gli articoli",
+ "by": "Di {name}"
},
"error": {
"notFoundTitle": "Pagina non trovata",
diff --git a/frontend/server/api/articles/[slug].get.ts b/frontend/server/api/articles/[slug].get.ts
index 7e5ad32..e8a4cb3 100644
--- a/frontend/server/api/articles/[slug].get.ts
+++ b/frontend/server/api/articles/[slug].get.ts
@@ -1,6 +1,20 @@
import type { Article } from '#shared/types/blog'
-type RawArticle = Omit & { content: string | null }
+interface AdminUser {
+ firstname: string | null
+ lastname: string | null
+}
+
+type RawArticle = Omit & {
+ content: string | null
+ createdBy?: AdminUser | null
+}
+
+/** Joins the admin user's name parts; returns null when neither is set. */
+function byline(user: AdminUser | null | undefined): string | null {
+ const name = [user?.firstname, user?.lastname].filter(Boolean).join(' ').trim()
+ return name || null
+}
export default defineEventHandler(async (event): Promise => {
const slug = getRouterParam(event, 'slug')
@@ -19,6 +33,11 @@ export default defineEventHandler(async (event): Promise => {
throw createError({ statusCode: 404, statusMessage: 'Article not found' })
}
- const { content, ...rest } = article
- return { ...rest, html: renderMarkdown(content), summary: summarise(content) }
+ const { content, createdBy, ...rest } = article
+ return {
+ ...rest,
+ html: renderMarkdown(content),
+ summary: summarise(content),
+ author: byline(createdBy),
+ }
})
diff --git a/frontend/server/utils/queries.ts b/frontend/server/utils/queries.ts
index 260c796..b41cb3b 100644
--- a/frontend/server/utils/queries.ts
+++ b/frontend/server/utils/queries.ts
@@ -16,6 +16,9 @@ export const ARTICLE_DETAIL_QUERY = [
list('fields', ['title', 'slug', 'content', 'publishedAt']),
list('populate[cover][fields]', IMAGE_FIELDS),
list('populate[category][fields]', ['name', 'slug']),
+ // The byline is the admin user who created the entry; Strapi exposes the
+ // name fields only because the content type sets populateCreatorFields.
+ list('populate[createdBy][fields]', ['firstname', 'lastname']),
].join('&')
export const PAGE_SIZE = 12
diff --git a/frontend/shared/types/blog.ts b/frontend/shared/types/blog.ts
index 2fcb710..86a3680 100644
--- a/frontend/shared/types/blog.ts
+++ b/frontend/shared/types/blog.ts
@@ -26,6 +26,8 @@ export interface Article extends ArticleSummary {
html: string
/** Plain-text opening of the body, used as the meta description. */
summary: string
+ /** Byline: the full name of the admin user who wrote the article. */
+ author: string | null
}
export interface Paginated {
diff --git a/frontend/shared/utils/format.ts b/frontend/shared/utils/format.ts
index 52e369a..88e2ba8 100644
--- a/frontend/shared/utils/format.ts
+++ b/frontend/shared/utils/format.ts
@@ -4,6 +4,13 @@ export function formatDate(value: string | null | undefined, locale = 'en-GB'):
return new Intl.DateTimeFormat(locale, { dateStyle: 'long' }).format(new Date(value))
}
+/** Date and time of publication, shown in the article byline. */
+export function formatDateTime(value: string | null | undefined, locale = 'en-GB'): string {
+ if (!value) return ''
+ return new Intl.DateTimeFormat(locale, { dateStyle: 'long', timeStyle: 'short' })
+ .format(new Date(value))
+}
+
/** Machine-readable date for the `datetime` attribute and structured data. */
export function isoDate(value: string | null | undefined): string {
return value ? new Date(value).toISOString() : ''