Skip to content

Commit c7bd3af

Browse files
Merge pull request #467 from CodeForAfrica/ft/ui-fixes-2
feat(SEO): SEO Setup
2 parents d6fc851 + def9eeb commit c7bd3af

File tree

22 files changed

+1246
-311
lines changed

22 files changed

+1246
-311
lines changed

package.json

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,18 @@
3232
"@mui/material-nextjs": "^7.3.2",
3333
"@mui/utils": "^7.3.2",
3434
"@next/third-parties": "^15.5.3",
35-
"@payloadcms/db-mongodb": "3.56.0",
36-
"@payloadcms/email-nodemailer": "^3.56.0",
37-
"@payloadcms/next": "3.56.0",
38-
"@payloadcms/payload-cloud": "3.56.0",
39-
"@payloadcms/plugin-cloud-storage": "^3.56.0",
40-
"@payloadcms/plugin-multi-tenant": "^3.56.0",
41-
"@payloadcms/plugin-sentry": "^3.56.0",
42-
"@payloadcms/richtext-lexical": "3.56.0",
43-
"@payloadcms/storage-s3": "^3.56.0",
44-
"@payloadcms/translations": "^3.56.0",
45-
"@payloadcms/ui": "3.56.0",
35+
"@payloadcms/db-mongodb": "3.58.0",
36+
"@payloadcms/email-nodemailer": "^3.58.0",
37+
"@payloadcms/next": "3.58.0",
38+
"@payloadcms/payload-cloud": "3.58.0",
39+
"@payloadcms/plugin-cloud-storage": "^3.58.0",
40+
"@payloadcms/plugin-multi-tenant": "^3.58.0",
41+
"@payloadcms/plugin-sentry": "^3.58.0",
42+
"@payloadcms/plugin-seo": "^3.58.0",
43+
"@payloadcms/richtext-lexical": "3.58.0",
44+
"@payloadcms/storage-s3": "^3.58.0",
45+
"@payloadcms/translations": "^3.58.0",
46+
"@payloadcms/ui": "3.58.0",
4647
"@sentry/nextjs": "^10.6.0",
4748
"@svgr/webpack": "^8.1.0",
4849
"ai": "^5.0.26",
@@ -55,7 +56,7 @@
5556
"graphql": "^16.11.0",
5657
"lucide-react": "^0.542.0",
5758
"next": "15.4.7",
58-
"payload": "3.56.0",
59+
"payload": "3.58.0",
5960
"react": "19.1.1",
6061
"react-share": "^5.2.2",
6162
"require-in-the-middle": "^7.5.2",

pnpm-lock.yaml

Lines changed: 134 additions & 110 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/favicon.ico

14.7 KB
Binary file not shown.

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

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,102 @@
11
import React, { Suspense } from "react";
2+
import type { Metadata } from "next";
3+
import { notFound, redirect } from "next/navigation";
24

35
import { getGlobalPayload, queryPageBySlug } from "@/lib/payload";
4-
import { notFound, redirect } from "next/navigation";
56
import { getDomain } from "@/lib/domain";
67
import { CommonHomePage } from "@/components/CommonHomePage";
78
import { BlockRenderer } from "@/components/BlockRenderer";
89
import Navigation from "@/components/Navigation";
910
import Footer from "@/components/Footer";
1011
import { getTenantBySubDomain, getTenantNavigation } from "@/lib/data/tenants";
1112
import { getPoliticalEntitiesByTenant } from "@/lib/data/politicalEntities";
13+
import {
14+
buildSeoMetadata,
15+
getEntitySeo,
16+
getPageSeo,
17+
resolveTenantSeoContext,
18+
} from "@/lib/seo";
1219

1320
type Args = {
1421
params: Promise<{
1522
slugs?: string[];
1623
}>;
1724
};
1825

26+
export async function generateMetadata({ params }: Args): Promise<Metadata> {
27+
const paramsValue = await params;
28+
const slugs = paramsValue?.slugs ?? [];
29+
30+
const { subdomain } = await getDomain();
31+
const tenantResolution = await resolveTenantSeoContext(subdomain);
32+
33+
if (tenantResolution.status === "missing") {
34+
return tenantResolution.metadata;
35+
}
36+
37+
const {
38+
tenant,
39+
tenantSettings,
40+
tenantSeo,
41+
tenantTitleBase,
42+
} = tenantResolution.context;
43+
44+
const politicalEntities = await getPoliticalEntitiesByTenant(tenant);
45+
const [maybePoliticalEntitySlug, pageSlugCandidate] = slugs;
46+
const politicalEntity = politicalEntities.find(
47+
(entity) => entity.slug === maybePoliticalEntitySlug,
48+
);
49+
50+
if (!politicalEntity) {
51+
return buildSeoMetadata({
52+
meta: tenantSettings?.meta,
53+
defaults: tenantSeo,
54+
});
55+
}
56+
57+
const { seo: entitySeo, positionRegion } = getEntitySeo({
58+
entity: politicalEntity,
59+
tenant,
60+
tenantSeo,
61+
tenantTitleBase,
62+
});
63+
64+
const pageSlug = pageSlugCandidate ?? "index";
65+
const page = await queryPageBySlug({ slug: pageSlug, tenant });
66+
67+
if (!page) {
68+
return buildSeoMetadata({
69+
meta: politicalEntity.meta,
70+
defaults: entitySeo,
71+
});
72+
}
73+
74+
const { defaults: pageSeoDefaults, meta: pageSeoMeta } = getPageSeo({
75+
page,
76+
entity: politicalEntity,
77+
tenantSeo,
78+
entitySeo,
79+
positionRegion,
80+
});
81+
82+
return buildSeoMetadata({
83+
meta: pageSeoMeta ?? page.meta,
84+
defaults: pageSeoDefaults,
85+
});
86+
}
87+
1988
export default async function Page(params: Args) {
2089
const { subdomain, tenantSelectionHref } = await getDomain();
2190

2291
const tenant = await getTenantBySubDomain(subdomain);
2392

2493
if (!tenant) {
94+
if (subdomain) {
95+
const target = tenantSelectionHref || "/";
96+
if (target !== "/") {
97+
redirect(target);
98+
}
99+
}
25100
return <CommonHomePage />;
26101
}
27102

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

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import React from "react";
22
import NextLink from "next/link";
33
import Image from "next/image";
4-
import { notFound } from "next/navigation";
4+
import type { Metadata } from "next";
5+
import { notFound, redirect } from "next/navigation";
56
import { Box, Button, Container, Grid, Typography } from "@mui/material";
67

78
import Navigation from "@/components/Navigation";
@@ -14,6 +15,12 @@ import { getTenantBySubDomain, getTenantNavigation } from "@/lib/data/tenants";
1415
import { getPoliticalEntityBySlug } from "@/lib/data/politicalEntities";
1516
import { getPromiseById } from "@/lib/data/promises";
1617
import { resolveMedia } from "@/lib/data/media";
18+
import {
19+
buildSeoMetadata,
20+
getEntitySeo,
21+
composeTitleSegments,
22+
resolveTenantSeoContext,
23+
} from "@/lib/seo";
1724
import type {
1825
Promise as PromiseDocument,
1926
PromiseStatus as PromiseStatusDocument,
@@ -27,6 +34,89 @@ type Params = {
2734
promiseId: string;
2835
};
2936

37+
export async function generateMetadata({
38+
params,
39+
}: {
40+
params: Promise<Params>;
41+
}): Promise<Metadata> {
42+
const paramsValue = await params;
43+
const { entitySlug, promiseId } = paramsValue;
44+
45+
const { subdomain } = await getDomain();
46+
const tenantResolution = await resolveTenantSeoContext(subdomain);
47+
48+
if (tenantResolution.status === "missing") {
49+
return tenantResolution.metadata;
50+
}
51+
52+
const {
53+
tenant,
54+
tenantSettings,
55+
tenantSeo,
56+
tenantTitleBase,
57+
} = tenantResolution.context;
58+
59+
const politicalEntity = await getPoliticalEntityBySlug(tenant, entitySlug);
60+
61+
if (!politicalEntity) {
62+
return buildSeoMetadata({
63+
meta: tenantSettings?.meta,
64+
defaults: tenantSeo,
65+
});
66+
}
67+
68+
const { seo: entitySeo, positionRegion } = getEntitySeo({
69+
entity: politicalEntity,
70+
tenant,
71+
tenantSeo,
72+
tenantTitleBase,
73+
});
74+
75+
const promise = await getPromiseById(promiseId);
76+
77+
if (!promise) {
78+
return buildSeoMetadata({
79+
meta: politicalEntity.meta,
80+
defaults: entitySeo,
81+
});
82+
}
83+
84+
const relation = promise.politicalEntity;
85+
const promiseEntityId =
86+
typeof relation === "string" ? relation : relation?.id ?? null;
87+
88+
if (!promiseEntityId || promiseEntityId !== politicalEntity.id) {
89+
return buildSeoMetadata({
90+
meta: politicalEntity.meta,
91+
defaults: entitySeo,
92+
});
93+
}
94+
95+
const fallbackTitle =
96+
composeTitleSegments(
97+
promise.title?.trim() || tenantTitleBase || tenantSeo.title,
98+
politicalEntity.name,
99+
positionRegion,
100+
) ??
101+
entitySeo.title ??
102+
tenantSeo.title;
103+
104+
return buildSeoMetadata({
105+
meta: promise.meta,
106+
defaults: {
107+
title: fallbackTitle,
108+
description:
109+
promise.description?.trim() ||
110+
entitySeo.description ||
111+
tenantSeo.description,
112+
image:
113+
promise.image ??
114+
entitySeo.image ??
115+
tenantSeo.image,
116+
},
117+
});
118+
}
119+
30120
const parseYear = (value?: string | null): number | null => {
31121
if (!value) {
32122
return null;
@@ -87,6 +177,12 @@ export default async function PromiseDetailPage({
87177
const tenant = await getTenantBySubDomain(subdomain);
88178

89179
if (!tenant) {
180+
if (subdomain) {
181+
const target = tenantSelectionHref || "/";
182+
if (target !== "/") {
183+
redirect(target);
184+
}
185+
}
90186
return <CommonHomePage />;
91187
}
92188

src/app/(frontend)/layout.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { AppRouterCacheProvider } from "@mui/material-nextjs/v15-appRouter";
33
import theme from "@/theme/theme";
44
import { ThemeProvider } from "@mui/material";
55
import { Amiri, Open_Sans, Source_Sans_3 } from "next/font/google";
6+
import type { Metadata } from "next";
67

78
const amiri = Amiri({
89
subsets: ["arabic", "latin"],
@@ -27,9 +28,13 @@ const sourceSans = Source_Sans_3({
2728
variable: "--font-source-sans",
2829
});
2930

30-
export const metadata = {
31+
export const metadata: Metadata = {
3132
description: "PromiseTracker",
3233
title: "PromiseTracker",
34+
icons: {
35+
icon: "/favicon.ico",
36+
shortcut: "/favicon.ico",
37+
},
3338
};
3439

3540
export default async function RootLayout(props: { children: React.ReactNode }) {

src/app/(payload)/admin/importMap.js

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/blocks/Newsletter/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Block } from "payload";
44
export const Newsletter: Block = {
55
slug: "newsletter",
66
imageURL: "/cms/newsletter.png",
7+
interfaceName: "NewsletterBlock",
78
labels: {
89
singular: "Newsletter",
910
plural: "Newsletters",

src/collections/Pages/hooks/newsletterSettingsToBlock.ts

Lines changed: 0 additions & 53 deletions
This file was deleted.

src/collections/Pages/index.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { CollectionConfig } from "payload";
22
import { ensureUniqueSlug } from "./hooks/ensureUniqueSlug";
3-
import newsletterSettingsToBlock from "./hooks/newsletterSettingsToBlock";
43
import Partners from "@/blocks/Partners";
54
import Newsletter from "@/blocks/Newsletter";
65
import LatestPromises from "@/blocks/LatestPromises";
@@ -54,7 +53,4 @@ export const Pages: CollectionConfig = {
5453
],
5554
},
5655
],
57-
hooks: {
58-
afterRead: [newsletterSettingsToBlock],
59-
},
6056
};

0 commit comments

Comments
 (0)