Skip to content

Commit

Permalink
Demo: add site metadata generation (#3268)
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasdax98 authored Jan 28, 2025
1 parent 86c1d59 commit a8cd424
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 67 deletions.
20 changes: 13 additions & 7 deletions demo/site/src/app/[domain]/[language]/[[...path]]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,15 @@ import { GQLLayoutQuery, GQLLayoutQueryVariables } from "@src/app/[domain]/[lang
import { Footer } from "@src/layout/footer/Footer";
import { footerFragment } from "@src/layout/footer/Footer.fragment";
import { createGraphQLFetch } from "@src/util/graphQLClient";
import { getSiteConfigForDomain } from "@src/util/siteConfig";
import type { Metadata } from "next";
import { PropsWithChildren } from "react";

export const metadata: Metadata = {
title: "Comet Starter",
};
interface LayoutProps {
params: { domain: string; language: string };
}

export default async function Layout({
children,
params: { domain, language },
}: PropsWithChildren<{ params: { domain: string; language: string } }>) {
export default async function Layout({ children, params: { domain, language } }: PropsWithChildren<LayoutProps>) {
const { previewData } = (await previewParams()) || { previewData: undefined };
const graphqlFetch = createGraphQLFetch(previewData);

Expand All @@ -37,3 +35,11 @@ export default async function Layout({
</>
);
}

export async function generateMetadata({ params }: LayoutProps): Promise<Metadata> {
const siteConfig = getSiteConfigForDomain(params.domain);

return {
metadataBase: new URL(siteConfig.url),
};
}
77 changes: 41 additions & 36 deletions demo/site/src/app/[domain]/[language]/[[...path]]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { gql, previewParams } from "@comet/cms-site";
import { ExternalLinkBlockData, InternalLinkBlockData, RedirectsLinkBlockData } from "@src/blocks.generated";
import { documentTypes } from "@src/documents";
import { GQLPageTreeNodeScope, GQLPageTreeNodeScopeInput } from "@src/graphql.generated";
import { GQLPageTreeNodeScope } from "@src/graphql.generated";
import { createGraphQLFetch } from "@src/util/graphQLClient";
import { getSiteConfigForDomain } from "@src/util/siteConfig";
import type { Metadata, ResolvingMetadata } from "next";
import { Metadata, ResolvingMetadata } from "next";
import { notFound, redirect } from "next/navigation";

import { GQLDocumentTypeQuery, GQLDocumentTypeQueryVariables } from "./page.generated";
Expand All @@ -27,54 +27,38 @@ const documentTypeQuery = gql`
}
`;

async function fetchPageTreeNode({ domain, language, path }: Props["params"]) {
const skipPage = !getSiteConfigForDomain(domain).scope.languages.includes(language);
const { scope, previewData } = (await previewParams()) || { scope: { domain, language }, previewData: undefined };
async function fetchPageTreeNode(params: { path: string[]; domain: string; language: string }) {
const { previewData } = (await previewParams()) || { previewData: undefined };
const siteConfig = getSiteConfigForDomain(params.domain);

// Redirects are scoped by domain only, not by language.
// If the language param isn't a valid language, it may still be the first segment of a redirect source.
// In that case we skip resolving page and only check if the path is a redirect source.
const skipPage = !siteConfig.scope.languages.includes(params.language);

const path = `/${(params.path ?? []).join("/")}`;
const { scope } = { scope: { domain: params.domain, language: params.language } };
const graphQLFetch = createGraphQLFetch(previewData);
const pathname = `/${(path ?? []).join("/")}`;

return graphQLFetch<GQLDocumentTypeQuery, GQLDocumentTypeQueryVariables>(
documentTypeQuery,
{
skipPage,
path: pathname,
scope: scope as GQLPageTreeNodeScopeInput, //TODO fix type, the scope from previewParams() is not compatible with GQLPageTreeNodeScopeInput
redirectSource: `/${language}${pathname !== "/" ? pathname : ""}`,
path,
scope,
redirectSource: `/${params.language}${path !== "/" ? path : ""}`,
redirectScope: { domain: scope.domain },
},
{ method: "GET" }, //for request memoization
);
}

type Props = {
interface PageProps {
params: { path: string[]; domain: string; language: string };
};

export async function generateMetadata({ params }: Props, parent: ResolvingMetadata): Promise<Metadata> {
// TODO support multiple domains, get domain by Host header
const { scope } = (await previewParams()) || { scope: { domain: params.domain, language: params.language } };

const data = await fetchPageTreeNode(params);

if (!data.pageTreeNodeByPath?.documentType) {
return {};
}

const pageTreeNodeId = data.pageTreeNodeByPath.id;

const props = {
pageTreeNodeId,
scope,
};
const { generateMetadata } = documentTypes[data.pageTreeNodeByPath.documentType];
if (!generateMetadata) return {};

return generateMetadata(props, parent);
}

export default async function Page({ params }: Props) {
// TODO support multiple domains, get domain by Host header
const { scope } = (await previewParams()) || { scope: { domain: params.domain, language: params.language } };

export default async function Page({ params }: PageProps) {
const scope = { domain: params.domain, language: params.language };
const data = await fetchPageTreeNode(params);

if (!data.pageTreeNodeByPath?.documentType) {
Expand Down Expand Up @@ -108,11 +92,32 @@ export default async function Page({ params }: Props) {
pageTreeNodeId,
scope,
};

const { component: Component } = documentTypes[data.pageTreeNodeByPath.documentType];

return <Component {...props} />;
}

export async function generateMetadata({ params }: PageProps, parent: ResolvingMetadata): Promise<Metadata> {
const scope = { domain: params.domain, language: params.language };
const data = await fetchPageTreeNode(params);

if (!data.pageTreeNodeByPath?.documentType) {
return {};
}

const pageTreeNodeId = data.pageTreeNodeByPath.id;

const props = {
pageTreeNodeId,
scope,
};
const { generateMetadata } = documentTypes[data.pageTreeNodeByPath.documentType];
if (!generateMetadata) return {};

return generateMetadata(props, parent);
}

export async function generateStaticParams() {
return [];
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import { useBlockPreviewFetch, useIFrameBridge } from "@comet/cms-site";
import { FooterContentBlockData } from "@src/blocks.generated";
import { FooterContentBlock } from "@src/layout/footer/blocks/FooterContentBlock";
import { recursivelyLoadBlockData } from "@src/recursivelyLoadBlockData";
import { withBlockPreview } from "@src/util/blockPreview";
import { recursivelyLoadBlockData } from "@src/util/recursivelyLoadBlockData";
import { useEffect, useState } from "react";

export default withBlockPreview(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import { useBlockPreviewFetch, useIFrameBridge } from "@comet/cms-site";
import { PageContentBlockData } from "@src/blocks.generated";
import { PageContentBlock } from "@src/documents/pages/blocks/PageContentBlock";
import { recursivelyLoadBlockData } from "@src/recursivelyLoadBlockData";
import { withBlockPreview } from "@src/util/blockPreview";
import { recursivelyLoadBlockData } from "@src/util/recursivelyLoadBlockData";
import { useEffect, useState } from "react";

export default withBlockPreview(() => {
Expand Down
38 changes: 21 additions & 17 deletions demo/site/src/documents/pages/Page.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import { generateImageUrl, gql, previewParams } from "@comet/cms-site";
import Breadcrumbs from "@src/common/components/Breadcrumbs";
import { breadcrumbsFragment } from "@src/common/components/Breadcrumbs.fragment";
import { PageContentBlock } from "@src/documents/pages/blocks/PageContentBlock";
import { StageBlock } from "@src/documents/pages/blocks/StageBlock";
import { GQLPageTreeNodeScopeInput } from "@src/graphql.generated";
import { Header } from "@src/layout/header/Header";
import { headerFragment } from "@src/layout/header/Header.fragment";
import { TopNavigation } from "@src/layout/topNavigation/TopNavigation";
import { topMenuPageTreeNodeFragment } from "@src/layout/topNavigation/TopNavigation.fragment";
import { recursivelyLoadBlockData } from "@src/recursivelyLoadBlockData";
import { createGraphQLFetch } from "@src/util/graphQLClient";
import type { Metadata, ResolvingMetadata } from "next";
import { recursivelyLoadBlockData } from "@src/util/recursivelyLoadBlockData";
import { Metadata, ResolvingMetadata } from "next";
import { notFound } from "next/navigation";

import { PageContentBlock } from "./blocks/PageContentBlock";
import { StageBlock } from "./blocks/StageBlock";
import { GQLPageQuery, GQLPageQueryVariables } from "./Page.generated";

// @TODO: Scope for menu should also be of type PageTreeNodeScopeInput
const pageQuery = gql`
query Page($pageTreeNodeId: ID!, $domain: String!, $language: String!) {
pageContent: pageTreeNode(id: $pageTreeNodeId) {
id
name
path
document {
__typename
... on Page {
Expand All @@ -29,11 +31,9 @@ const pageQuery = gql`
}
...Breadcrumbs
}
header: mainMenu(scope: { domain: $domain, language: $language }) {
...Header
}
topMenu(scope: { domain: $domain, language: $language }) {
...TopMenuPageTreeNode
}
Expand Down Expand Up @@ -107,7 +107,7 @@ export async function generateMetadata({ pageTreeNodeId, scope }: Props, parent:
if (link.code && link.url) acc[link.code] = link.url;
return acc;
},
{ [scope.language]: canonicalUrl },
{ [scope.language]: canonicalUrl } as Record<string, string>,
),
},
};
Expand All @@ -123,22 +123,26 @@ export async function Page({ pageTreeNodeId, scope }: { pageTreeNodeId: string;
// no document attached to page
notFound(); //no return needed
}
if (data.pageContent.document?.__typename != "Page") throw new Error(`invalid document type`);
if (document.__typename != "Page") throw new Error(`invalid document type`);

[data.pageContent.document.content, data.pageContent.document.seo] = await Promise.all([
[document.content, document.seo] = await Promise.all([
recursivelyLoadBlockData({
blockType: "PageContent",
blockData: data.pageContent.document.content,
blockData: document.content,
graphQLFetch,
fetch,
pageTreeNodeId,
}),
recursivelyLoadBlockData({
blockType: "Seo",
blockData: data.pageContent.document.seo,
blockData: document.seo,
graphQLFetch,
fetch,
}),
recursivelyLoadBlockData({
blockType: "Stage",
blockData: document.stage,
graphQLFetch,
fetch,
pageTreeNodeId,
}),
]);

Expand All @@ -150,10 +154,10 @@ export async function Page({ pageTreeNodeId, scope }: { pageTreeNodeId: string;
<TopNavigation data={data.topMenu} />
<Header header={data.header} />
<Breadcrumbs {...data.pageContent} />
<div>
<main>
<StageBlock data={document.stage} />
<PageContentBlock data={data.pageContent.document.content} />
</div>
<PageContentBlock data={document.content} />
</main>
</>
);
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import { BlockLoader, BlockLoaderDependencies, recursivelyLoadBlockData as cometRecursivelyLoadBlockData } from "@comet/cms-site";

import { loader as newsDetailLoader } from "./news/blocks/NewsDetailBlock.loader";
import { loader as newsListLoader } from "./news/blocks/NewsListBlock.loader";
import { loader as newsDetailLoader } from "@src/news/blocks/NewsDetailBlock.loader";
import { loader as newsListLoader } from "@src/news/blocks/NewsListBlock.loader";

declare module "@comet/cms-site" {
export interface BlockLoaderDependencies {
pageTreeNodeId?: string;
}
}

export const blockLoaders: Record<string, BlockLoader> = {
const blockLoaders: Record<string, BlockLoader> = {
NewsDetail: newsDetailLoader,
NewsList: newsListLoader,
};

//small wrapper for @comet/cms-site recursivelyLoadBlockData that injects blockMeta from block-meta.json
export async function recursivelyLoadBlockData(options: { blockType: string; blockData: unknown } & BlockLoaderDependencies) {
const blocksMeta = await import("../block-meta.json"); //dynamic import to avoid this json in client bundle
const blocksMeta = await import("../../block-meta.json"); //dynamic import to avoid this json in client bundle
return cometRecursivelyLoadBlockData({ ...options, blocksMeta: blocksMeta.default, loaders: blockLoaders });
}

0 comments on commit a8cd424

Please sign in to comment.