From 63f557cd334f7726d3066a06e3bdb602e9dd92fb Mon Sep 17 00:00:00 2001 From: Anthony Geourjon Date: Tue, 4 Aug 2026 16:39:18 +0200 Subject: [PATCH 1/3] fix(labels): wire model labels to the backend SiteLabel API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit model-labels-server.ts read/wrote a local config/model-labels.json file, completely bypassing apps.common.SiteLabel and its /api/v1/site-labels/ endpoint (backend#139) — so backoffice edits to that table never reached the rendered site. Point readModelLabels/writeModelLabels at the backend instead, and align the footer keys to the backend's Key enum (footerLine1/footerLine2/footerBottomLine) since it hard-rejects unknown keys on write. footer.tsx still references the old footerFunded/footerCopyright keys — left for a follow-up since it needs to decide how to lay out 3 lines instead of 2. Co-Authored-By: Claude Sonnet 5 --- app/api/model-labels/route.ts | 6 +++- app/backoffice/translations/page.tsx | 17 +++++++---- lib/model-labels-server.ts | 43 ++++++++++++++++++---------- lib/model-labels.ts | 29 +++++++++++++------ 4 files changed, 65 insertions(+), 30 deletions(-) diff --git a/app/api/model-labels/route.ts b/app/api/model-labels/route.ts index 0a1cd776..5a88c65b 100644 --- a/app/api/model-labels/route.ts +++ b/app/api/model-labels/route.ts @@ -46,7 +46,11 @@ export async function PUT(request: NextRequest) { const config: ModelLabelsConfig = { labels: normalizeModelLabels((body as { labels?: unknown }).labels as Record), }; - await writeModelLabels(config); + try { + await writeModelLabels(config, token); + } catch { + return NextResponse.json({ error: 'Failed to update site labels' }, { status: 502 }); + } revalidatePath('/', 'layout'); return NextResponse.json(config); } diff --git a/app/backoffice/translations/page.tsx b/app/backoffice/translations/page.tsx index 47ea9613..e01dca62 100644 --- a/app/backoffice/translations/page.tsx +++ b/app/backoffice/translations/page.tsx @@ -31,14 +31,19 @@ const generalConfigFieldMeta: Array<{ key: ModelLabelKey; title: string; descrip description: 'The short strapline shown next to the site title in the header.', }, { - key: 'footerFunded', - title: 'Footer: Funding Statement', - description: 'The funding acknowledgement shown in the footer.', + key: 'footerLine1', + title: 'Footer: Line 1', + description: 'The first line of footer text.', }, { - key: 'footerCopyright', - title: 'Footer: Copyright Notice', - description: 'The copyright notice shown at the bottom of the footer.', + key: 'footerLine2', + title: 'Footer: Line 2', + description: 'The second line of footer text.', + }, + { + key: 'footerBottomLine', + title: 'Footer: Bottom Line', + description: 'The copyright/attribution notice shown at the bottom of the footer.', }, ]; diff --git a/lib/model-labels-server.ts b/lib/model-labels-server.ts index 5a71ac12..401bf503 100644 --- a/lib/model-labels-server.ts +++ b/lib/model-labels-server.ts @@ -1,27 +1,40 @@ -import { readJsonConfig, writeJsonConfig } from './json-config-file'; +import { apiFetch, authFetch } from './api-fetch'; import { getDefaultModelLabelsConfig, normalizeModelLabels, type ModelLabelsConfig, } from './model-labels'; -const CONFIG_FILE = 'model-labels.json'; +const SITE_LABELS_PATH = '/api/v1/site-labels/'; +/** + * Model labels are backend-owned (`SiteLabel` rows, superuser-editable via + * the backoffice). Any failure - network error, non-200, or an unexpected + * response shape - falls back to defaults so SSR never 500s over a backend + * hiccup (matches `getPublishedPages`'s fallback behavior). + */ export async function readModelLabels(): Promise { const defaults = getDefaultModelLabelsConfig(); - return readJsonConfig( - CONFIG_FILE, - (raw) => { - // If the file was hand-edited to `null`, an array, or a primitive, - // reading `parsed.labels` below would crash; bail to defaults. - if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return defaults; - const parsed = raw as Partial; - return { labels: normalizeModelLabels(parsed.labels) }; - }, - defaults - ); + try { + const res = await apiFetch(SITE_LABELS_PATH); + if (!res.ok) return defaults; + const raw = await res.json(); + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return defaults; + const parsed = raw as Partial; + return { labels: normalizeModelLabels(parsed.labels) }; + } catch { + return defaults; + } } -export async function writeModelLabels(config: ModelLabelsConfig): Promise { - await writeJsonConfig(CONFIG_FILE, { labels: normalizeModelLabels(config.labels) }); +/** Upserts the given keys via the backend's superuser-only PUT; throws on failure. */ +export async function writeModelLabels(config: ModelLabelsConfig, token: string): Promise { + const res = await authFetch(SITE_LABELS_PATH, token, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ labels: normalizeModelLabels(config.labels) }), + }); + if (!res.ok) { + throw new Error(`Failed to write site labels: ${res.status}`); + } } diff --git a/lib/model-labels.ts b/lib/model-labels.ts index 774279e1..710a7d2c 100644 --- a/lib/model-labels.ts +++ b/lib/model-labels.ts @@ -22,8 +22,9 @@ export type ModelLabelKey = // General site branding, shown in the header and footer. | 'siteTitle' | 'siteTagline' - | 'footerFunded' - | 'footerCopyright'; + | 'footerLine1' + | 'footerLine2' + | 'footerBottomLine'; export type ModelLabelLocale = 'en' | 'fr'; @@ -59,13 +60,25 @@ export const DEFAULT_MODEL_LABELS: Record = { en: 'Archetype tagline', fr: 'Archetype tagline', }, - footerFunded: { - en: 'Archetype funding text.', - fr: "Texte de financement d'Archetype.", + footerLine1: { + en: 'Footer first section', + fr: 'Pied de page, première section', }, - footerCopyright: { - en: 'Archetype copyright', - fr: "Droits d'auteur d'Archetype", + footerLine2: { + en: 'Footer second section', + fr: 'Pied de page, deuxième section', + }, + footerBottomLine: { + en: ( + '©2015–17 Models of Authority. Some parts available under CC-BY licence. ' + + 'All manuscript images are copyright of their respective repositories. ' + + 'Website by DDH / KDL. Built with Archetype.' + ), + fr: ( + '©2015–17 Models of Authority. Certaines parties sont disponibles sous licence CC-BY. ' + + 'Toutes les images de manuscrits sont la propriété de leurs dépôts respectifs. ' + + 'Site web par DDH / KDL. Construit avec Archetype.' + ), }, }; From 5b0bdfc052d6328539f3a4e458b44030f7183c53 Mon Sep 17 00:00:00 2001 From: Anthony Geourjon Date: Tue, 4 Aug 2026 16:45:22 +0200 Subject: [PATCH 2/3] Fix key and label in footer --- components/layout/footer.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/layout/footer.tsx b/components/layout/footer.tsx index 70f98b80..cfb61692 100644 --- a/components/layout/footer.tsx +++ b/components/layout/footer.tsx @@ -76,8 +76,8 @@ export default async function Footer() {

{getLabel('siteTitle')}

-

{t('about')}

-

{getLabel('footerFunded')}

+

{getLabel('footerLine1')}

+

{getLabel('footerLine2')}

{/* Links column */} @@ -131,7 +131,7 @@ export default async function Footer() { {/* Bottom bar */}

- {getLabel('footerCopyright')} + {getLabel('footerBottomLine')}

Date: Tue, 4 Aug 2026 16:54:30 +0200 Subject: [PATCH 3/3] Fix CI (format, build, lint) --- app/api/model-labels/route.ts | 11 +++++++++-- components/layout/footer.tsx | 2 +- lib/model-labels-server.ts | 6 +++++- lib/model-labels.ts | 18 ++++++++---------- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/app/api/model-labels/route.ts b/app/api/model-labels/route.ts index 5a88c65b..415b8d53 100644 --- a/app/api/model-labels/route.ts +++ b/app/api/model-labels/route.ts @@ -48,8 +48,15 @@ export async function PUT(request: NextRequest) { }; try { await writeModelLabels(config, token); - } catch { - return NextResponse.json({ error: 'Failed to update site labels' }, { status: 502 }); + } catch (err) { + const status = + err && + typeof err === 'object' && + 'status' in err && + typeof (err as { status: unknown }).status === 'number' + ? (err as { status: number }).status + : 502; + return NextResponse.json({ error: 'Failed to update site labels' }, { status }); } revalidatePath('/', 'layout'); return NextResponse.json(config); diff --git a/components/layout/footer.tsx b/components/layout/footer.tsx index cfb61692..58cd3613 100644 --- a/components/layout/footer.tsx +++ b/components/layout/footer.tsx @@ -57,7 +57,7 @@ export default async function Footer() { getPublishedPages(), ]); const locale = rawLocale as ModelLabelLocale; - const getLabel = (key: 'siteTitle' | 'footerFunded' | 'footerCopyright') => + const getLabel = (key: 'siteTitle' | 'footerLine1' | 'footerLine2' | 'footerBottomLine') => resolveModelLabel(modelLabels.labels[key], locale); const quickLinkPages = pages .filter((page) => page.include_in_quick_link) diff --git a/lib/model-labels-server.ts b/lib/model-labels-server.ts index 401bf503..e4e9a1fb 100644 --- a/lib/model-labels-server.ts +++ b/lib/model-labels-server.ts @@ -35,6 +35,10 @@ export async function writeModelLabels(config: ModelLabelsConfig, token: string) body: JSON.stringify({ labels: normalizeModelLabels(config.labels) }), }); if (!res.ok) { - throw new Error(`Failed to write site labels: ${res.status}`); + const details = await res.text().catch(() => ''); + throw Object.assign( + new Error(`Failed to write site labels: ${res.status}${details ? ` - ${details}` : ''}`), + { status: res.status } + ); } } diff --git a/lib/model-labels.ts b/lib/model-labels.ts index 710a7d2c..14b0fcc1 100644 --- a/lib/model-labels.ts +++ b/lib/model-labels.ts @@ -69,16 +69,14 @@ export const DEFAULT_MODEL_LABELS: Record = { fr: 'Pied de page, deuxième section', }, footerBottomLine: { - en: ( - '©2015–17 Models of Authority. Some parts available under CC-BY licence. ' + - 'All manuscript images are copyright of their respective repositories. ' + - 'Website by DDH / KDL. Built with Archetype.' - ), - fr: ( - '©2015–17 Models of Authority. Certaines parties sont disponibles sous licence CC-BY. ' + - 'Toutes les images de manuscrits sont la propriété de leurs dépôts respectifs. ' + - 'Site web par DDH / KDL. Construit avec Archetype.' - ), + en: + 'Archetype is freely-available software for structured annotations of images which allows users to search for, ' + + 'view, and organise detailed characteristics of handwriting or other material in both verbal and visual form. ' + + 'Archetype is designed primarily for the palaeographical analysis of handwriting.', + fr: + 'Archetype est un logiciel libre pour les annotations structurées d’images qui permet aux utilisateurs de rechercher' + + ' , de visualiser et d’organiser les caractéristiques détaillées de l’écriture ou d’autres matériaux sous forme verbale et visuelle. ' + + 'Archetype est conçu principalement pour l’analyse paléographique de l’écriture.', }, };