Skip to content

Commit cce23f7

Browse files
Merge pull request #497 from CodeForAfrica/ft/global-pages
feat(pages): add global pages support with shared blocks and routing
2 parents 6a407b4 + 696744a commit cce23f7

File tree

31 files changed

+738
-455
lines changed

31 files changed

+738
-455
lines changed

src/app/(frontend)/[...slugs]/page.tsx

Lines changed: 114 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,26 @@ import React, { Suspense } from "react";
22
import type { Metadata } from "next";
33
import { notFound, redirect } from "next/navigation";
44

5-
import { getGlobalPayload, queryPageBySlug } from "@/lib/payload";
5+
import {
6+
getGlobalPayload,
7+
queryGlobalPageBySlug,
8+
queryPageBySlug,
9+
} from "@/lib/payload";
610
import { getDomain } from "@/lib/domain";
7-
import { CommonHomePage } from "@/components/CommonHomePage";
811
import { BlockRenderer } from "@/components/BlockRenderer";
912
import Navigation from "@/components/Navigation";
1013
import Footer from "@/components/Footer";
1114
import { getTenantBySubDomain, getTenantNavigation } from "@/lib/data/tenants";
1215
import { getPoliticalEntitiesByTenant } from "@/lib/data/politicalEntities";
1316
import {
1417
buildSeoMetadata,
18+
composeTitleSegments,
1519
getEntitySeo,
1620
getPageSeo,
1721
resolveTenantSeoContext,
1822
} from "@/lib/seo";
23+
import { resolveTenantLocale } from "@/utils/locales";
24+
import { resolveBrowserLocale } from "../layout";
1925

2026
type Args = {
2127
params: Promise<{
@@ -26,18 +32,35 @@ type Args = {
2632
export async function generateMetadata({ params }: Args): Promise<Metadata> {
2733
const paramsValue = await params;
2834
const slugs = paramsValue?.slugs ?? [];
35+
const pageSlug = slugs[0] ?? "index";
2936

3037
const { subdomain } = await getDomain();
3138
const tenantResolution = await resolveTenantSeoContext(subdomain);
3239

3340
if (tenantResolution.status === "missing") {
34-
return tenantResolution.metadata;
41+
const locale = await resolveBrowserLocale();
42+
const globalPage = await queryGlobalPageBySlug({ slug: pageSlug, locale });
43+
44+
if (!globalPage) {
45+
return tenantResolution.metadata;
46+
}
47+
48+
return buildSeoMetadata({
49+
defaults: {
50+
...tenantResolution.defaultSeo,
51+
title: globalPage.title || tenantResolution.defaultSeo.title,
52+
},
53+
});
3554
}
3655

3756
const { tenant, tenantSettings, tenantSeo, tenantTitleBase } =
3857
tenantResolution.context;
58+
const tenantLocale = resolveTenantLocale(tenant);
3959

40-
const politicalEntities = await getPoliticalEntitiesByTenant(tenant);
60+
const politicalEntities = await getPoliticalEntitiesByTenant(
61+
tenant,
62+
tenantLocale
63+
);
4164
const [maybePoliticalEntitySlug, pageSlugCandidate] = slugs;
4265
const politicalEntity = politicalEntities.find(
4366
(entity) => entity.slug === maybePoliticalEntitySlug
@@ -57,10 +80,30 @@ export async function generateMetadata({ params }: Args): Promise<Metadata> {
5780
tenantTitleBase,
5881
});
5982

60-
const pageSlug = pageSlugCandidate ?? "index";
61-
const page = await queryPageBySlug({ slug: pageSlug, tenant });
83+
const tenantPageSlug = pageSlugCandidate ?? "index";
84+
const page = await queryPageBySlug({
85+
slug: tenantPageSlug,
86+
tenant,
87+
locale: tenantLocale,
88+
});
6289

6390
if (!page) {
91+
const globalPage = await queryGlobalPageBySlug({
92+
slug: tenantPageSlug,
93+
locale: tenantLocale,
94+
});
95+
96+
if (globalPage) {
97+
return buildSeoMetadata({
98+
defaults: {
99+
...entitySeo,
100+
title:
101+
composeTitleSegments(globalPage.title, politicalEntity.name) ||
102+
entitySeo.title,
103+
},
104+
});
105+
}
106+
64107
return buildSeoMetadata({
65108
meta: politicalEntity.meta,
66109
defaults: entitySeo,
@@ -85,11 +128,26 @@ export default async function Page(params: Args) {
85128
const { subdomain, tenantSelectionHref } = await getDomain();
86129

87130
const tenant = await getTenantBySubDomain(subdomain);
131+
const locale = tenant
132+
? resolveTenantLocale(tenant)
133+
: await resolveBrowserLocale();
88134

89-
const { title, description, navigation, footer } =
90-
await getTenantNavigation(tenant);
135+
const { title, description, navigation, footer } = await getTenantNavigation(
136+
tenant,
137+
locale
138+
);
139+
140+
const paramsValue = await params.params;
141+
const slugs = paramsValue?.slugs ?? [];
91142

92143
if (!tenant) {
144+
const pageSlug = slugs[0] ?? "index";
145+
const globalPage = await queryGlobalPageBySlug({ slug: pageSlug, locale });
146+
147+
if (!globalPage) {
148+
return notFound();
149+
}
150+
93151
return (
94152
<>
95153
<Navigation
@@ -98,16 +156,17 @@ export default async function Page(params: Args) {
98156
tenantSelectionHref={tenantSelectionHref}
99157
showSearch={false}
100158
/>
101-
<CommonHomePage />
159+
<Suspense>
160+
<BlockRenderer blocks={globalPage.blocks} />
161+
</Suspense>
162+
<Footer title={title} description={description} {...footer} />
102163
</>
103164
);
104165
}
105166

106-
const paramsValue = await params.params;
107-
const slugs = paramsValue?.slugs ?? [];
108167
const [maybePoliticalEntitySlug, pageSlugCandidate] = slugs;
109168

110-
const politicalEntities = await getPoliticalEntitiesByTenant(tenant);
169+
const politicalEntities = await getPoliticalEntitiesByTenant(tenant, locale);
111170

112171
const politicalEntity = politicalEntities.find(
113172
(entity) => entity.slug === maybePoliticalEntitySlug
@@ -128,17 +187,13 @@ export default async function Page(params: Args) {
128187
redirect(targetPath);
129188
}
130189
}
131-
const fallbackPageSlugs = slugs.length > 0 ? slugs : ["index"];
190+
132191
const payload = await getGlobalPayload();
133192
const homePage = await payload.findGlobal({
134-
slug: "home-page",
193+
slug: "entity-page",
194+
locale,
135195
});
136-
const entityBlocks = (homePage?.entitySelector?.blocks ?? []).map(
137-
(block) =>
138-
block.blockType === "entity-selection"
139-
? { ...block, pageSlugs: fallbackPageSlugs }
140-
: block
141-
);
196+
const entityBlocks = homePage?.entitySelector?.blocks ?? [];
142197

143198
return (
144199
<>
@@ -147,18 +202,53 @@ export default async function Page(params: Args) {
147202
{...navigation}
148203
tenantSelectionHref={tenantSelectionHref}
149204
/>
150-
<BlockRenderer blocks={entityBlocks} />
205+
<Suspense>
206+
<BlockRenderer blocks={entityBlocks} />
207+
</Suspense>
151208
<Footer title={title} description={description} {...footer} />
152209
</>
153210
);
154211
}
155212

156-
const pageSlug = pageSlugCandidate ?? "index";
213+
const tenantPageSlug = pageSlugCandidate ?? "index";
157214

158-
const page = await queryPageBySlug({ slug: pageSlug, tenant });
215+
const page = await queryPageBySlug({
216+
slug: tenantPageSlug,
217+
tenant,
218+
locale,
219+
});
159220

160221
if (!page) {
161-
return notFound();
222+
const globalPage = await queryGlobalPageBySlug({
223+
slug: tenantPageSlug,
224+
locale,
225+
});
226+
if (!globalPage) {
227+
return notFound();
228+
}
229+
230+
return (
231+
<>
232+
<Navigation
233+
title={title}
234+
{...navigation}
235+
entitySlug={politicalEntity.slug}
236+
tenantName={tenant?.name ?? null}
237+
tenantSelectionHref={tenantSelectionHref}
238+
tenantFlag={tenant?.flag ?? null}
239+
tenantFlagLabel={tenant?.name ?? tenant?.country ?? null}
240+
/>
241+
<Suspense>
242+
<BlockRenderer blocks={globalPage.blocks} entity={politicalEntity} />
243+
</Suspense>
244+
<Footer
245+
title={title}
246+
description={description}
247+
{...footer}
248+
entitySlug={politicalEntity.slug}
249+
/>
250+
</>
251+
);
162252
}
163253

164254
const { blocks } = page;

src/app/(frontend)/[entitySlug]/promises/[promiseId]/page.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import PromiseStatus from "@/components/PromiseStatus";
1010
import PromiseTimeline from "@/components/PromiseTimeline";
1111
import PromiseActions from "@/components/PromiseActions";
1212
import EntityBackLink from "@/components/EntityBackLink";
13-
import { CommonHomePage } from "@/components/CommonHomePage";
1413
import { getDomain } from "@/lib/domain";
1514
import {
1615
getTenantBySubDomain,
@@ -137,7 +136,11 @@ export async function generateMetadata({
137136
tenantResolution.context;
138137
const tenantLocale = resolveTenantLocale(tenant);
139138

140-
const politicalEntity = await getPoliticalEntityBySlug(tenant, entitySlug);
139+
const politicalEntity = await getPoliticalEntityBySlug(
140+
tenant,
141+
entitySlug,
142+
tenantLocale
143+
);
141144
if (!politicalEntity) {
142145
return buildSeoMetadata({
143146
meta: tenantSettings?.meta,
@@ -256,14 +259,16 @@ export default async function PromiseDetailPage({
256259
const tenant = await getTenantBySubDomain(subdomain);
257260

258261
if (!tenant) {
259-
return <CommonHomePage />;
262+
return notFound();
260263
}
261264
const locale = resolveTenantLocale(tenant);
262265

263-
const { title, description, navigation, footer } =
264-
await getTenantNavigation(tenant);
266+
const { title, description, navigation, footer } = await getTenantNavigation(
267+
tenant,
268+
locale
269+
);
265270

266-
const entity = await getPoliticalEntityBySlug(tenant, entitySlug);
271+
const entity = await getPoliticalEntityBySlug(tenant, entitySlug, locale);
267272

268273
if (!entity) {
269274
return notFound();
@@ -305,7 +310,7 @@ export default async function PromiseDetailPage({
305310
const promiseUrl = typeof promise.url === "string" ? promise.url : "";
306311
const titleText = promise.title?.trim() || "Promise";
307312
const promiseUpdateSettings = await getPromiseUpdateEmbed();
308-
const siteSettings = await getTenantSiteSettings(tenant);
313+
const siteSettings = await getTenantSiteSettings(tenant, locale);
309314
const rawPromiseUpdateEmbed = promiseUpdateSettings?.embedCode ?? null;
310315
const promiseUpdateEmbed = rawPromiseUpdateEmbed
311316
? prefillAirtableForm(

src/app/(frontend)/layout.tsx

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@ import theme from "@/theme/theme";
44
import { ThemeProvider } from "@mui/material";
55
import { Amiri, Open_Sans, Source_Sans_3 } from "next/font/google";
66
import type { Metadata } from "next";
7+
import { headers } from "next/headers";
78
import { getDomain } from "@/lib/domain";
89
import { getTenantBySubDomain } from "@/lib/data/tenants";
9-
import { resolveTenantLocale } from "@/utils/locales";
10+
import {
11+
PAYLOAD_SUPPORTED_LOCALES,
12+
type PayloadLocale,
13+
resolveTenantLocale,
14+
} from "@/utils/locales";
1015

1116
const amiri = Amiri({
1217
subsets: ["arabic", "latin"],
@@ -40,12 +45,42 @@ export const metadata: Metadata = {
4045
},
4146
};
4247

48+
export const resolveBrowserLocale = async (): Promise<PayloadLocale> => {
49+
const headerList = await headers();
50+
const acceptLanguage = headerList.get("accept-language");
51+
if (!acceptLanguage) {
52+
return "en";
53+
}
54+
55+
const supported = new Set<PayloadLocale>(PAYLOAD_SUPPORTED_LOCALES);
56+
const candidates = acceptLanguage
57+
.split(",")
58+
.map((entry) => entry.trim().split(";")[0]?.toLowerCase())
59+
.filter(Boolean);
60+
61+
for (const candidate of candidates) {
62+
if (!candidate) continue;
63+
if (supported.has(candidate as PayloadLocale)) {
64+
return candidate as PayloadLocale;
65+
}
66+
67+
const [base] = candidate.split("-");
68+
if (base && supported.has(base as PayloadLocale)) {
69+
return base as PayloadLocale;
70+
}
71+
}
72+
73+
return "en";
74+
};
75+
4376
export default async function RootLayout(props: { children: React.ReactNode }) {
4477
const { children } = props;
4578

4679
const { subdomain } = await getDomain();
4780
const tenant = await getTenantBySubDomain(subdomain);
48-
const locale = resolveTenantLocale(tenant);
81+
const locale = tenant
82+
? resolveTenantLocale(tenant)
83+
: await resolveBrowserLocale();
4984

5085
return (
5186
<html

src/blocks/ActNow/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,24 @@ export const ActNow: Block = {
1111
image({
1212
name: "logo",
1313
required: true,
14+
localized: true,
1415
}),
1516
{
1617
name: "description",
1718
type: "textarea",
1819
required: true,
20+
localized: true,
1921
},
2022
image({
2123
name: "image",
2224
required: true,
25+
localized: true,
2326
}),
2427
link({
2528
appearances: false,
2629
overrides: {
2730
required: true,
31+
localized: true,
2832
},
2933
}),
3034
],

0 commit comments

Comments
 (0)