feat: customisable 404 + gallery-not-found pages via CMS (#324)

The 404 catch-all and the "gallery not found" branches in GalleryPage
were hard-coded English strings on a default-themed background — the
one place where a white-labelled deployment leaked the PicPeak default
look. Pluggable now via the existing CMS Pages mechanism.

Backend:
- Seed two new default CMS pages: `not-found` and `gallery-not-found`,
  with sensible English/German copy admins can edit in /admin/cms.
- Add `cms_pages.logo_url` (nullable) for per-page logo override; online
  migration on existing deployments. Null falls back to the global
  branding logo.
- New per-page logo upload (POST /api/admin/cms/pages/:slug/logo) +
  clear endpoint (DELETE …/logo). Reuses the existing /uploads/logos
  storage location with a `cms-<slug>-` filename prefix.
- adminCMS PUT now accepts logo_url; publicCMS GET returns it.

Frontend:
- New <CMSContentBlock slug fallback> component renders the CMS page in
  the standard branded shell (logo precedence: page → branding → bundled
  default), with DOMPurified content and footer/legal links.
- App.tsx: `path="*"` catch-all routes through CMSContentBlock("not-found").
- GalleryPage: collapses the two "gallery not found" branches (invalid
  identifier + infoError archived/missing) into a single
  CMSContentBlock("gallery-not-found"), so admins can edit one source
  of truth.
- Admin CMS Page editor gains an "Upload Logo / Use site default"
  control per page; falls back to the page's own English title in the
  page list when no `legal.<slug>` translation is registered.
This commit is contained in:
Paul Nothaft
2026-04-27 22:38:00 +02:00
parent b63a8774c4
commit 4f77905b87
9 changed files with 417 additions and 141 deletions
+28 -2
View File
@@ -7,6 +7,15 @@ export interface CMSPage {
title_de: string;
content_en: string;
content_de: string;
logo_url: string | null;
updated_at: string;
}
export interface PublicCMSPage {
title: string;
content: string;
slug: string;
logo_url: string | null;
updated_at: string;
}
@@ -30,10 +39,27 @@ export const cmsService = {
},
// Get public CMS page (no auth required)
async getPublicPage(slug: string, lang: string = 'en'): Promise<{ title: string; content: string }> {
const response = await api.get<{ title: string; content: string }>(`/public/pages/${slug}`, {
async getPublicPage(slug: string, lang: string = 'en'): Promise<PublicCMSPage> {
const response = await api.get<PublicCMSPage>(`/public/pages/${slug}`, {
params: { lang }
});
return response.data;
},
// Upload a per-page logo (#324)
async uploadPageLogo(slug: string, file: File): Promise<{ logo_url: string }> {
const formData = new FormData();
formData.append('logo', file);
const response = await api.post<{ logo_url: string }>(
`/admin/cms/pages/${slug}/logo`,
formData,
{ headers: { 'Content-Type': 'multipart/form-data' } }
);
return response.data;
},
// Clear a per-page logo override (revert to global branding logo).
async clearPageLogo(slug: string): Promise<void> {
await api.delete(`/admin/cms/pages/${slug}/logo`);
}
};