From e1980cac3726184f25953761fd61f47a67daae25 Mon Sep 17 00:00:00 2001 From: EagleFox31 <86088743+EagleFox31@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:46:30 +0100 Subject: [PATCH 1/3] feat(editorial): six bilingual rubrics rebased over merged GEO work --- redirects.ts | 23 +++- src/Header/Component.tsx | 36 ++++-- .../(sitemaps)/pages-sitemap.xml/route.ts | 19 +-- .../api/editorial/setup-rubriques/route.ts | 75 ++++++++++++ src/app/(frontend)/en/africa/page.tsx | 7 +- .../(frontend)/en/built-in-africa/page.tsx | 7 ++ .../(frontend)/en/business-finance/page.tsx | 7 ++ src/app/(frontend)/en/business/page.tsx | 7 +- .../(frontend)/en/education-careers/page.tsx | 7 ++ src/app/(frontend)/en/gaming-culture/page.tsx | 7 ++ .../en/information-systems/page.tsx | 7 +- src/app/(frontend)/en/society-future/page.tsx | 7 ++ src/app/(frontend)/en/tech-ai/page.tsx | 7 ++ src/app/(frontend)/en/technology/page.tsx | 7 +- src/app/(frontend)/fr/africa/page.tsx | 7 +- .../(frontend)/fr/business-finance/page.tsx | 7 ++ src/app/(frontend)/fr/business/page.tsx | 7 +- .../(frontend)/fr/etudes-carrieres/page.tsx | 7 ++ src/app/(frontend)/fr/gaming-culture/page.tsx | 7 ++ .../fr/information-systems/page.tsx | 7 +- src/app/(frontend)/fr/made-in-africa/page.tsx | 7 ++ src/app/(frontend)/fr/societe-avenir/page.tsx | 7 ++ src/app/(frontend)/fr/tech-ia/page.tsx | 7 ++ src/app/(frontend)/fr/technology/page.tsx | 7 +- src/app/(frontend)/globals.css | 17 +++ src/components/SetupRubricsNavControl.tsx | 43 +++++++ src/components/insights/InsightsHome.tsx | 49 +++----- src/components/insights/LocalizedDeskPage.tsx | 98 +++++++--------- .../insights/NewsroomHome.module.css | 21 ++-- src/editorial/rubrics.ts | 109 ++++++++++++++++++ src/i18n/config.ts | 4 + src/i18n/content.ts | 4 +- src/i18n/messages.ts | 8 +- src/payload.config.ts | 2 +- tests/unit/editorialRubrics.spec.ts | 91 +++++++++++++++ 35 files changed, 576 insertions(+), 163 deletions(-) create mode 100644 src/app/(frontend)/api/editorial/setup-rubriques/route.ts create mode 100644 src/app/(frontend)/en/built-in-africa/page.tsx create mode 100644 src/app/(frontend)/en/business-finance/page.tsx create mode 100644 src/app/(frontend)/en/education-careers/page.tsx create mode 100644 src/app/(frontend)/en/gaming-culture/page.tsx create mode 100644 src/app/(frontend)/en/society-future/page.tsx create mode 100644 src/app/(frontend)/en/tech-ai/page.tsx create mode 100644 src/app/(frontend)/fr/business-finance/page.tsx create mode 100644 src/app/(frontend)/fr/etudes-carrieres/page.tsx create mode 100644 src/app/(frontend)/fr/gaming-culture/page.tsx create mode 100644 src/app/(frontend)/fr/made-in-africa/page.tsx create mode 100644 src/app/(frontend)/fr/societe-avenir/page.tsx create mode 100644 src/app/(frontend)/fr/tech-ia/page.tsx create mode 100644 src/components/SetupRubricsNavControl.tsx create mode 100644 src/editorial/rubrics.ts create mode 100644 tests/unit/editorialRubrics.spec.ts diff --git a/redirects.ts b/redirects.ts index d613c15..3996409 100644 --- a/redirects.ts +++ b/redirects.ts @@ -1,3 +1,4 @@ +import { editorialRubrics, localizedRubricPath } from './src/editorial/rubrics' import type { NextConfig } from 'next' export const redirects: NextConfig['redirects'] = async () => { @@ -32,5 +33,25 @@ export const redirects: NextConfig['redirects'] = async () => { source: '/:path((?!ie-incompatible.html$).*)', // all pages except the incompatibility page } - return [legacyVercelHostRedirect, homepageRedirect, internetExplorerRedirect] + const legacyEditorialPaths = [ + { old: 'technology', rubric: 'tech-ai' }, + { old: 'business', rubric: 'business-finance' }, + { old: 'information-systems', rubric: 'tech-ai' }, + { old: 'africa', rubric: 'society-future' }, + { old: 'consommer-local', rubric: 'made-in-africa' }, + ] as const + + const editorialRedirects = (['fr', 'en'] as const).flatMap((locale) => + legacyEditorialPaths.map(({ old, rubric }) => { + const target = editorialRubrics.find((section) => section.id === rubric) + if (!target) throw new Error(`Unknown canonical rubric: ${rubric}`) + return { + source: `/${locale}/${old}`, + destination: localizedRubricPath(target, locale), + permanent: true, + } + }), + ) + + return [legacyVercelHostRedirect, homepageRedirect, ...editorialRedirects, internetExplorerRedirect] } diff --git a/src/Header/Component.tsx b/src/Header/Component.tsx index 2f3d0f0..3e60043 100644 --- a/src/Header/Component.tsx +++ b/src/Header/Component.tsx @@ -4,24 +4,24 @@ import { BrandMark } from '@/components/insights/BrandMark' import { LanguageSwitcher } from '@/components/insights/LanguageSwitcher' import { localeFromPathname, withLocale } from '@/i18n/config' import { getMessages } from '@/i18n/messages' -import { ArrowUpRight } from 'lucide-react' +import { ArrowUpRight, Menu, X } from 'lucide-react' +import { editorialRubrics, localizedRubricPath } from '@/editorial/rubrics' import Link from 'next/link' import { usePathname } from 'next/navigation' -import React from 'react' +import React, { useState } from 'react' export function Header() { const pathname = usePathname() || '/fr' const locale = localeFromPathname(pathname) const t = getMessages(locale) - const navItems = [ - { href: withLocale(locale, '/technology'), label: t.header.tech }, - { href: withLocale(locale, '/business'), label: t.header.business }, - { href: withLocale(locale, '/information-systems'), label: t.header.systems }, - { href: withLocale(locale, '/africa'), label: t.header.africa }, - { href: withLocale(locale, '/consommer-local'), label: t.header.consumeLocal }, - { href: withLocale(locale, '/methodology'), label: t.header.methodology }, - ] + const [openedFromPath, setOpenedFromPath] = useState(null) + const menuOpen = openedFromPath === pathname + const navItems = editorialRubrics.map((rubric) => ({ + href: localizedRubricPath(rubric, locale), + label: rubric.title[locale], + })) + return (
@@ -36,11 +36,27 @@ export function Header() {
+ {t.header.brief}
+
) } diff --git a/src/app/(frontend)/(sitemaps)/pages-sitemap.xml/route.ts b/src/app/(frontend)/(sitemaps)/pages-sitemap.xml/route.ts index 8ac5a3b..7c23b72 100644 --- a/src/app/(frontend)/(sitemaps)/pages-sitemap.xml/route.ts +++ b/src/app/(frontend)/(sitemaps)/pages-sitemap.xml/route.ts @@ -4,15 +4,11 @@ import config from '@payload-config' import { unstable_cache } from 'next/cache' import { SITE_IDENTITY } from '@/seo/structuredData' +import { editorialRubrics, localizedRubricPath } from '@/editorial/rubrics' const localizedEditorialPaths = [ '', '/posts', - '/technology', - '/business', - '/information-systems', - '/africa', - '/consommer-local', '/consommer-local/cameroun', '/consommer-local/afrique', '/methodology', @@ -47,12 +43,16 @@ const getPagesSitemap = unstable_cache( const dateFallback = new Date().toISOString() const locales = ['fr', 'en'] as const - const localizedEditorialPages = locales.flatMap((locale) => - localizedEditorialPaths.map((path) => ({ + const localizedEditorialPages = locales.flatMap((locale) => [ + ...localizedEditorialPaths.map((path) => ({ loc: `${SITE_URL}/${locale}${path}`, lastmod: dateFallback, })), - ) + ...editorialRubrics.map((rubric) => ({ + loc: `${SITE_URL}${localizedRubricPath(rubric, locale)}`, + lastmod: dateFallback, + })), + ]) // Generic CMS pages are not localized yet, so keep their canonical legacy URLs. const cmsPages = results.docs @@ -66,9 +66,10 @@ const getPagesSitemap = unstable_cache( return [...localizedEditorialPages, ...cmsPages] }, - ['pages-sitemap-v3'], + ['pages-sitemap-v4'], { tags: ['pages-sitemap'], + revalidate: 86400, }, ) diff --git a/src/app/(frontend)/api/editorial/setup-rubriques/route.ts b/src/app/(frontend)/api/editorial/setup-rubriques/route.ts new file mode 100644 index 0000000..4d85afe --- /dev/null +++ b/src/app/(frontend)/api/editorial/setup-rubriques/route.ts @@ -0,0 +1,75 @@ +import config from '@payload-config' +import { headers } from 'next/headers' +import { createLocalReq, getPayload } from 'payload' + +import { editorialRubrics } from '@/editorial/rubrics' + +// Authenticated, idempotent opt-in. No mutation happens on a frontend GET/build. +// Keeps legacy CMS categories and existing article relationships intact. +export async function POST(): Promise { + const payload = await getPayload({ config }) + const { user } = await payload.auth({ headers: await headers() }) + if (!user) { + return Response.json({ success: false, message: 'Authentification Payload requise.' }, { status: 403 }) + } + + try { + const req = await createLocalReq({ user }, payload) + const results = [] + + for (const rubric of editorialRubrics) { + const existingResult = await payload.find({ + collection: 'categories', + depth: 0, + fallbackLocale: false, + locale: 'fr', + limit: 1, + overrideAccess: false, + req, + where: { slug: { equals: rubric.id } }, + }) + const existing = existingResult.docs[0] + + if (existing) { + results.push({ slug: rubric.id, id: existing.id, action: 'already-exists' }) + continue + } + + const doc = await payload.create({ + collection: 'categories', + locale: 'fr', + depth: 0, + overrideAccess: false, + req, + data: { + slug: rubric.id, + color: rubric.color, + title: rubric.title.fr, + manifesto: rubric.description.fr, + }, + }) + + await payload.update({ + collection: 'categories', + id: doc.id, + locale: 'en', + fallbackLocale: false, + depth: 0, + overrideAccess: false, + req, + data: { title: rubric.title.en, manifesto: rubric.description.en }, + }) + + results.push({ slug: rubric.id, id: doc.id, action: 'created' }) + } + + return Response.json({ + success: true, + results, + message: 'Les six rubriques FR/EN sont prêtes dans Payload. Aucun article existant n’a été modifié.', + }) + } catch (error) { + payload.logger.error({ err: error, message: 'Editorial rubric setup failed' }) + return Response.json({ success: false, message: 'Installation des rubriques impossible.' }, { status: 500 }) + } +} diff --git a/src/app/(frontend)/en/africa/page.tsx b/src/app/(frontend)/en/africa/page.tsx index 6c1e911..b92edc5 100644 --- a/src/app/(frontend)/en/africa/page.tsx +++ b/src/app/(frontend)/en/africa/page.tsx @@ -1,7 +1,6 @@ -import { LocalizedDeskPage, localizedDeskMetadata } from '@/components/insights/LocalizedDeskPage' +import { permanentRedirect } from 'next/navigation' +// Backstop for old links; next.config also installs a 308 at the edge. export default function Page() { - return + permanentRedirect('/en/society-future') } - -export const metadata = localizedDeskMetadata('en', 'africa') diff --git a/src/app/(frontend)/en/built-in-africa/page.tsx b/src/app/(frontend)/en/built-in-africa/page.tsx new file mode 100644 index 0000000..806def2 --- /dev/null +++ b/src/app/(frontend)/en/built-in-africa/page.tsx @@ -0,0 +1,7 @@ +import { LocalizedDeskPage, localizedDeskMetadata } from '@/components/insights/LocalizedDeskPage' + +export default function Page() { + return +} + +export const metadata = localizedDeskMetadata('en', 'made-in-africa') diff --git a/src/app/(frontend)/en/business-finance/page.tsx b/src/app/(frontend)/en/business-finance/page.tsx new file mode 100644 index 0000000..7a45a2e --- /dev/null +++ b/src/app/(frontend)/en/business-finance/page.tsx @@ -0,0 +1,7 @@ +import { LocalizedDeskPage, localizedDeskMetadata } from '@/components/insights/LocalizedDeskPage' + +export default function Page() { + return +} + +export const metadata = localizedDeskMetadata('en', 'business-finance') diff --git a/src/app/(frontend)/en/business/page.tsx b/src/app/(frontend)/en/business/page.tsx index bc79f18..249a564 100644 --- a/src/app/(frontend)/en/business/page.tsx +++ b/src/app/(frontend)/en/business/page.tsx @@ -1,7 +1,6 @@ -import { LocalizedDeskPage, localizedDeskMetadata } from '@/components/insights/LocalizedDeskPage' +import { permanentRedirect } from 'next/navigation' +// Backstop for old links; next.config also installs a 308 at the edge. export default function Page() { - return + permanentRedirect('/en/business-finance') } - -export const metadata = localizedDeskMetadata('en', 'business') diff --git a/src/app/(frontend)/en/education-careers/page.tsx b/src/app/(frontend)/en/education-careers/page.tsx new file mode 100644 index 0000000..12355d9 --- /dev/null +++ b/src/app/(frontend)/en/education-careers/page.tsx @@ -0,0 +1,7 @@ +import { LocalizedDeskPage, localizedDeskMetadata } from '@/components/insights/LocalizedDeskPage' + +export default function Page() { + return +} + +export const metadata = localizedDeskMetadata('en', 'education-careers') diff --git a/src/app/(frontend)/en/gaming-culture/page.tsx b/src/app/(frontend)/en/gaming-culture/page.tsx new file mode 100644 index 0000000..bc0015e --- /dev/null +++ b/src/app/(frontend)/en/gaming-culture/page.tsx @@ -0,0 +1,7 @@ +import { LocalizedDeskPage, localizedDeskMetadata } from '@/components/insights/LocalizedDeskPage' + +export default function Page() { + return +} + +export const metadata = localizedDeskMetadata('en', 'gaming-culture') diff --git a/src/app/(frontend)/en/information-systems/page.tsx b/src/app/(frontend)/en/information-systems/page.tsx index 96c118e..df48b20 100644 --- a/src/app/(frontend)/en/information-systems/page.tsx +++ b/src/app/(frontend)/en/information-systems/page.tsx @@ -1,7 +1,6 @@ -import { LocalizedDeskPage, localizedDeskMetadata } from '@/components/insights/LocalizedDeskPage' +import { permanentRedirect } from 'next/navigation' +// Backstop for old links; next.config also installs a 308 at the edge. export default function Page() { - return + permanentRedirect('/en/tech-ai') } - -export const metadata = localizedDeskMetadata('en', 'information-systems') diff --git a/src/app/(frontend)/en/society-future/page.tsx b/src/app/(frontend)/en/society-future/page.tsx new file mode 100644 index 0000000..153f76c --- /dev/null +++ b/src/app/(frontend)/en/society-future/page.tsx @@ -0,0 +1,7 @@ +import { LocalizedDeskPage, localizedDeskMetadata } from '@/components/insights/LocalizedDeskPage' + +export default function Page() { + return +} + +export const metadata = localizedDeskMetadata('en', 'society-future') diff --git a/src/app/(frontend)/en/tech-ai/page.tsx b/src/app/(frontend)/en/tech-ai/page.tsx new file mode 100644 index 0000000..00226e4 --- /dev/null +++ b/src/app/(frontend)/en/tech-ai/page.tsx @@ -0,0 +1,7 @@ +import { LocalizedDeskPage, localizedDeskMetadata } from '@/components/insights/LocalizedDeskPage' + +export default function Page() { + return +} + +export const metadata = localizedDeskMetadata('en', 'tech-ai') diff --git a/src/app/(frontend)/en/technology/page.tsx b/src/app/(frontend)/en/technology/page.tsx index c39f21f..df48b20 100644 --- a/src/app/(frontend)/en/technology/page.tsx +++ b/src/app/(frontend)/en/technology/page.tsx @@ -1,7 +1,6 @@ -import { LocalizedDeskPage, localizedDeskMetadata } from '@/components/insights/LocalizedDeskPage' +import { permanentRedirect } from 'next/navigation' +// Backstop for old links; next.config also installs a 308 at the edge. export default function Page() { - return + permanentRedirect('/en/tech-ai') } - -export const metadata = localizedDeskMetadata('en', 'technology') diff --git a/src/app/(frontend)/fr/africa/page.tsx b/src/app/(frontend)/fr/africa/page.tsx index 2a77f21..9814ca8 100644 --- a/src/app/(frontend)/fr/africa/page.tsx +++ b/src/app/(frontend)/fr/africa/page.tsx @@ -1,7 +1,6 @@ -import { LocalizedDeskPage, localizedDeskMetadata } from '@/components/insights/LocalizedDeskPage' +import { permanentRedirect } from 'next/navigation' +// Backstop for old links; next.config also installs a 308 at the edge. export default function Page() { - return + permanentRedirect('/fr/societe-avenir') } - -export const metadata = localizedDeskMetadata('fr', 'africa') diff --git a/src/app/(frontend)/fr/business-finance/page.tsx b/src/app/(frontend)/fr/business-finance/page.tsx new file mode 100644 index 0000000..ff23e64 --- /dev/null +++ b/src/app/(frontend)/fr/business-finance/page.tsx @@ -0,0 +1,7 @@ +import { LocalizedDeskPage, localizedDeskMetadata } from '@/components/insights/LocalizedDeskPage' + +export default function Page() { + return +} + +export const metadata = localizedDeskMetadata('fr', 'business-finance') diff --git a/src/app/(frontend)/fr/business/page.tsx b/src/app/(frontend)/fr/business/page.tsx index 00f0a30..6b6488c 100644 --- a/src/app/(frontend)/fr/business/page.tsx +++ b/src/app/(frontend)/fr/business/page.tsx @@ -1,7 +1,6 @@ -import { LocalizedDeskPage, localizedDeskMetadata } from '@/components/insights/LocalizedDeskPage' +import { permanentRedirect } from 'next/navigation' +// Backstop for old links; next.config also installs a 308 at the edge. export default function Page() { - return + permanentRedirect('/fr/business-finance') } - -export const metadata = localizedDeskMetadata('fr', 'business') diff --git a/src/app/(frontend)/fr/etudes-carrieres/page.tsx b/src/app/(frontend)/fr/etudes-carrieres/page.tsx new file mode 100644 index 0000000..13c0eaf --- /dev/null +++ b/src/app/(frontend)/fr/etudes-carrieres/page.tsx @@ -0,0 +1,7 @@ +import { LocalizedDeskPage, localizedDeskMetadata } from '@/components/insights/LocalizedDeskPage' + +export default function Page() { + return +} + +export const metadata = localizedDeskMetadata('fr', 'education-careers') diff --git a/src/app/(frontend)/fr/gaming-culture/page.tsx b/src/app/(frontend)/fr/gaming-culture/page.tsx new file mode 100644 index 0000000..6d02d51 --- /dev/null +++ b/src/app/(frontend)/fr/gaming-culture/page.tsx @@ -0,0 +1,7 @@ +import { LocalizedDeskPage, localizedDeskMetadata } from '@/components/insights/LocalizedDeskPage' + +export default function Page() { + return +} + +export const metadata = localizedDeskMetadata('fr', 'gaming-culture') diff --git a/src/app/(frontend)/fr/information-systems/page.tsx b/src/app/(frontend)/fr/information-systems/page.tsx index 184f9b4..58a8abe 100644 --- a/src/app/(frontend)/fr/information-systems/page.tsx +++ b/src/app/(frontend)/fr/information-systems/page.tsx @@ -1,7 +1,6 @@ -import { LocalizedDeskPage, localizedDeskMetadata } from '@/components/insights/LocalizedDeskPage' +import { permanentRedirect } from 'next/navigation' +// Backstop for old links; next.config also installs a 308 at the edge. export default function Page() { - return + permanentRedirect('/fr/tech-ia') } - -export const metadata = localizedDeskMetadata('fr', 'information-systems') diff --git a/src/app/(frontend)/fr/made-in-africa/page.tsx b/src/app/(frontend)/fr/made-in-africa/page.tsx new file mode 100644 index 0000000..7501019 --- /dev/null +++ b/src/app/(frontend)/fr/made-in-africa/page.tsx @@ -0,0 +1,7 @@ +import { LocalizedDeskPage, localizedDeskMetadata } from '@/components/insights/LocalizedDeskPage' + +export default function Page() { + return +} + +export const metadata = localizedDeskMetadata('fr', 'made-in-africa') diff --git a/src/app/(frontend)/fr/societe-avenir/page.tsx b/src/app/(frontend)/fr/societe-avenir/page.tsx new file mode 100644 index 0000000..18ae4b0 --- /dev/null +++ b/src/app/(frontend)/fr/societe-avenir/page.tsx @@ -0,0 +1,7 @@ +import { LocalizedDeskPage, localizedDeskMetadata } from '@/components/insights/LocalizedDeskPage' + +export default function Page() { + return +} + +export const metadata = localizedDeskMetadata('fr', 'society-future') diff --git a/src/app/(frontend)/fr/tech-ia/page.tsx b/src/app/(frontend)/fr/tech-ia/page.tsx new file mode 100644 index 0000000..238d08d --- /dev/null +++ b/src/app/(frontend)/fr/tech-ia/page.tsx @@ -0,0 +1,7 @@ +import { LocalizedDeskPage, localizedDeskMetadata } from '@/components/insights/LocalizedDeskPage' + +export default function Page() { + return +} + +export const metadata = localizedDeskMetadata('fr', 'tech-ai') diff --git a/src/app/(frontend)/fr/technology/page.tsx b/src/app/(frontend)/fr/technology/page.tsx index 8f31c01..58a8abe 100644 --- a/src/app/(frontend)/fr/technology/page.tsx +++ b/src/app/(frontend)/fr/technology/page.tsx @@ -1,7 +1,6 @@ -import { LocalizedDeskPage, localizedDeskMetadata } from '@/components/insights/LocalizedDeskPage' +import { permanentRedirect } from 'next/navigation' +// Backstop for old links; next.config also installs a 308 at the edge. export default function Page() { - return + permanentRedirect('/fr/tech-ia') } - -export const metadata = localizedDeskMetadata('fr', 'technology') diff --git a/src/app/(frontend)/globals.css b/src/app/(frontend)/globals.css index 6c0b2e9..f12a3c8 100644 --- a/src/app/(frontend)/globals.css +++ b/src/app/(frontend)/globals.css @@ -2401,3 +2401,20 @@ a { top: 15px; } } + +/* Navigation for six editorial desks: full-width desktop links plus accessible mobile menu. */ +.site-header__nav { gap: clamp(8px, 1vw, 17px); } +.site-header__nav a { font-size: 11px; white-space: nowrap; } +.site-header__menu-toggle { display: none; align-items: center; justify-content: center; width: 40px; height: 40px; border: 1px solid #d7d8d5; border-radius: 8px; color: #102f52; background: #fff; } +.site-header__mobile-nav { display: none; } +@media (max-width: 1180px) { + .site-header__nav { display: none; } + .site-header__inner { grid-template-columns: 1fr auto; } + .site-header__menu-toggle { display: inline-flex; } + .site-header__mobile-nav.is-open { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px 18px; padding: 14px max(20px, calc((100vw - 1160px) / 2)) 20px; border-top: 1px solid #dfded7; background: #fafaf7; } + .site-header__mobile-nav a { padding: 12px 10px; font-size: 14px; font-weight: 650; color: #102f52; border-bottom: 1px solid #e4e3dd; } + .site-header__mobile-nav a:hover { color: #bc6316; } +} +@media (max-width: 640px) { + .site-header__mobile-nav.is-open { grid-template-columns: 1fr; } +} diff --git a/src/components/SetupRubricsNavControl.tsx b/src/components/SetupRubricsNavControl.tsx new file mode 100644 index 0000000..ee52325 --- /dev/null +++ b/src/components/SetupRubricsNavControl.tsx @@ -0,0 +1,43 @@ +'use client' + +import { useState } from 'react' + +/** + * Visible only in authenticated Payload admin navigation. + * Editors opt in once; a frontend deployment cannot mutate production content. + */ +export function SetupRubricsNavControl() { + const [busy, setBusy] = useState(false) + const [message, setMessage] = useState('') + + async function setupRubrics() { + setBusy(true) + setMessage('') + try { + const response = await fetch('/api/editorial/setup-rubriques', { + method: 'POST', + credentials: 'include', + }) + const result = (await response.json()) as { message?: string } + setMessage(result.message || (response.ok ? 'Rubriques créées.' : 'Échec de configuration.')) + } catch { + setMessage('Impossible de joindre le serveur.') + } finally { + setBusy(false) + } + } + + return ( +
+ + {message &&

{message}

} +
+ ) +} diff --git a/src/components/insights/InsightsHome.tsx b/src/components/insights/InsightsHome.tsx index ace77a5..f1aa288 100644 --- a/src/components/insights/InsightsHome.tsx +++ b/src/components/insights/InsightsHome.tsx @@ -9,6 +9,7 @@ import Link from 'next/link' import React from 'react' import { NewsletterForm } from './NewsletterForm' +import { editorialRubrics, localizedRubricPath, rubricMatchesCategory, type EditorialRubricId } from '@/editorial/rubrics' import { ArticleImpressionBoundary } from '@/components/analytics/ArticleImpressionBoundary' import { TrackedArticleLink } from '@/components/analytics/TrackedArticleLink' import type { ArticlePlacement } from '@/lib/analytics/events' @@ -64,15 +65,10 @@ function categorySlug(post: Post) { return category && typeof category === 'object' ? category.slug || '' : '' } -function postMatchesChannel(post: Post, channel: string) { - const slug = categorySlug(post) - - if (channel === 'technology') return slug === 'technology' - if (channel === 'business') return slug === 'business' - if (channel === 'systems') return slug === 'information-systems' - if (channel === 'africa') return slug === 'africa' - - return false +function postMatchesChannel(post: Post, channel: EditorialRubricId) { + return post.categories?.some( + (item) => Boolean(item && typeof item === 'object' && rubricMatchesCategory(channel, item.slug)), + ) ?? false } function ArticleCardOverlay({ @@ -124,32 +120,13 @@ export function InsightsHome({ latestAfterCompact.length >= 2 ? latestAfterCompact : latestCandidates.slice(0, 5) const trending = remaining.slice(0, 3) - const channels = [ - { - color: '#e07520', - description: t.pillars.technology.description, - id: 'technology', - title: t.pillars.technology.title, - }, - { - color: '#15355a', - description: t.pillars.business.description, - id: 'business', - title: t.pillars.business.title, - }, - { - color: '#2c5f7a', - description: t.pillars.systems.description, - id: 'systems', - title: t.pillars.systems.title, - }, - { - color: '#5e7a3a', - description: t.pillars.africa.description, - id: 'africa', - title: t.pillars.africa.title, - }, - ] + const channels = editorialRubrics.map((rubric) => ({ + color: rubric.color, + description: rubric.description[locale], + id: rubric.id, + title: rubric.title[locale], + path: localizedRubricPath(rubric, locale), + })) if (!featured) { return ( @@ -536,7 +513,7 @@ export function InsightsHome({

{t.newsroom.deskPrefix} {channel.title}

-

{channel.title}

+

{channel.title}

{channel.description}

{channelPost ? ( diff --git a/src/components/insights/LocalizedDeskPage.tsx b/src/components/insights/LocalizedDeskPage.tsx index d59ef26..186b088 100644 --- a/src/components/insights/LocalizedDeskPage.tsx +++ b/src/components/insights/LocalizedDeskPage.tsx @@ -1,53 +1,44 @@ import type { Metadata } from 'next' +import Link from 'next/link' import configPromise from '@payload-config' import { getPayload } from 'payload' import { CollectionArchive } from '@/components/CollectionArchive' import type { CardPostData } from '@/components/Card' import type { SiteLocale } from '@/i18n/config' -import { getMessages } from '@/i18n/messages' +import { getEditorialRubric, localizedRubricPath, type EditorialRubricId } from '@/editorial/rubrics' import { absoluteCanonicalURL, buildBreadcrumbJsonLd, serializeJsonLd } from '@/seo/structuredData' -export type DeskSlug = 'technology' | 'business' | 'information-systems' | 'africa' - -const deskKey: Record = { - technology: 'technology', - business: 'business', - 'information-systems': 'systems', - africa: 'africa', -} - export async function LocalizedDeskPage({ locale, desk, }: { locale: SiteLocale - desk: DeskSlug + desk: EditorialRubricId }) { + const rubric = getEditorialRubric(desk) + const copy = { title: rubric.title[locale], description: rubric.description[locale] } + const path = localizedRubricPath(rubric, locale) const payload = await getPayload({ config: configPromise }) - const t = getMessages(locale) - const copy = t.pillars[deskKey[desk]] + // Query the new CMS category and original category IDs at the same time. + // Legacy post relationships survive this navigation change without a DB migration. const categoryResult = await payload.find({ collection: 'categories', depth: 0, fallbackLocale: false, locale, - limit: 1, + limit: 50, pagination: false, overrideAccess: false, - where: { - slug: { - equals: desk, - }, - }, + where: { slug: { in: [rubric.id, ...rubric.legacySlugs] } }, }) - const category = categoryResult.docs?.[0] + const categoryIds = categoryResult.docs.map((category) => category.id) let posts: CardPostData[] = [] - if (category) { - const postResult = await payload.find({ + if (categoryIds.length > 0) { + const result = await payload.find({ collection: 'posts', depth: 1, fallbackLocale: false, @@ -58,57 +49,50 @@ export async function LocalizedDeskPage({ sort: '-publishedAt', where: { and: [ - { - _status: { - equals: 'published', - }, - }, - { - categories: { - in: [category.id], - }, - }, + { _status: { equals: 'published' } }, + { categories: { in: categoryIds } }, ], }, - select: { - title: true, - slug: true, - categories: true, - meta: true, - }, + select: { title: true, slug: true, categories: true, meta: true }, }) - - posts = postResult.docs.filter((post) => Boolean(post.title)) + posts = result.docs.filter((post) => Boolean(post.title && post.slug)) } const breadcrumbJsonLd = buildBreadcrumbJsonLd([ { name: locale === 'fr' ? 'Accueil' : 'Home', path: `/${locale}` }, - { name: copy.title, path: `/${locale}/${desk}` }, + { name: copy.title, path }, ]) return (
-