diff --git a/apps/codeforafrica/jest.setup.js b/apps/codeforafrica/jest.setup.js index d477634b5..646b9d21c 100644 --- a/apps/codeforafrica/jest.setup.js +++ b/apps/codeforafrica/jest.setup.js @@ -17,6 +17,8 @@ global.TextDecoder = jest.fn().mockImplementation(() => ({ decode: jest.fn(), })); +process.env.NEXT_PUBLIC_APP_URL = "http://localhost:3000"; + jest.mock("next/router", () => ({ useRouter: jest.fn().mockImplementation(() => ({ asPath: "", diff --git a/apps/codeforafrica/payload.config.ts b/apps/codeforafrica/payload.config.ts index 4f9e02573..5f5f8927d 100644 --- a/apps/codeforafrica/payload.config.ts +++ b/apps/codeforafrica/payload.config.ts @@ -1,13 +1,15 @@ import path from "path"; import { buildConfig } from "payload/config"; +import Authors from "./src/payload/collections/Authors"; import GuidingPrinciples from "./src/payload/collections/GuidingPrinciples"; import Impact from "./src/payload/collections/Impact"; import Media from "./src/payload/collections/Media"; +import Members from "./src/payload/collections/Members"; import Pages from "./src/payload/collections/Pages"; import Partners from "./src/payload/collections/Partners"; import Settings from "./src/payload/globals/Settings"; -import Members from "./src/payload/collections/Members"; +import Tags from "./src/payload/collections/Tags"; import { CollectionConfig, GlobalConfig } from "payload/types"; import dotenv from "dotenv"; import seo from "@payloadcms/plugin-seo"; @@ -44,12 +46,14 @@ const adapter = s3Adapter({ export default buildConfig({ serverURL: appURL, collections: [ + Authors, GuidingPrinciples, Impact, + Members, Pages, Media, Partners, - Members, + Tags, ] as CollectionConfig[], globals: [Settings] as GlobalConfig[], admin: { @@ -79,7 +83,7 @@ export default buildConfig({ }, }), seo({ - collections: [], + collections: ["pages"], globals: [], uploadsCollection: "media", generateTitle: ({ doc }: any) => doc?.title?.value as string, diff --git a/apps/codeforafrica/src/lib/data/common/index.js b/apps/codeforafrica/src/lib/data/common/index.js index 3fb635c49..0fad1a51b 100644 --- a/apps/codeforafrica/src/lib/data/common/index.js +++ b/apps/codeforafrica/src/lib/data/common/index.js @@ -1,5 +1,6 @@ import blockify from "@/codeforafrica/lib/data/blockify"; import pagify from "@/codeforafrica/lib/data/pagify"; +import getPageSeoFromMeta from "@/codeforafrica/lib/data/seo"; import { imageFromMedia } from "@/codeforafrica/lib/data/utils"; function getNavBar(settings) { @@ -161,10 +162,12 @@ export async function getPageProps(api, context) { const navbar = getNavBar(settings); const footer = getFooter(settings); + const seo = getPageSeoFromMeta(page, settings); return { blocks, footer, navbar, + seo, }; } diff --git a/apps/codeforafrica/src/lib/data/seo.js b/apps/codeforafrica/src/lib/data/seo.js new file mode 100644 index 000000000..3de731194 --- /dev/null +++ b/apps/codeforafrica/src/lib/data/seo.js @@ -0,0 +1,67 @@ +import site from "@/codeforafrica/utils/site"; + +function stringifyDescription(description) { + if (!Array.isArray(description)) { + return ""; + } + return description.reduce((result, item) => { + if (item.text) { + // eslint-disable-next-line no-param-reassign + result += item.text; + } + + if (Array.isArray(item.children)) { + // eslint-disable-next-line no-param-reassign + result += stringifyDescription(item.children); + } + return result; + }, ""); +} + +export default function getPageSeoFromMeta(page, settings) { + const { title: pageTitle, meta: pageMeta } = page; + const { + title: metaTitle, + description: metaDescription, + image = {}, + } = pageMeta; + const { title: siteTitle, description: siteDescription } = settings; + const title = + metaTitle || + pageTitle || + siteTitle || + process.env.NEXT_PUBLIC_APP_NAME || + null; + const description = + metaDescription || stringifyDescription(siteDescription) || null; + const titleTemplate = siteTitle ? `%s | ${siteTitle}` : null; + const defaultTitle = siteTitle || null; + const canonical = site.url.replace(/\/+$/, ""); + const openGraph = { + title, + description, + type: "website", + site_name: siteTitle, + }; + if (image.url) { + const { alt, height, mimeType: type, url, width } = image; + openGraph.images = [ + { + alt: alt || title || defaultTitle, + height, + type, + url, + width, + }, + ]; + } + + return { + title, + titleTemplate, + defaultTitle, + description, + canonical, + openGraph, + }; +} diff --git a/apps/codeforafrica/src/payload/blocks/ExternalEmbed.js b/apps/codeforafrica/src/payload/blocks/ExternalEmbed.js new file mode 100644 index 000000000..fd89a9b68 --- /dev/null +++ b/apps/codeforafrica/src/payload/blocks/ExternalEmbed.js @@ -0,0 +1,61 @@ +import blockFields from "../fields/blockFields"; + +const ExternalEmbed = { + slug: "external-embed", + fields: [ + blockFields({ + name: "embedBlockFields", + fields: [ + { + type: "row", + fields: [ + { + name: "embedType", + type: "radio", + defaultValue: "url", + options: [ + { + label: "URL", + value: "url", + }, + { + label: "Code", + value: "code", + }, + ], + }, + ], + }, + { + name: "url", + label: "URL", + type: "text", + required: true, + admin: { + condition: (_, siblingData) => siblingData?.embedType === "url", + }, + }, + { + name: "caption", + label: "Caption", + type: "text", + localized: true, + admin: { + condition: (_, siblingData) => siblingData?.embedType === "url", + }, + }, + { + name: "code", + label: "Code", + type: "code", + required: true, + admin: { + condition: (_, siblingData) => siblingData?.embedType === "code", + }, + }, + ], + }), + ], +}; + +export default ExternalEmbed; diff --git a/apps/codeforafrica/src/payload/blocks/MediaBlock.js b/apps/codeforafrica/src/payload/blocks/MediaBlock.js new file mode 100644 index 000000000..08b2530a6 --- /dev/null +++ b/apps/codeforafrica/src/payload/blocks/MediaBlock.js @@ -0,0 +1,23 @@ +import blockFields from "../fields/blockFields"; + +const MediaBlock = { + slug: "mediaBlock", + fields: [ + blockFields({ + name: "mediaBlockFields", + fields: [ + { + name: "image", + type: "upload", + relationTo: "media", + required: true, + filterOptions: { + mimeType: { contains: "image" }, + }, + }, + ], + }), + ], +}; + +export default MediaBlock; diff --git a/apps/codeforafrica/src/payload/blocks/RichText.js b/apps/codeforafrica/src/payload/blocks/RichText.js new file mode 100644 index 000000000..ae19f33fc --- /dev/null +++ b/apps/codeforafrica/src/payload/blocks/RichText.js @@ -0,0 +1,32 @@ +import blockFields from "../fields/blockFields"; +import richText from "../fields/richText"; + +const RichText = { + slug: "richText", + fields: [ + blockFields({ + name: "richTextBlockFields", + fields: [ + richText({ + name: "content", + admin: { + elements: [ + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "link", + "ol", + "ul", + "indent", + ], + }, + }), + ], + }), + ], +}; + +export default RichText; diff --git a/apps/codeforafrica/src/payload/collections/Authors.js b/apps/codeforafrica/src/payload/collections/Authors.js new file mode 100644 index 000000000..5967d4152 --- /dev/null +++ b/apps/codeforafrica/src/payload/collections/Authors.js @@ -0,0 +1,20 @@ +const Authors = { + slug: "author", + access: { + read: () => true, + }, + admin: { + useAsTitle: "fullName", + }, + fields: [ + { + name: "fullName", + type: "text", + label: "Full Name", + localized: false, + required: true, + }, + ], +}; + +export default Authors; diff --git a/apps/codeforafrica/src/payload/collections/Tags.js b/apps/codeforafrica/src/payload/collections/Tags.js new file mode 100644 index 000000000..0f3cf7fe2 --- /dev/null +++ b/apps/codeforafrica/src/payload/collections/Tags.js @@ -0,0 +1,24 @@ +import slug from "../fields/slug"; + +const Tags = { + slug: "tag", + admin: { + useAsTitle: "name", + }, + access: { + read: () => true, + }, + fields: [ + { + name: "name", + label: "Name", + type: "text", + localized: true, + required: true, + unique: true, + }, + slug({ fieldToUse: "name" }), + ], +}; + +export default Tags; diff --git a/apps/codeforafrica/src/payload/fields/authors.js b/apps/codeforafrica/src/payload/fields/authors.js new file mode 100644 index 000000000..7d29076b3 --- /dev/null +++ b/apps/codeforafrica/src/payload/fields/authors.js @@ -0,0 +1,17 @@ +import { deepmerge } from "@mui/utils"; + +const authors = (overrides) => + deepmerge( + { + name: "authors", + type: "relationship", + relationTo: "author", + hasMany: true, + admin: { + position: "sidebar", + }, + }, + overrides, + ); + +export default authors; diff --git a/apps/codeforafrica/src/payload/fields/blockFields.js b/apps/codeforafrica/src/payload/fields/blockFields.js new file mode 100644 index 000000000..55188a7c5 --- /dev/null +++ b/apps/codeforafrica/src/payload/fields/blockFields.js @@ -0,0 +1,21 @@ +import { deepmerge } from "@mui/utils"; + +const blockFields = ({ name, fields, overrides }) => + deepmerge( + { + name, + label: false, + type: "group", + admin: { + hideGutter: true, + style: { + margin: 0, + padding: 0, + }, + }, + fields, + }, + overrides, + ); + +export default blockFields; diff --git a/apps/codeforafrica/src/payload/fields/content.js b/apps/codeforafrica/src/payload/fields/content.js new file mode 100644 index 000000000..893bfaf00 --- /dev/null +++ b/apps/codeforafrica/src/payload/fields/content.js @@ -0,0 +1,18 @@ +import { deepmerge } from "@mui/utils"; + +import ExternalEmbed from "../blocks/ExternalEmbed"; +import MediaBlock from "../blocks/MediaBlock"; +import RichText from "../blocks/RichText"; + +const content = (overrides) => + deepmerge( + { + name: "content", + type: "blocks", + blocks: [RichText, MediaBlock, ExternalEmbed], + localized: true, + }, + overrides, + ); + +export default content; diff --git a/apps/codeforafrica/src/payload/fields/publishedOn.js b/apps/codeforafrica/src/payload/fields/publishedOn.js new file mode 100644 index 000000000..f735543a8 --- /dev/null +++ b/apps/codeforafrica/src/payload/fields/publishedOn.js @@ -0,0 +1,22 @@ +import { deepmerge } from "@mui/utils"; + +const publishedOn = (overrides) => + deepmerge( + { + name: "publishedOn", + type: "date", + required: true, + hooks: { + beforeValidate: [({ value }) => (value ? new Date(value) : new Date())], + }, + admin: { + date: { + pickerAppearance: "dayAndTime", + }, + position: "sidebar", + }, + }, + overrides, + ); + +export default publishedOn; diff --git a/apps/codeforafrica/src/payload/fields/tags.js b/apps/codeforafrica/src/payload/fields/tags.js new file mode 100644 index 000000000..d38440d6f --- /dev/null +++ b/apps/codeforafrica/src/payload/fields/tags.js @@ -0,0 +1,14 @@ +import { deepmerge } from "@mui/utils"; + +const tags = (overrides) => { + const field = { + name: "tags", + required: true, + type: "relationship", + relationTo: "tag", + hasMany: true, + }; + return deepmerge(field, overrides); +}; + +export default tags; diff --git a/apps/codeforafrica/turbo.json b/apps/codeforafrica/turbo.json index 650d86ac6..854195e02 100644 --- a/apps/codeforafrica/turbo.json +++ b/apps/codeforafrica/turbo.json @@ -6,7 +6,7 @@ }, "build-next": { "outputs": [".next/**", "!.next/cache/**", "dist/**"], - "env": [] + "env": ["NEXT_PUBLIC_APP_NAME", "NEXT_PUBLIC_APP_URL"] }, "build-payload": { "outputs": ["build/**"],