Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sanity metadata #62

Merged
merged 4 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/foundation/app/(Sanity)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { ReactNode } from 'react';
import '@session/ui/styles';
import type { Metadata } from 'next';

export const metadata: Metadata = {
title: 'STF | Sanity Studio',
};

export default function SanityLayout({ children }: { children: ReactNode }) {
return (
Expand Down
64 changes: 63 additions & 1 deletion apps/foundation/app/(Site)/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import { getLandingPageSlug } from '@/lib/sanity/sanity-server';
import PortableText from '@/components/PortableText';
import logger from '@/lib/logger';
import { NEXTJS_EXPLICIT_IGNORED_ROUTES, NEXTJS_IGNORED_PATTERNS } from '@/lib/constants';
import type { Metadata, ResolvingMetadata } from 'next';
import { generateSanityMetadata } from '@session/sanity-cms/lib/metadata';
import { getFileBySlug } from '@session/sanity-cms/queries/getFile';
import FileDownload from '@session/sanity-cms/components/SanityFileDownload';
import { getTranslations } from 'next-intl/server';

/**
* Force static rendering and cache the data of a layout or page by forcing `cookies()`, `headers()`
Expand All @@ -19,6 +24,39 @@ export const dynamic = 'force-static';
*/
export const dynamicParams = true;

export async function generateMetadata(
{ params }: PageProps,
parent: ResolvingMetadata
): Promise<Metadata> {
const slug = params.slug;
if (!slug) {
logger.warn(`No slug provided for metadata generation`);
return {};
}

if (
NEXTJS_EXPLICIT_IGNORED_ROUTES.includes(slug) ||
NEXTJS_IGNORED_PATTERNS.some((pattern) => slug.includes(pattern))
) {
return {};
}

logger.info(`Generating metadata for slug ${slug}`);

const page = await getPageBySlug({ client, slug });

if (!page) {
logger.warn(`No page found for slug ${slug}`);
return {};
}
const parentMetadata = await parent;
return generateSanityMetadata(client, {
seo: page.seo,
parentMetadata,
type: 'website',
});
}

export async function generateStaticParams() {
const pages = await getPagesSlugs({ client });
const slugs = new Set(pages.map((page) => page.slug.current));
Expand Down Expand Up @@ -58,7 +96,31 @@ export default async function UniversalPage({ params }: PageProps) {
slug,
});

if (!page) return notFound();
if (!page) {
const file = await getFileBySlug({
client,
slug,
});

if (file?.src && file?.fileName) {
const fileDictionary = await getTranslations('fileDownload');
return (
<FileDownload
fileName={file.fileName}
src={file.src}
strings={{
fetching: fileDictionary('fetching'),
clickToDownload: fileDictionary('clickToDownload'),
clickToDownloadAria: fileDictionary('clickToDownloadAria'),
openPdfInNewTab: fileDictionary('openPdfInNewTab'),
openPdfInNewTabAria: fileDictionary('openPdfInNewTabAria'),
}}
/>
);
}

return notFound();
}

return <PortableText body={page.body} className="max-w-screen-md" wrapperComponent="main" />;
}
36 changes: 36 additions & 0 deletions apps/foundation/app/(Site)/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { getPostsSlugs } from '@session/sanity-cms/queries/getPosts';
import { getPostBySlug } from '@session/sanity-cms/queries/getPost';
import logger from '@/lib/logger';
import BlogPost from '@/app/(Site)/blog/[slug]/BlogPost';
import type { Metadata, ResolvingMetadata } from 'next';
import { generateSanityMetadata } from '@session/sanity-cms/lib/metadata';

/**
* Force static rendering and cache the data of a layout or page by forcing `cookies()`, `headers()`
Expand All @@ -17,6 +19,40 @@ export const dynamic = 'force-static';
*/
export const dynamicParams = true;

export async function generateMetadata(
{ params }: PageProps,
parent: ResolvingMetadata
): Promise<Metadata> {
const slug = params.slug;
if (!slug) {
logger.warn(`No slug provided for metadata generation`);
return {};
}

logger.info(`Generating metadata for slug ${slug}`);

const post = await getPostBySlug({ client, slug });

if (!post) {
logger.warn(`No post found for slug ${slug}`);
return {};
}
const parentMetadata = await parent;

const publishedTime = post.date ? new Date(post.date).toISOString() : undefined;
const authors = post.author?.name ? [post.author.name] : undefined;
const tags = post.tags?.length ? post.tags : undefined;

return generateSanityMetadata(client, {
seo: post.seo,
parentMetadata,
type: 'article',
publishedTime,
authors,
tags,
});
}

export async function generateStaticParams() {
const posts = await getPostsSlugs({ client });
const slugs = new Set(posts.map((post) => post.slug.current));
Expand Down
15 changes: 15 additions & 0 deletions apps/foundation/app/(Site)/blog/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
import type { ReactNode } from 'react';
import { Footer } from '@/components/Footer';
import { getInitialSiteDataForSSR } from '@/lib/sanity/sanity-server';
import type { Metadata, ResolvingMetadata } from 'next';
import { generateSanityMetadata } from '@session/sanity-cms/lib/metadata';
import { client } from '@/lib/sanity/sanity.client';

export async function generateMetadata(_: object, parent: ResolvingMetadata): Promise<Metadata> {
const { settings } = await getInitialSiteDataForSSR();

return settings.blogSeo
? await generateSanityMetadata(client, {
seo: settings.blogSeo,
parentMetadata: await parent,
type: 'website',
})
: {};
}

export default async function BlogLayout({ children }: { children: ReactNode }) {
const { settings } = await getInitialSiteDataForSSR();
Expand Down
20 changes: 19 additions & 1 deletion apps/foundation/app/(Site)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,25 @@ import DevSheetServerSide from '@/components/DevSheetServerSide';
import { getInitialSiteDataForSSR } from '@/lib/sanity/sanity-server';
import Head from 'next/head';
import { isProduction } from '@session/util-js/env';
import { client } from '@/lib/sanity/sanity.client';
import { generateSanityMetadata } from '@session/sanity-cms/lib/metadata';
import type { Metadata } from 'next';

export async function generateMetadata(): Promise<Metadata> {
const { settings } = await getInitialSiteDataForSSR();

const generatedMetadata = settings.seo
? await generateSanityMetadata(client, {
seo: settings.seo,
type: 'website',
})
: {};

return {
...generatedMetadata,
manifest: '/site.webmanifest',
};
}

export default async function RootLayout({ children }: { children: ReactNode }) {
const { locale, direction, messages } = await getLocalizationData();
Expand All @@ -25,7 +44,6 @@ export default async function RootLayout({ children }: { children: ReactNode })
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
</Head>
<GlobalProvider messages={messages} locale={locale}>
<body className="bg-session-white font-roboto-flex text-session-text-black mx-4 flex flex-col items-center overflow-x-hidden">
Expand Down
13 changes: 12 additions & 1 deletion apps/foundation/app/(Site)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getLandingPageSlug } from '@/lib/sanity/sanity-server';
import UniversalPage from './[slug]/page';
import UniversalPage, { generateMetadata as generateMetadataUniversalPage } from './[slug]/page';
import UniversalPageLayout from '@/app/(Site)/[slug]/layout';
import type { ResolvingMetadata } from 'next';

/**
* Force static rendering and cache the data of a layout or page by forcing `cookies()`, `headers()`
Expand All @@ -9,6 +10,16 @@ import UniversalPageLayout from '@/app/(Site)/[slug]/layout';
*/
export const dynamic = 'force-static';

export async function generateMetadata(_: object, parent: ResolvingMetadata) {
const slug = await getLandingPageSlug();

if (!slug) {
throw new Error('No landing page set in settings to generate metadata');
}

return generateMetadataUniversalPage({ params: { slug } }, parent);
}

export default async function LandingPage() {
const slug = await getLandingPageSlug();

Expand Down
3 changes: 2 additions & 1 deletion apps/foundation/lib/sanity/sanity.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createSanityConfig } from '@session/sanity-cms/lib/config';
import { NEXT_PUBLIC_SANITY_DATASET, NEXT_PUBLIC_SANITY_PROJECT_ID } from '@/lib/env';
import {
authorSchema,
fileSchema,
pageSchema,
postSchema,
siteSchema,
Expand All @@ -18,6 +19,6 @@ export const sanityConfig = createSanityConfig({
enableDrafts: SANITY_UTIL_PATH.ENABLE_DRAFT,
disableDrafts: SANITY_UTIL_PATH.DISABLE_DRAFT,
},
schemas: [pageSchema, postSchema, authorSchema, socialSchema, specialSchema],
schemas: [pageSchema, postSchema, authorSchema, socialSchema, specialSchema, fileSchema],
singletonSchemas: [siteSchema],
});
7 changes: 7 additions & 0 deletions apps/foundation/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,12 @@
"inThisArticle": "In this article",
"readMore": "Read more",
"morePosts": "More posts"
},
"fileDownload": {
"fetching": "Fetching '{name'}...",
"clickToDownload": "Click to download",
"clickToDownloadAria": "Click to download file",
"openPdfInNewTab": "Open PDF in new tab",
"openPdfInNewTabAria": "Open PDF in new tab"
}
}
2 changes: 1 addition & 1 deletion packages/sanity-cms/api/validate-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const createValidateLinkHandler = ({
});
}

const linksArray = Array.from(links);
const linksArray = Array.from(links).filter((link) => !link.startsWith('mailto:'));

logger.info(`Generating static params for ${links.size} links`);
logger.info(linksArray);
Expand Down
58 changes: 58 additions & 0 deletions packages/sanity-cms/components/SanityFileDownload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
'use client';

import SanityPdf from './SanityPdf';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { cleanSanityString } from '../lib/string';

type FileDownloadProps = {
fileName: string;
src: string;
strings: {
fetching: string;
clickToDownload: string;
clickToDownloadAria: string;
openPdfInNewTab: string;
openPdfInNewTabAria: string;
};
};

export default function FileDownload({ fileName, src, strings }: FileDownloadProps) {
const [downloaded, setDownloaded] = useState(false);
const router = useRouter();
if (!src || !fileName) return null;
yougotwill marked this conversation as resolved.
Show resolved Hide resolved

const name = cleanSanityString(fileName);
const srcWithParams = new URL(src);
srcWithParams.searchParams.set('dl', name);

if (src.includes('.pdf')) {
return <SanityPdf src={src} url={srcWithParams} strings={strings} />;
}

// Download file on mount
useEffect(() => {
if (!downloaded) {
setDownloaded(true);
void router.push(srcWithParams.href);
}
}, [src]);

return (
<div className="my-12 flex flex-col items-center justify-center gap-2">
<p className="text-center text-sm">{strings.fetching.replace('{name}', fileName)}</p>
<a
href={srcWithParams.href}
className="group"
target="_blank"
rel="noopener noreferrer"
download
aria-label={strings.clickToDownloadAria}
>
<button className="group-hover:decoration-session-green hover:decoration-session-green decoration-session-black mt-1 w-max text-sm underline group-hover:underline">
{strings.clickToDownload}
</button>
</a>
</div>
);
}
11 changes: 10 additions & 1 deletion packages/sanity-cms/components/SanityImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,25 @@ import type {
import { cn } from '@session/ui/lib/utils';
import { safeTry } from '@session/util-js/try';
import { Fragment } from 'react';
import type { SanityImageSource } from '@sanity/asset-utils';
import type { CustomImageType } from '../schemas/fields/basic/seo';

export type SanityImageType = ImageFieldsSchemaType | ImageFieldsSchemaTypeWithoutAltText;

export const getSanityImageUrlBuilder = (
client: SessionSanityClient,
value: SanityImageType | SanityImageSource | CustomImageType
) => {
return urlBuilder(client).image(value).fit('max').auto('format');
};

/**
* Build image data from Sanity image schema and the image source.
* @param client Sanity client
* @param value Sanity image schema
*/
async function buildImage(client: SessionSanityClient, value: SanityImageType) {
const src = urlBuilder(client).image(value).fit('max').auto('format').url();
const src = getSanityImageUrlBuilder(client, value).url();
const buffer = await fetch(src).then(async (res) => Buffer.from(await res.arrayBuffer()));

const {
Expand Down
33 changes: 33 additions & 0 deletions packages/sanity-cms/components/SanityPdf.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { NavLink } from '@session/ui/components/NavLink';
import { LinkOutIcon } from '@session/ui/icons/LinkOutIcon';

export type SanityPdfProps = {
src: string;
url: URL;
strings: {
openPdfInNewTab: string;
openPdfInNewTabAria: string;
};
};

export default function SanityPdf({ src, url, strings }: SanityPdfProps) {
if (!url || !src) return null;
yougotwill marked this conversation as resolved.
Show resolved Hide resolved

return (
<main className="flex h-full min-h-screen w-full max-w-3xl flex-col items-center justify-center gap-4">
<NavLink
href={url.href}
aria-label={strings.openPdfInNewTabAria}
className="inline-flex items-center justify-center gap-2 align-middle"
>
{strings.openPdfInNewTab} <LinkOutIcon className="h-4 w-4" />
</NavLink>
<iframe
src={src}
className="h-full min-h-screen w-full"
style={{ border: 'none' }}
title="PDF"
/>
</main>
);
}
Loading
Loading