diff --git a/apps/addon-catalog/app/(home)/tag/[...name]/page.tsx b/apps/addon-catalog/app/(home)/tag/[...name]/page.tsx index 37a839eb..1a73581d 100644 --- a/apps/addon-catalog/app/(home)/tag/[...name]/page.tsx +++ b/apps/addon-catalog/app/(home)/tag/[...name]/page.tsx @@ -1,26 +1,49 @@ +import { type Metadata } from 'next'; import { notFound } from 'next/navigation'; import { Preview } from '../../../../components/preview'; import { fetchTagDetailsData } from '../../../../lib/fetch-tag-details-data'; +import type { Tag } from '../../../../types'; // 60*60*24 = 24 hrs export const revalidate = 86400; +interface Params { + name: string[]; +} + +type GenerateMetaData = (props: { + params: Promise; +}) => Promise; + interface TagDetailsProps { - params: { - name: string[]; - }; + params: Params; } +async function getTagFromName( + tagName: string[], +): Promise { + const name = tagName.join('/'); + return (await fetchTagDetailsData(name)) || {}; +} + +export const generateMetadata: GenerateMetaData = async ({ params }) => { + const tagName = (await params).name.join('/'); + const data = (await fetchTagDetailsData(tagName)) || {}; + + if ('error' in data) return {}; + const { displayName } = data; + + return { + title: displayName ? `${displayName} tag | Storybook integrations` : undefined, + }; +}; + export default async function TagDetails({ params: { name }, }: TagDetailsProps) { - const tagName = name.join('/'); - - if (!tagName) return notFound(); - - const data = (await fetchTagDetailsData(tagName)) || {}; + const data = await getTagFromName(name); - if ('error' in data) return notFound(); + if (!data || 'error' in data) return notFound(); return ( <> diff --git a/apps/addon-catalog/app/[...addonName]/page.tsx b/apps/addon-catalog/app/[...addonName]/page.tsx index 0db06665..ebf22fab 100644 --- a/apps/addon-catalog/app/[...addonName]/page.tsx +++ b/apps/addon-catalog/app/[...addonName]/page.tsx @@ -1,27 +1,51 @@ +import { type Metadata } from 'next'; +import { notFound } from 'next/navigation'; import { SubHeader } from '@repo/ui'; import { cn } from '@repo/utils'; import { fetchAddonDetailsData } from '../../lib/fetch-addon-details-data'; import { AddonHero } from '../../components/addon/addon-hero'; import { AddonSidebar } from '../../components/addon/addon-sidebar'; import { Highlight } from '../../components/highlight'; +import type { AddonWithTagLinks } from '../../types'; // 60*60*24 = 24 hrs export const revalidate = 86400; +interface Params { + addonName: string[]; +} + +type GenerateMetaData = (props: { + params: Promise; +}) => Promise; + interface AddonDetailsProps { - params: { - addonName: string[]; - }; + params: Params; } -export default async function AddonDetails({ params }: AddonDetailsProps) { +async function getAddonFromName( + addonName: string[], +): Promise { // TODO: Better decoding? - const name = params.addonName.join('/').replace('%40', '@'); - const addon = await fetchAddonDetailsData(name); + const name = addonName.join('/').replace('%40', '@'); + return await fetchAddonDetailsData(name); +} + +export const generateMetadata: GenerateMetaData = async ({ params }) => { + const name = (await params).addonName; + const addon = await getAddonFromName(name); + + return { + title: addon?.displayName + ? `${addon.displayName} | Storybook integrations` + : undefined, + }; +}; + +export default async function AddonDetails({ params }: AddonDetailsProps) { + const addon = await getAddonFromName(params.addonName); - if (!addon) { - return
Not found.
; - } + if (!addon) notFound(); return (
diff --git a/apps/addon-catalog/app/layout.tsx b/apps/addon-catalog/app/layout.tsx index 73cfd846..93ff5468 100644 --- a/apps/addon-catalog/app/layout.tsx +++ b/apps/addon-catalog/app/layout.tsx @@ -18,7 +18,7 @@ const fontSans = nunitoSans({ export const metadata: Metadata = { metadataBase: new URL('https://storybook.js.org/integrations'), - title: 'Integrations | Storybook: Frontend workshop for UI development', + title: 'Storybook integrations', description: 'Integrations enable advanced functionality and unlock new workflows. Contributed by core maintainers and the amazing developer community.', openGraph: { diff --git a/apps/frontpage/app/community/page.tsx b/apps/frontpage/app/community/page.tsx index 3f3179d5..423906f3 100644 --- a/apps/frontpage/app/community/page.tsx +++ b/apps/frontpage/app/community/page.tsx @@ -1,3 +1,4 @@ +import type { Metadata } from 'next'; import { Header, Footer, Container, NewsletterForm } from '@repo/ui'; import { fetchDiscordMembers, @@ -18,6 +19,12 @@ import { Sponsor } from '../../components/community/sponsor'; import { Testimonials } from '../../components/community/testimonials'; import { CommunityProvider } from './provider'; +export function generateMetadata(): Metadata { + return { + title: 'Community | Storybook', + }; +} + export default async function Page() { const { number: githubCount, formattedResult: githubCountFormatted } = await fetchGithubCount(); diff --git a/apps/frontpage/app/docs/[...slug]/page.tsx b/apps/frontpage/app/docs/[...slug]/page.tsx index e3313384..daff163e 100644 --- a/apps/frontpage/app/docs/[...slug]/page.tsx +++ b/apps/frontpage/app/docs/[...slug]/page.tsx @@ -1,23 +1,43 @@ +import { type Metadata } from 'next'; import { notFound, redirect } from 'next/navigation'; import { globalSearchMetaKeys, globalSearchImportance } from '@repo/ui'; -import { latestVersion } from '@repo/utils'; -import { type Metadata } from 'next'; +import { type DocsVersion, latestVersion } from '@repo/utils'; import { getVersion } from '../../../lib/get-version'; -import { getPageData } from '../../../lib/get-page'; +import { getPageData, type PageDataProps } from '../../../lib/get-page'; import { getAllTrees } from '../../../lib/get-all-trees'; import { getFlatTree } from '../../../lib/get-flat-tree'; import { Content } from '../../../components/docs/content'; +interface Params { + slug: string[]; +} + +type GenerateMetaData = (props: { + params: Promise; +}) => Promise; + interface PageProps { - params: { - slug?: string[]; - }; + params: Params; } -export async function generateMetadata({ - params: { slug }, -}: PageProps): Promise { +async function getPageFromSlug( + slug: string[], +): Promise<{ activeVersion: DocsVersion; page: PageDataProps | undefined }> { const activeVersion = getVersion(slug); + const isLatest = activeVersion.id === latestVersion.id; + + const slugToFetch = slug ? [...slug] : []; + if (!isLatest) slugToFetch.shift(); + slugToFetch.unshift(activeVersion.id); + + const page = await getPageData(slugToFetch, activeVersion); + return { activeVersion, page }; +} + +export const generateMetadata: GenerateMetaData = async ({ params }) => { + const slug = (await params).slug; + const { activeVersion, page } = await getPageFromSlug(slug); + const listofTrees = getAllTrees(); const flatTree = getFlatTree({ tree: listofTrees, @@ -29,11 +49,8 @@ export async function generateMetadata({ (node) => node.slug === `/docs/${newSlug.join('/')}`, ); - const slugToFetch = slug ? [...slug] : []; - const page = await getPageData(slugToFetch, activeVersion); - return { - title: `${page?.title ?? 'Docs'} | Storybook`, + title: page?.title ? `${page.title} | Storybook docs` : undefined, alternates: { canonical: findPage?.canonical, }, @@ -42,7 +59,7 @@ export async function generateMetadata({ [globalSearchMetaKeys.importance]: globalSearchImportance.docs, }, }; -} +}; export const generateStaticParams = () => { const listofTrees = getAllTrees(); @@ -62,21 +79,15 @@ export const generateStaticParams = () => { }; export default async function Page({ params: { slug } }: PageProps) { - const activeVersion = getVersion(slug); - const isLatest = activeVersion.id === latestVersion.id; - const slugToFetch = slug ? [...slug] : []; - if (!isLatest) slugToFetch.shift(); - slugToFetch.unshift(activeVersion.id); - const newSlug = slug ?? []; - - const page = await getPageData(slugToFetch, activeVersion); - - const isIndex = slug && slug[slug.length - 1] === 'index'; - const pathWithoutIndex = `/docs/${newSlug.slice(0, -1).join('/')}`; - // If the page is an index page, redirect to the parent page - if (isIndex) redirect(pathWithoutIndex); + const isIndex = slug && slug[slug.length - 1] === 'index'; + if (isIndex) { + const newSlug = slug ?? []; + const pathWithoutIndex = `/docs/${newSlug.slice(0, -1).join('/')}`; + redirect(pathWithoutIndex); + } + const { page } = await getPageFromSlug(slug); if (!page) notFound(); return ; diff --git a/apps/frontpage/app/docs/layout.tsx b/apps/frontpage/app/docs/layout.tsx index 5658bed4..f72295c9 100644 --- a/apps/frontpage/app/docs/layout.tsx +++ b/apps/frontpage/app/docs/layout.tsx @@ -2,7 +2,6 @@ import { Header, Footer, Container } from '@repo/ui'; import Image from 'next/image'; import { fetchGithubCount } from '@repo/utils'; import { type ReactNode, Suspense } from 'react'; -import { type Metadata } from 'next'; import { Sidebar } from '../../components/docs/sidebar/sidebar'; import { NavDocs } from '../../components/docs/sidebar/docs-nav'; import { Submenu } from '../../components/docs/submenu'; @@ -12,12 +11,6 @@ import { getAllTrees } from '../../lib/get-all-trees'; import { DocsProvider } from './provider'; import { RendererCookie } from './renderer-cookie'; -export function generateMetadata(): Metadata { - return { - title: 'Docs | Storybook', - }; -} - export default async function Layout({ children }: { children: ReactNode }) { const { number: githubCount } = await fetchGithubCount(); const listofTrees = getAllTrees(); @@ -39,7 +32,7 @@ export default async function Layout({ children }: { children: ReactNode }) { Storybook Docs { + const page = await getPageData([latestVersion.id], latestVersion); + return { + title: page?.title ? `${page.title} | Storybook docs` : undefined, alternates: { canonical: '/docs', }, diff --git a/apps/frontpage/app/recipes/[...name]/page.tsx b/apps/frontpage/app/recipes/[...name]/page.tsx index 7a51ffb6..8b3ce057 100644 --- a/apps/frontpage/app/recipes/[...name]/page.tsx +++ b/apps/frontpage/app/recipes/[...name]/page.tsx @@ -1,5 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; +import { type Metadata } from 'next'; +import { notFound } from 'next/navigation'; import { Container, Footer, @@ -15,13 +17,51 @@ import { MDXRemote } from 'next-mdx-remote/rsc'; import { fetchRecipeDetailsData } from '../../../lib/fetch-recipe-details-data'; import { fetchRecipesData } from '../../../lib/fetch-recipes-data'; import { EmbeddedExample } from '../../../components/recipes/embedded-example'; +import type { RecipeWithTagLinks } from '../../../types'; + +interface Params { + name: string[]; +} + +type GenerateMetaData = (props: { + params: Promise; +}) => Promise; interface RecipeDetailsProps { - params: { - name: string[]; - }; + params: Params; +} + +async function getRecipeFromName( + addonName: string[], +): Promise< + // TODO: More precise typing to avoid these omits + | Omit< + RecipeWithTagLinks, + | 'disabled' + | 'monthlyViews' + | 'publisher' + | 'status' + | 'type' + | 'yearlyViews' + > + | undefined +> { + // TODO: Better decoding? + const name = addonName.join('/').replace('%40', '@'); + return await fetchRecipeDetailsData(name); } +export const generateMetadata: GenerateMetaData = async ({ params }) => { + const name = (await params).name; + const recipe = await getRecipeFromName(name); + + return { + title: recipe?.displayName + ? `${recipe.displayName} | Storybook recipes` + : undefined, + }; +}; + export async function generateStaticParams() { const recipes = (await fetchRecipesData()) || []; const listOfNames = recipes.map((recipe) => ({ @@ -33,12 +73,12 @@ export async function generateStaticParams() { export default async function RecipeDetails({ params }: RecipeDetailsProps) { const { number: githubCount } = await fetchGithubCount(); - // TODO: Better decoding? - const name = params.name.join('/').replace('%40', '@'); - const metadata = await fetchRecipeDetailsData(name); + const recipe = await getRecipeFromName(params.name); + + if (!recipe) notFound(); const mdx = await fs.promises.readFile( - path.join(process.cwd(), 'content/recipes', `${name}.mdx`), + path.join(process.cwd(), 'content/recipes', `${recipe.name}.mdx`), 'utf-8', ); @@ -51,7 +91,7 @@ export default async function RecipeDetails({ params }: RecipeDetailsProps) {
-
+
{metadata?.name
-

- Integrate {metadata?.displayName} with Storybook +

+ Integrate {recipe.displayName} with Storybook

- {metadata?.description} + {recipe.description}
@@ -96,20 +136,20 @@ export default async function RecipeDetails({ params }: RecipeDetailsProps) { />
-
Tags
-
    - {metadata?.tags?.map((tag) => ( +
    Tags
    +
      + {recipe.tags?.map((tag) => ( {tag.name} ))}
    -
    +
    Contributors
    -
      - {metadata?.authors?.map((author) => ( +
        + {recipe.authors?.map((author) => (
      • {author.gravatarUrl ? ( -
        +
        {author.username} { + tags: TagLinkType[]; +} + export type Status = 'default' | 'essential' | 'deprecated'; export interface Tag {