refactor(theme-{classic,common}): change how site/page/search metadata is handled (#6925)

This commit is contained in:
Sébastien Lorber 2022-03-18 18:53:00 +01:00 committed by GitHub
parent 74e37e86ba
commit 74f653dd82
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 808 additions and 625 deletions

View file

@ -92,8 +92,6 @@ declare module '@theme/Layout' {
export interface Props {
readonly children?: ReactNode;
readonly title?: string;
readonly description?: string;
}
export default function Layout(props: Props): JSX.Element;
}
@ -117,6 +115,10 @@ declare module '@theme/Root' {
export default function Root({children}: Props): JSX.Element;
}
declare module '@theme/SiteMetadata' {
export default function SiteMetadata(): JSX.Element;
}
declare module '@docusaurus/constants' {
export const DEFAULT_PLUGIN_ID: 'default';
}

View file

@ -364,31 +364,17 @@ declare module '@theme/Layout' {
export interface Props {
readonly children?: ReactNode;
readonly title?: string;
readonly noFooter?: boolean;
readonly description?: string;
readonly image?: string;
readonly keywords?: string | string[];
readonly permalink?: string;
readonly wrapperClassName?: string;
readonly pageClassName?: string;
readonly searchMetadata?: {
readonly version?: string;
readonly tag?: string;
};
// Not really layout-related, but kept for convenience/retro-compatibility
readonly title?: string;
readonly description?: string;
}
export default function Layout(props: Props): JSX.Element;
}
declare module '@theme/LayoutHead' {
import type {Props as LayoutProps} from '@theme/Layout';
export interface Props extends Omit<LayoutProps, 'children'> {}
export default function LayoutHead(props: Props): JSX.Element;
}
declare module '@theme/LayoutProviders' {
import type {ReactNode} from 'react';
@ -480,7 +466,7 @@ declare module '@theme/Navbar/Content' {
declare module '@theme/Navbar/Layout' {
export interface Props {
children: React.ReactNode;
readonly children: React.ReactNode;
}
export default function NavbarLayout(props: Props): JSX.Element;
@ -927,17 +913,3 @@ declare module '@theme/prism-include-languages' {
PrismObject: typeof PrismNamespace,
): void;
}
declare module '@theme/Seo' {
import type {ReactNode} from 'react';
export interface Props {
readonly title?: string;
readonly description?: string;
readonly keywords?: readonly string[] | string;
readonly image?: string;
readonly children?: ReactNode;
}
export default function Seo(props: Props): JSX.Element;
}

View file

@ -10,6 +10,7 @@ import Layout from '@theme/Layout';
import Link from '@docusaurus/Link';
import type {ArchiveBlogPost, Props} from '@theme/BlogArchivePage';
import {translate} from '@docusaurus/Translate';
import {PageMetadata} from '@docusaurus/theme-common';
type YearProp = {
year: string;
@ -75,7 +76,9 @@ export default function BlogArchive({archive}: Props): JSX.Element {
});
const years = listPostsByYears(archive.blogPosts);
return (
<Layout title={title} description={description}>
<>
<PageMetadata title={title} description={description} />
<Layout>
<header className="hero hero--primary">
<div className="container">
<h1 className="hero__title">{title}</h1>
@ -84,5 +87,6 @@ export default function BlogArchive({archive}: Props): JSX.Element {
</header>
<main>{years.length > 0 && <YearsSection years={years} />}</main>
</Layout>
</>
);
}

View file

@ -12,28 +12,34 @@ import BlogLayout from '@theme/BlogLayout';
import BlogPostItem from '@theme/BlogPostItem';
import BlogListPaginator from '@theme/BlogListPaginator';
import type {Props} from '@theme/BlogListPage';
import {ThemeClassNames} from '@docusaurus/theme-common';
import {
PageMetadata,
HtmlClassNameProvider,
ThemeClassNames,
} from '@docusaurus/theme-common';
import SearchMetadata from '@theme/SearchMetadata';
import clsx from 'clsx';
export default function BlogListPage(props: Props): JSX.Element {
const {metadata, items, sidebar} = props;
function BlogListPageMetadata(props: Props): JSX.Element {
const {metadata} = props;
const {
siteConfig: {title: siteTitle},
} = useDocusaurusContext();
const {blogDescription, blogTitle, permalink} = metadata;
const isBlogOnlyMode = permalink === '/';
const title = isBlogOnlyMode ? siteTitle : blogTitle;
return (
<BlogLayout
title={title}
description={blogDescription}
wrapperClassName={ThemeClassNames.wrapper.blogPages}
pageClassName={ThemeClassNames.page.blogListPage}
searchMetadata={{
// assign unique search tag to exclude this page from search results!
tag: 'blog_posts_list',
}}
sidebar={sidebar}>
<>
<PageMetadata title={title} description={blogDescription} />
<SearchMetadata tag="blog_posts_list" />
</>
);
}
function BlogListPageContent(props: Props): JSX.Element {
const {metadata, items, sidebar} = props;
return (
<BlogLayout sidebar={sidebar}>
{items.map(({content: BlogPostContent}) => (
<BlogPostItem
key={BlogPostContent.metadata.permalink}
@ -48,3 +54,16 @@ export default function BlogListPage(props: Props): JSX.Element {
</BlogLayout>
);
}
export default function BlogListPage(props: Props): JSX.Element {
return (
<HtmlClassNameProvider
className={clsx(
ThemeClassNames.wrapper.blogPages,
ThemeClassNames.page.blogListPage,
)}>
<BlogListPageMetadata {...props} />
<BlogListPageContent {...props} />
</HtmlClassNameProvider>
);
}

View file

@ -6,63 +6,32 @@
*/
import React from 'react';
import Seo from '@theme/Seo';
import BlogLayout from '@theme/BlogLayout';
import BlogPostItem from '@theme/BlogPostItem';
import BlogPostPaginator from '@theme/BlogPostPaginator';
import type {Props} from '@theme/BlogPostPage';
import {ThemeClassNames} from '@docusaurus/theme-common';
import {
PageMetadata,
HtmlClassNameProvider,
ThemeClassNames,
} from '@docusaurus/theme-common';
import TOC from '@theme/TOC';
import clsx from 'clsx';
export default function BlogPostPage(props: Props): JSX.Element {
const {content: BlogPostContents, sidebar} = props;
function BlogPostPageMetadata(props: Props): JSX.Element {
const {content: BlogPostContents} = props;
const {assets, metadata} = BlogPostContents;
const {
title,
description,
nextItem,
prevItem,
date,
tags,
authors,
frontMatter,
} = metadata;
const {
hide_table_of_contents: hideTableOfContents,
keywords,
toc_min_heading_level: tocMinHeadingLevel,
toc_max_heading_level: tocMaxHeadingLevel,
} = frontMatter;
const {title, description, date, tags, authors, frontMatter} = metadata;
const {keywords} = frontMatter;
const image = assets.image ?? frontMatter.image;
return (
<BlogLayout
wrapperClassName={ThemeClassNames.wrapper.blogPages}
pageClassName={ThemeClassNames.page.blogPostPage}
sidebar={sidebar}
toc={
!hideTableOfContents &&
BlogPostContents.toc &&
BlogPostContents.toc.length > 0 ? (
<TOC
toc={BlogPostContents.toc}
minHeadingLevel={tocMinHeadingLevel}
maxHeadingLevel={tocMaxHeadingLevel}
/>
) : undefined
}>
<Seo
// TODO refactor needed: it's a bit annoying but Seo MUST be inside
// BlogLayout, otherwise default image (set by BlogLayout) would shadow
// the custom blog post image
<PageMetadata
title={title}
description={description}
keywords={keywords}
image={image}>
<meta property="og:type" content="article" />
<meta property="article:published_time" content={date} />
{/* TODO double check those article meta array syntaxes, see https://ogp.me/#array */}
{authors.some((author) => author.url) && (
<meta
@ -79,8 +48,33 @@ export default function BlogPostPage(props: Props): JSX.Element {
content={tags.map((tag) => tag.label).join(',')}
/>
)}
</Seo>
</PageMetadata>
);
}
function BlogPostPageContent(props: Props): JSX.Element {
const {content: BlogPostContents, sidebar} = props;
const {assets, metadata} = BlogPostContents;
const {nextItem, prevItem, frontMatter} = metadata;
const {
hide_table_of_contents: hideTableOfContents,
toc_min_heading_level: tocMinHeadingLevel,
toc_max_heading_level: tocMaxHeadingLevel,
} = frontMatter;
return (
<BlogLayout
sidebar={sidebar}
toc={
!hideTableOfContents &&
BlogPostContents.toc &&
BlogPostContents.toc.length > 0 ? (
<TOC
toc={BlogPostContents.toc}
minHeadingLevel={tocMinHeadingLevel}
maxHeadingLevel={tocMaxHeadingLevel}
/>
) : undefined
}>
<BlogPostItem
frontMatter={frontMatter}
assets={assets}
@ -95,3 +89,16 @@ export default function BlogPostPage(props: Props): JSX.Element {
</BlogLayout>
);
}
export default function BlogPostPage(props: Props): JSX.Element {
return (
<HtmlClassNameProvider
className={clsx(
ThemeClassNames.wrapper.blogPages,
ThemeClassNames.page.blogPostPage,
)}>
<BlogPostPageMetadata {...props} />
<BlogPostPageContent {...props} />
</HtmlClassNameProvider>
);
}

View file

@ -11,25 +11,29 @@ import BlogLayout from '@theme/BlogLayout';
import TagsListByLetter from '@theme/TagsListByLetter';
import type {Props} from '@theme/BlogTagsListPage';
import {
PageMetadata,
HtmlClassNameProvider,
ThemeClassNames,
translateTagsPageTitle,
} from '@docusaurus/theme-common';
import SearchMetadata from '../SearchMetadata';
import clsx from 'clsx';
export default function BlogTagsListPage(props: Props): JSX.Element {
const {tags, sidebar} = props;
const title = translateTagsPageTitle();
return (
<BlogLayout
title={title}
wrapperClassName={ThemeClassNames.wrapper.blogPages}
pageClassName={ThemeClassNames.page.blogTagsListPage}
searchMetadata={{
// assign unique search tag to exclude this page from search results!
tag: 'blog_tags_list',
}}
sidebar={sidebar}>
<HtmlClassNameProvider
className={clsx(
ThemeClassNames.wrapper.blogPages,
ThemeClassNames.page.blogTagsListPage,
)}>
<PageMetadata title={title} />
<SearchMetadata tag="blog_tags_list" />
<BlogLayout sidebar={sidebar}>
<h1>{title}</h1>
<TagsListByLetter tags={Object.values(tags)} />
</BlogLayout>
</HtmlClassNameProvider>
);
}

View file

@ -12,8 +12,15 @@ import BlogLayout from '@theme/BlogLayout';
import BlogPostItem from '@theme/BlogPostItem';
import type {Props} from '@theme/BlogTagsPostsPage';
import Translate, {translate} from '@docusaurus/Translate';
import {ThemeClassNames, usePluralForm} from '@docusaurus/theme-common';
import {
PageMetadata,
HtmlClassNameProvider,
ThemeClassNames,
usePluralForm,
} from '@docusaurus/theme-common';
import BlogListPaginator from '@theme/BlogListPaginator';
import SearchMetadata from '@theme/SearchMetadata';
import clsx from 'clsx';
// Very simple pluralization: probably good enough for now
function useBlogPostsPlural() {
@ -47,15 +54,14 @@ export default function BlogTagsPostsPage(props: Props): JSX.Element {
);
return (
<BlogLayout
title={title}
wrapperClassName={ThemeClassNames.wrapper.blogPages}
pageClassName={ThemeClassNames.page.blogTagPostListPage}
searchMetadata={{
// assign unique search tag to exclude this page from search results!
tag: 'blog_tags_posts',
}}
sidebar={sidebar}>
<HtmlClassNameProvider
className={clsx(
ThemeClassNames.wrapper.blogPages,
ThemeClassNames.page.blogTagPostListPage,
)}>
<PageMetadata title={title} />
<SearchMetadata tag="blog_tags_posts" />
<BlogLayout sidebar={sidebar}>
<header className="margin-bottom--xl">
<h1>{title}</h1>
@ -80,5 +86,6 @@ export default function BlogTagsPostsPage(props: Props): JSX.Element {
))}
<BlogListPaginator metadata={listMetadata} />
</BlogLayout>
</HtmlClassNameProvider>
);
}

View file

@ -6,11 +6,13 @@
*/
import React from 'react';
import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
import {
PageMetadata,
useCurrentSidebarCategory,
} from '@docusaurus/theme-common';
import type {Props} from '@theme/DocCategoryGeneratedIndexPage';
import DocCardList from '@theme/DocCardList';
import DocPaginator from '@theme/DocPaginator';
import Seo from '@theme/Seo';
import DocVersionBanner from '@theme/DocVersionBanner';
import DocVersionBadge from '@theme/DocVersionBadge';
import DocBreadcrumbs from '@theme/DocBreadcrumbs';
@ -19,13 +21,27 @@ import useBaseUrl from '@docusaurus/useBaseUrl';
import styles from './styles.module.css';
export default function DocCategoryGeneratedIndexPage({
function DocCategoryGeneratedIndexPageMetadata({
categoryGeneratedIndex,
}: Props): JSX.Element {
return (
<PageMetadata
title={categoryGeneratedIndex.title}
description={categoryGeneratedIndex.description}
keywords={categoryGeneratedIndex.keywords}
// TODO `require` this?
image={useBaseUrl(categoryGeneratedIndex.image)}
/>
);
}
function DocCategoryGeneratedIndexPageContent({
categoryGeneratedIndex,
}: Props): JSX.Element {
const category = useCurrentSidebarCategory();
return (
<>
<Seo
<PageMetadata
title={categoryGeneratedIndex.title}
description={categoryGeneratedIndex.description}
keywords={categoryGeneratedIndex.keywords}
@ -57,3 +73,14 @@ export default function DocCategoryGeneratedIndexPage({
</>
);
}
export default function DocCategoryGeneratedIndexPage(
props: Props,
): JSX.Element {
return (
<>
<DocCategoryGeneratedIndexPageMetadata {...props} />
<DocCategoryGeneratedIndexPageContent {...props} />
</>
);
}

View file

@ -10,7 +10,6 @@ import clsx from 'clsx';
import DocPaginator from '@theme/DocPaginator';
import DocVersionBanner from '@theme/DocVersionBanner';
import DocVersionBadge from '@theme/DocVersionBadge';
import Seo from '@theme/Seo';
import type {Props} from '@theme/DocItem';
import DocItemFooter from '@theme/DocItemFooter';
import TOC from '@theme/TOC';
@ -18,6 +17,7 @@ import TOCCollapsible from '@theme/TOCCollapsible';
import Heading from '@theme/Heading';
import styles from './styles.module.css';
import {
PageMetadata,
HtmlClassNameProvider,
ThemeClassNames,
useWindowSize,
@ -25,18 +25,26 @@ import {
import DocBreadcrumbs from '@theme/DocBreadcrumbs';
import MDXContent from '@theme/MDXContent';
export default function DocItem(props: Props): JSX.Element {
function DocItemMetadata(props: Props): JSX.Element {
const {content: DocContent} = props;
const {metadata, frontMatter, assets} = DocContent;
const {keywords} = frontMatter;
const {description, title} = metadata;
const image = assets.image ?? frontMatter.image;
return <PageMetadata {...{title, description, keywords, image}} />;
}
function DocItemContent(props: Props): JSX.Element {
const {content: DocContent} = props;
const {metadata, frontMatter} = DocContent;
const {
keywords,
hide_title: hideTitle,
hide_table_of_contents: hideTableOfContents,
toc_min_heading_level: tocMinHeadingLevel,
toc_max_heading_level: tocMaxHeadingLevel,
} = frontMatter;
const {description, title} = metadata;
const image = assets.image ?? frontMatter.image;
const {title} = metadata;
// We only add a title if:
// - user asks to hide it with front matter
@ -53,9 +61,6 @@ export default function DocItem(props: Props): JSX.Element {
canRenderTOC && (windowSize === 'desktop' || windowSize === 'ssr');
return (
<HtmlClassNameProvider className={`docs-doc-id-${metadata.unversionedId}`}>
<Seo {...{title, description, keywords, image}} />
<div className="row">
<div className={clsx('col', !hideTableOfContents && styles.docItemCol)}>
<DocVersionBanner />
@ -76,8 +81,7 @@ export default function DocItem(props: Props): JSX.Element {
/>
)}
<div
className={clsx(ThemeClassNames.docs.docMarkdown, 'markdown')}>
<div className={clsx(ThemeClassNames.docs.docMarkdown, 'markdown')}>
{/*
Title can be declared inside md content or declared through
front matter and added manually. To make both cases consistent,
@ -111,6 +115,15 @@ export default function DocItem(props: Props): JSX.Element {
</div>
)}
</div>
);
}
export default function DocItem(props: Props): JSX.Element {
const docHtmlClassName = `docs-doc-id-${props.content.metadata.unversionedId}`;
return (
<HtmlClassNameProvider className={docHtmlClassName}>
<DocItemMetadata {...props} />
<DocItemContent {...props} />
</HtmlClassNameProvider>
);
}

View file

@ -29,6 +29,7 @@ import {
useDocsSidebar,
DocsVersionProvider,
} from '@docusaurus/theme-common';
import SearchMetadata from '@theme/SearchMetadata';
type DocPageContentProps = {
readonly currentDocRoute: DocumentRoute;
@ -56,13 +57,12 @@ function DocPageContent({
}, [hiddenSidebar]);
return (
<Layout
wrapperClassName={ThemeClassNames.wrapper.docsPages}
pageClassName={ThemeClassNames.page.docsDocPage}
searchMetadata={{
version,
tag: docVersionSearchTag(pluginId, version),
}}>
<>
<SearchMetadata
version={version}
tag={docVersionSearchTag(pluginId, version)}
/>
<Layout>
<div className={styles.docPage}>
<BackToTopButton />
@ -75,7 +75,9 @@ function DocPageContent({
)}
onTransitionEnd={(e) => {
if (
!e.currentTarget.classList.contains(styles.docSidebarContainer!)
!e.currentTarget.classList.contains(
styles.docSidebarContainer!,
)
) {
return;
}
@ -137,6 +139,7 @@ function DocPageContent({
</main>
</div>
</Layout>
</>
);
}
@ -161,7 +164,12 @@ export default function DocPage(props: Props): JSX.Element {
: null;
return (
<HtmlClassNameProvider className={versionMetadata.className}>
<HtmlClassNameProvider
className={clsx(
ThemeClassNames.wrapper.docsPages,
ThemeClassNames.page.docsDocPage,
versionMetadata.className,
)}>
<DocsVersionProvider version={versionMetadata}>
<DocsSidebarProvider sidebar={sidebar ?? null}>
<DocPageContent

View file

@ -10,10 +10,6 @@
--doc-sidebar-hidden-width: 30px;
}
:global(.docs-wrapper) {
display: flex;
}
.docPage,
.docMainContainer {
display: flex;

View file

@ -9,10 +9,17 @@ import React from 'react';
import Layout from '@theme/Layout';
import Link from '@docusaurus/Link';
import {ThemeClassNames, usePluralForm} from '@docusaurus/theme-common';
import {
PageMetadata,
HtmlClassNameProvider,
ThemeClassNames,
usePluralForm,
} from '@docusaurus/theme-common';
import type {PropTagDocListDoc} from '@docusaurus/plugin-content-docs';
import Translate, {translate} from '@docusaurus/Translate';
import type {Props} from '@theme/DocTagDocListPage';
import SearchMetadata from '@theme/SearchMetadata';
import clsx from 'clsx';
// Very simple pluralization: probably good enough for now
function useNDocsTaggedPlural() {
@ -55,14 +62,14 @@ export default function DocTagDocListPage({tag}: Props): JSX.Element {
);
return (
<Layout
title={title}
wrapperClassName={ThemeClassNames.wrapper.docsPages}
pageClassName={ThemeClassNames.page.docsTagDocListPage}
searchMetadata={{
// assign unique search tag to exclude this page from search results!
tag: 'doc_tag_doc_list',
}}>
<HtmlClassNameProvider
className={clsx(
ThemeClassNames.wrapper.docsPages,
ThemeClassNames.page.docsTagDocListPage,
)}>
<PageMetadata title={title} />
<SearchMetadata tag="doc_tag_doc_list" />
<Layout>
<div className="container margin-vert--lg">
<div className="row">
<main className="col col--8 col--offset-2">
@ -85,5 +92,6 @@ export default function DocTagDocListPage({tag}: Props): JSX.Element {
</div>
</div>
</Layout>
</HtmlClassNameProvider>
);
}

View file

@ -9,23 +9,27 @@ import React from 'react';
import Layout from '@theme/Layout';
import {
PageMetadata,
HtmlClassNameProvider,
ThemeClassNames,
translateTagsPageTitle,
} from '@docusaurus/theme-common';
import TagsListByLetter from '@theme/TagsListByLetter';
import type {Props} from '@theme/DocTagsListPage';
import SearchMetadata from '@theme/SearchMetadata';
import clsx from 'clsx';
export default function DocTagsListPage({tags}: Props): JSX.Element {
const title = translateTagsPageTitle();
return (
<Layout
title={title}
wrapperClassName={ThemeClassNames.wrapper.docsPages}
pageClassName={ThemeClassNames.page.docsTagsListPage}
searchMetadata={{
// assign unique search tag to exclude this page from search results!
tag: 'doc_tags_list',
}}>
<HtmlClassNameProvider
className={clsx(
ThemeClassNames.wrapper.docsPages,
ThemeClassNames.page.docsTagsListPage,
)}>
<PageMetadata title={title} />
<SearchMetadata tag="doc_tags_list" />
<Layout>
<div className="container margin-vert--lg">
<div className="row">
<main className="col col--8 col--offset-2">
@ -35,5 +39,6 @@ export default function DocTagsListPage({tags}: Props): JSX.Element {
</div>
</div>
</Layout>
</HtmlClassNameProvider>
);
}

View file

@ -13,20 +13,30 @@ import AnnouncementBar from '@theme/AnnouncementBar';
import Navbar from '@theme/Navbar';
import Footer from '@theme/Footer';
import LayoutProviders from '@theme/LayoutProviders';
import LayoutHead from '@theme/LayoutHead';
import type {Props} from '@theme/Layout';
import {ThemeClassNames, useKeyboardNavigation} from '@docusaurus/theme-common';
import {
PageMetadata,
ThemeClassNames,
useKeyboardNavigation,
} from '@docusaurus/theme-common';
import ErrorPageContent from '@theme/ErrorPageContent';
import './styles.css';
export default function Layout(props: Props): JSX.Element {
const {children, noFooter, wrapperClassName, pageClassName} = props;
const {
children,
noFooter,
wrapperClassName,
// not really layout-related, but kept for convenience/retro-compatibility
title,
description,
} = props;
useKeyboardNavigation();
return (
<LayoutProviders>
<LayoutHead {...props} />
<PageMetadata title={title} description={description} />
<SkipToContent />
@ -34,12 +44,7 @@ export default function Layout(props: Props): JSX.Element {
<Navbar />
<div
className={clsx(
ThemeClassNames.wrapper.main,
wrapperClassName,
pageClassName,
)}>
<div className={clsx(ThemeClassNames.wrapper.main, wrapperClassName)}>
<ErrorBoundary fallback={ErrorPageContent}>{children}</ErrorBoundary>
</div>

View file

@ -11,25 +11,30 @@ import Layout from '@theme/Layout';
import MDXContent from '@theme/MDXContent';
import type {Props} from '@theme/MDXPage';
import TOC from '@theme/TOC';
import {ThemeClassNames} from '@docusaurus/theme-common';
import {
PageMetadata,
HtmlClassNameProvider,
ThemeClassNames,
} from '@docusaurus/theme-common';
import styles from './styles.module.css';
export default function MDXPage(props: Props): JSX.Element {
const {content: MDXPageContent} = props;
const {
metadata: {title, description, permalink, frontMatter},
metadata: {title, description, frontMatter},
} = MDXPageContent;
const {wrapperClassName, hide_table_of_contents: hideTableOfContents} =
frontMatter;
return (
<Layout
title={title}
description={description}
permalink={permalink}
wrapperClassName={wrapperClassName ?? ThemeClassNames.wrapper.mdxPages}
pageClassName={ThemeClassNames.page.mdxPage}>
<HtmlClassNameProvider
className={clsx(
wrapperClassName ?? ThemeClassNames.wrapper.mdxPages,
ThemeClassNames.page.mdxPage,
)}>
<PageMetadata title={title} description={description} />
<Layout>
<main className="container container--fluid margin-vert--lg">
<div className={clsx('row', styles.mdxPageWrapper)}>
<div className={clsx('col', !hideTableOfContents && 'col--8')}>
@ -49,5 +54,6 @@ export default function MDXPage(props: Props): JSX.Element {
</div>
</main>
</Layout>
</HtmlClassNameProvider>
);
}

View file

@ -8,14 +8,18 @@
import React from 'react';
import Layout from '@theme/Layout';
import Translate, {translate} from '@docusaurus/Translate';
import {PageMetadata} from '@docusaurus/theme-common';
export default function NotFound(): JSX.Element {
return (
<Layout
<>
<PageMetadata
title={translate({
id: 'theme.NotFound.title',
message: 'Page Not Found',
})}>
})}
/>
<Layout>
<main className="container margin-vert--xl">
<div className="row">
<div className="col col--6 col--offset-3">
@ -45,5 +49,6 @@ export default function NotFound(): JSX.Element {
</div>
</main>
</Layout>
</>
);
}

View file

@ -1,49 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import Head from '@docusaurus/Head';
import {useTitleFormatter} from '@docusaurus/theme-common';
import {useBaseUrlUtils} from '@docusaurus/useBaseUrl';
import type {Props} from '@theme/Seo';
export default function Seo({
title,
description,
keywords,
image,
children,
}: Props): JSX.Element {
const pageTitle = useTitleFormatter(title);
const {withBaseUrl} = useBaseUrlUtils();
const pageImage = image ? withBaseUrl(image, {absolute: true}) : undefined;
return (
<Head>
{title && <title>{pageTitle}</title>}
{title && <meta property="og:title" content={pageTitle} />}
{description && <meta name="description" content={description} />}
{description && <meta property="og:description" content={description} />}
{keywords && (
<meta
name="keywords"
content={
(Array.isArray(keywords) ? keywords.join(',') : keywords) as string
}
/>
)}
{pageImage && <meta property="og:image" content={pageImage} />}
{pageImage && <meta name="twitter:image" content={pageImage} />}
{children}
</Head>
);
}

View file

@ -9,19 +9,18 @@ import React from 'react';
import Head from '@docusaurus/Head';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import useBaseUrl from '@docusaurus/useBaseUrl';
import type {Props} from '@theme/Layout';
import SearchMetadata from '@theme/SearchMetadata';
import Seo from '@theme/Seo';
import {
PageMetadata,
DEFAULT_SEARCH_TAG,
useTitleFormatter,
useAlternatePageUtils,
useThemeConfig,
keyboardFocusedClassName,
} from '@docusaurus/theme-common';
import {useLocation} from '@docusaurus/router';
// Useful for SEO
// TODO move to SiteMetadataDefaults or theme-common ?
// Useful for i18n/SEO
// See https://developers.google.com/search/docs/advanced/crawling/localized-versions
// See https://github.com/facebook/docusaurus/issues/3317
function AlternateLangHeaders(): JSX.Element {
@ -66,6 +65,7 @@ function useDefaultCanonicalUrl() {
return siteUrl + useBaseUrl(pathname);
}
// TODO move to SiteMetadataDefaults or theme-common ?
function CanonicalUrlHeaders({permalink}: {permalink?: string}) {
const {
siteConfig: {url: siteUrl},
@ -83,45 +83,31 @@ function CanonicalUrlHeaders({permalink}: {permalink?: string}) {
);
}
export default function LayoutHead(props: Props): JSX.Element {
export default function SiteMetadata(): JSX.Element {
const {
siteConfig: {favicon},
i18n: {currentLocale, localeConfigs},
i18n: {currentLocale},
} = useDocusaurusContext();
// TODO maybe move these 2 themeConfig to siteConfig?
// These seems useful for other themes as well
const {metadata, image: defaultImage} = useThemeConfig();
const {title, description, image, keywords, searchMetadata} = props;
const faviconUrl = useBaseUrl(favicon);
const pageTitle = useTitleFormatter(title);
const {htmlLang, direction: htmlDir} = localeConfigs[currentLocale]!;
return (
<>
<Head>
<html lang={htmlLang} dir={htmlDir} />
{favicon && <link rel="icon" href={faviconUrl} />}
<title>{pageTitle}</title>
<meta property="og:title" content={pageTitle} />
<meta name="twitter:card" content="summary_large_image" />
{/* The keyboard focus class name need to be applied when SSR so links
are outlined when JS is disabled */}
<body className={keyboardFocusedClassName} />
</Head>
{/* image can override the default image */}
{defaultImage && <Seo image={defaultImage} />}
{image && <Seo image={image} />}
<Seo description={description} keywords={keywords} />
{defaultImage && <PageMetadata image={defaultImage} />}
<CanonicalUrlHeaders />
<AlternateLangHeaders />
<SearchMetadata
tag={DEFAULT_SEARCH_TAG}
locale={currentLocale}
{...searchMetadata}
/>
<SearchMetadata tag={DEFAULT_SEARCH_TAG} locale={currentLocale} />
<Head
// it's important to have an additional <Head> element here,

View file

@ -129,9 +129,10 @@ export {isRegexpStringMatch} from './utils/regexpUtils';
export {useHomePageRoute} from './utils/routesUtils';
export {
PageMetadata,
HtmlClassNameProvider,
PluginHtmlClassNameProvider,
} from './utils/metadataUtilsTemp';
} from './utils/metadataUtils';
export {
useColorMode,

View file

@ -9,6 +9,53 @@ import React, {type ReactNode} from 'react';
import Head from '@docusaurus/Head';
import clsx from 'clsx';
import useRouteContext from '@docusaurus/useRouteContext';
import {useBaseUrlUtils} from '@docusaurus/useBaseUrl';
import {useTitleFormatter} from './generalUtils';
interface PageMetadataProps {
readonly title?: string;
readonly description?: string;
readonly keywords?: readonly string[] | string;
readonly image?: string;
readonly children?: ReactNode;
}
// Helper component to manipulate page metadata and override site defaults
export function PageMetadata({
title,
description,
keywords,
image,
children,
}: PageMetadataProps): JSX.Element {
const pageTitle = useTitleFormatter(title);
const {withBaseUrl} = useBaseUrlUtils();
const pageImage = image ? withBaseUrl(image, {absolute: true}) : undefined;
return (
<Head>
{title && <title>{pageTitle}</title>}
{title && <meta property="og:title" content={pageTitle} />}
{description && <meta name="description" content={description} />}
{description && <meta property="og:description" content={description} />}
{keywords && (
<meta
name="keywords"
content={
(Array.isArray(keywords) ? keywords.join(',') : keywords) as string
}
/>
)}
{pageImage && <meta property="og:image" content={pageImage} />}
{pageImage && <meta name="twitter:image" content={pageImage} />}
{children}
</Head>
);
}
const HtmlClassNameContext = React.createContext<string | undefined>(undefined);

View file

@ -17,6 +17,7 @@ import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import {
HtmlClassNameProvider,
useTitleFormatter,
usePluralForm,
isRegexpStringMatch,
@ -149,7 +150,7 @@ type ResultDispatcher =
| {type: 'update'; value: ResultDispatcherState}
| {type: 'advance'; value?: undefined};
export default function SearchPage(): JSX.Element {
function SearchPageContent(): JSX.Element {
const {
siteConfig: {themeConfig},
i18n: {currentLocale},
@ -356,7 +357,7 @@ export default function SearchPage(): JSX.Element {
}, [makeSearch, searchResultState.lastPage]);
return (
<Layout wrapperClassName="search-page-wrapper">
<Layout>
<Head>
<title>{useTitleFormatter(getTitle())}</title>
{/*
@ -516,3 +517,11 @@ export default function SearchPage(): JSX.Element {
</Layout>
);
}
export default function SearchPage(): JSX.Element {
return (
<HtmlClassNameProvider className="search-page-wrapper">
<SearchPageContent />
</HtmlClassNameProvider>
);
}

View file

@ -13,7 +13,9 @@ import {BrowserContextProvider} from './exports/browserContext';
import {DocusaurusContextProvider} from './exports/docusaurusContext';
import PendingNavigation from './PendingNavigation';
import BaseUrlIssueBanner from './baseUrlIssueBanner/BaseUrlIssueBanner';
import SiteMetadataDefaults from './SiteMetadataDefaults';
import Root from '@theme/Root';
import SiteMetadata from '@theme/SiteMetadata';
import './client-lifecycles-dispatcher';
@ -27,6 +29,8 @@ export default function App(): JSX.Element {
<DocusaurusContextProvider>
<BrowserContextProvider>
<Root>
<SiteMetadataDefaults />
<SiteMetadata />
<BaseUrlIssueBanner />
<PendingNavigation routes={routes} delay={1000}>
{renderRoutes(routes)}

View file

@ -0,0 +1,27 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import Head from '@docusaurus/Head';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import useBaseUrl from '@docusaurus/useBaseUrl';
export default function SiteMetadataDefaults(): JSX.Element {
const {
siteConfig: {favicon, tagline, title},
i18n: {currentLocale, localeConfigs},
} = useDocusaurusContext();
const faviconUrl = useBaseUrl(favicon);
const {htmlLang, direction: htmlDir} = localeConfigs[currentLocale]!;
return (
<Head defaultTitle={`${title}${tagline ? ` · ${tagline}` : ''}`}>
<html lang={htmlLang} dir={htmlDir} />
{favicon && <link rel="icon" href={faviconUrl} />}
</Head>
);
}

View file

@ -9,6 +9,7 @@ import React from 'react';
import Layout from '@theme/Layout';
import ErrorBoundary from '@docusaurus/ErrorBoundary';
import type {Props} from '@theme/Error';
import Head from '@docusaurus/Head';
function ErrorDisplay({error, tryAgain}: Props): JSX.Element {
return (
@ -40,7 +41,10 @@ export default function Error({error, tryAgain}: Props): JSX.Element {
// Note: we display the original error here, not the error that we
// captured in this extra error boundary
fallback={() => <ErrorDisplay error={error} tryAgain={tryAgain} />}>
<Layout title="Page Error">
<Head>
<title>Page Error</title>
</Head>
<Layout>
<ErrorDisplay error={error} tryAgain={tryAgain} />
</Layout>
</ErrorBoundary>

View file

@ -6,31 +6,8 @@
*/
import React from 'react';
import Head from '@docusaurus/Head';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import useBaseUrl from '@docusaurus/useBaseUrl';
import type {Props} from '@theme/Layout';
export default function Layout({
children,
title,
description,
}: Props): JSX.Element {
const context = useDocusaurusContext();
const {siteConfig} = context;
const {favicon, tagline, title: defaultTitle} = siteConfig;
const faviconUrl = useBaseUrl(favicon);
return (
<>
<Head defaultTitle={`${defaultTitle}${tagline ? ` · ${tagline}` : ''}`}>
{title && <title>{`${title} · ${tagline}`}</title>}
{favicon && <link rel="icon" href={faviconUrl} />}
{description && <meta name="description" content={description} />}
{description && (
<meta property="og:description" content={description} />
)}
</Head>
{children}
</>
);
export default function Layout({children}: Props): JSX.Element {
return <>{children}</>;
}

View file

@ -7,10 +7,15 @@
import React from 'react';
import Layout from '@theme/Layout';
import Head from '@docusaurus/Head';
export default function NotFound(): JSX.Element {
return (
<Layout title="Page Not Found">
<>
<Head>
<title>Page Not Found</title>
</Head>
<Layout>
<div
style={{
display: 'flex',
@ -22,5 +27,6 @@ export default function NotFound(): JSX.Element {
<h1>Oops, page not found </h1>
</div>
</Layout>
</>
);
}

View file

@ -5,7 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
import React, {type ReactNode} from 'react';
import React from 'react';
import type {Props} from '@theme/Root';
// Wrapper at the very top of the app, that is applied constantly
// and does not depend on current route (unlike the layout)
@ -14,6 +15,6 @@ import React, {type ReactNode} from 'react';
// and these providers won't reset state when we navigate
//
// See https://github.com/facebook/docusaurus/issues/3919
export default function Root({children}: {children: ReactNode}): JSX.Element {
export default function Root({children}: Props): JSX.Element {
return <>{children}</>;
}

View file

@ -0,0 +1,11 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// To be implemented by the theme with <Head>
export default function SiteMetadata(): JSX.Element | null {
return null;
}

View file

@ -33,6 +33,7 @@ exports[`base webpack config creates webpack aliases 1`] = `
"@theme-original/PluginThemeComponentEnhanced": "secondPluginThemeFolder/PluginThemeComponentEnhanced.js",
"@theme-original/PluginThemeComponentOverriddenByUser": "pluginThemeFolder/PluginThemeComponentOverriddenByUser.js",
"@theme-original/Root": "../../../../client/theme-fallback/Root/index.tsx",
"@theme-original/SiteMetadata": "../../../../client/theme-fallback/SiteMetadata/index.tsx",
"@theme-original/subfolder/PluginThemeComponent2": "pluginThemeFolder/subfolder/PluginThemeComponent2.js",
"@theme/Error": "../../../../client/theme-fallback/Error/index.tsx",
"@theme/Layout": "../../../../client/theme-fallback/Layout/index.tsx",
@ -42,6 +43,7 @@ exports[`base webpack config creates webpack aliases 1`] = `
"@theme/PluginThemeComponentEnhanced": "src/theme/PluginThemeComponentEnhanced.js",
"@theme/PluginThemeComponentOverriddenByUser": "src/theme/PluginThemeComponentOverriddenByUser.js",
"@theme/Root": "../../../../client/theme-fallback/Root/index.tsx",
"@theme/SiteMetadata": "../../../../client/theme-fallback/SiteMetadata/index.tsx",
"@theme/UserThemeComponent1": "src/theme/UserThemeComponent1.js",
"@theme/subfolder/PluginThemeComponent2": "pluginThemeFolder/subfolder/PluginThemeComponent2.js",
"@theme/subfolder/UserThemeComponent2": "src/theme/subfolder/UserThemeComponent2.js",

View file

@ -0,0 +1,22 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
html.plugin-docs.plugin-id-docs-tests .red > a {
color: red;
}
html.plugin-docs.plugin-id-docs-tests .navbar {
border-bottom: solid thin cyan;
}
html.plugin-blog.plugin-id-blog-tests .navbar {
border-bottom: solid thin lime;
}
html.plugin-pages.plugin-id-pages-tests .navbar {
border-bottom: solid thin yellow;
}

View file

@ -35,9 +35,9 @@ Create a file `/src/pages/helloReact.js`:
import React from 'react';
import Layout from '@theme/Layout';
function Hello() {
export default function Hello() {
return (
<Layout title="Hello">
<Layout title="Hello" description="Hello React Page">
<div
style={{
display: 'flex',
@ -53,8 +53,6 @@ function Hello() {
</Layout>
);
}
export default Hello;
```
Once you save the file, the development server will automatically reload the changes. Now open `http://localhost:3000/helloReact` and you will see the new page you just created.

View file

@ -42,22 +42,6 @@ Similar to [global metadata](#global-metadata), Docusaurus also allows for the a
Some content...
```
```jsx title="my-react-page.jsx"
import React from 'react';
import Head from '@docusaurus/Head';
export default function page() {
return (
<Layout title="Page" description="A React page demo">
<Head>
<meta property="og:image" content="image.png" />
</Head>
{/* ... */}
</Layout>
);
}
```
Docusaurus automatically adds `description`, `title`, canonical URL links, and other useful metadata to each Markdown page. They are configurable through front matter:
```md
@ -77,6 +61,31 @@ Prefer to use front matter for fields like `description` and `keywords`: Docusau
:::
For JSX pages, you can use the Docusaurus [`<Head>`](docusaurus-core.md#head) component.
```jsx title="my-react-page.jsx"
import React from 'react';
import Layout from '@theme/Layout';
import Head from '@docusaurus/Head';
export default function page() {
return (
<Layout title="Page" description="A React page demo">
<Head>
<meta property="og:image" content="image.png" />
</Head>
{/* ... */}
</Layout>
);
}
```
:::tip
For convenience, the default theme `<Layout>` component accept `title` and `description` as props.
:::
## Static HTML generation {#static-html-generation}
Docusaurus is a static site generator—HTML files are statically generated for every URL route, which helps search engines discover your content more easily.

View file

@ -320,7 +320,10 @@ const config = {
remarkPlugins: [npm2yarn],
},
theme: {
customCss: [require.resolve('./src/css/custom.css')],
customCss: [
require.resolve('./src/css/custom.css'),
require.resolve('./_dogfooding/dogfooding.css'),
],
},
gtag: !isDeployPreview
? {

View file

@ -166,10 +166,6 @@ div[class^='announcementBar_'] {
font-weight: bold;
}
.red > a {
color: red;
}
.screen-reader-only {
border: 0;
clip: rect(0 0 0 0);

View file

@ -9,27 +9,35 @@ import React from 'react';
import BlogLayout from '@theme/BlogLayout';
import BlogListPaginator from '@theme/BlogListPaginator';
import type {Props} from '@theme/BlogListPage';
import {ThemeClassNames} from '@docusaurus/theme-common';
import {
PageMetadata,
HtmlClassNameProvider,
ThemeClassNames,
} from '@docusaurus/theme-common';
import Link from '@docusaurus/Link';
import ChangelogItem from '@theme/ChangelogItem';
import styles from './styles.module.css';
import SearchMetadata from '@theme/SearchMetadata';
import clsx from 'clsx';
export default function ChangelogList(props: Props): JSX.Element {
function ChangelogListMetadata(props: Props): JSX.Element {
const {metadata} = props;
const {blogTitle, blogDescription} = metadata;
return (
<>
<PageMetadata title={blogTitle} description={blogDescription} />
<SearchMetadata tag="blog_posts_list" />
</>
);
}
function ChangelogListContent(props: Props): JSX.Element {
const {metadata, items, sidebar} = props;
const {blogDescription, blogTitle} = metadata;
const {blogTitle} = metadata;
return (
<BlogLayout
title={blogTitle}
description={blogDescription}
wrapperClassName={ThemeClassNames.wrapper.blogPages}
pageClassName={ThemeClassNames.page.blogListPage}
searchMetadata={{
// assign unique search tag to exclude this page from search results!
tag: 'blog_posts_list',
}}
sidebar={sidebar}>
<BlogLayout sidebar={sidebar}>
<header className="margin-bottom--lg">
<h1 style={{fontSize: '3rem'}}>{blogTitle}</h1>
<p>
@ -88,3 +96,16 @@ export default function ChangelogList(props: Props): JSX.Element {
</BlogLayout>
);
}
export default function ChangelogList(props: Props): JSX.Element {
return (
<HtmlClassNameProvider
className={clsx(
ThemeClassNames.wrapper.blogPages,
ThemeClassNames.page.blogListPage,
)}>
<ChangelogListMetadata {...props} />
<ChangelogListContent {...props} />
</HtmlClassNameProvider>
);
}

View file

@ -6,59 +6,28 @@
*/
import React from 'react';
import Seo from '@theme/Seo';
import BlogLayout from '@theme/BlogLayout';
import ChangelogItem from '@theme/ChangelogItem';
import BlogPostPaginator from '@theme/BlogPostPaginator';
import type {Props} from '@theme/BlogPostPage';
import {ThemeClassNames} from '@docusaurus/theme-common';
import {
PageMetadata,
HtmlClassNameProvider,
ThemeClassNames,
} from '@docusaurus/theme-common';
import TOC from '@theme/TOC';
import Link from '@docusaurus/Link';
import clsx from 'clsx';
// This page doesn't change anything. It's just swapping BlogPostItem with our
// own ChangelogItem. We don't want to apply the swizzled item to the actual
// blog.
export default function BlogPostPage(props: Props): JSX.Element {
const {content: BlogPostContents, sidebar} = props;
function ChangelogPageMetadata(props: Props): JSX.Element {
const {content: BlogPostContents} = props;
const {assets, metadata} = BlogPostContents;
const {
title,
description,
nextItem,
prevItem,
date,
tags,
authors,
frontMatter,
// @ts-expect-error: we injected this
listPageLink,
} = metadata;
const {
hide_table_of_contents: hideTableOfContents,
keywords,
toc_min_heading_level: tocMinHeadingLevel,
toc_max_heading_level: tocMaxHeadingLevel,
} = frontMatter;
const {title, description, date, tags, authors, frontMatter} = metadata;
const {keywords} = frontMatter;
const image = assets.image ?? frontMatter.image;
return (
<BlogLayout
wrapperClassName={ThemeClassNames.wrapper.blogPages}
pageClassName={ThemeClassNames.page.blogPostPage}
sidebar={sidebar}
toc={
!hideTableOfContents &&
BlogPostContents.toc &&
BlogPostContents.toc.length > 0 ? (
<TOC
toc={BlogPostContents.toc}
minHeadingLevel={tocMinHeadingLevel}
maxHeadingLevel={tocMaxHeadingLevel}
/>
) : undefined
}>
<Seo
<PageMetadata
title={title}
description={description}
keywords={keywords}
@ -81,8 +50,42 @@ export default function BlogPostPage(props: Props): JSX.Element {
content={tags.map((tag) => tag.label).join(',')}
/>
)}
</Seo>
</PageMetadata>
);
}
function ChangelogPageContent(props: Props): JSX.Element {
const {content: BlogPostContents, sidebar} = props;
const {assets, metadata} = BlogPostContents;
const {
nextItem,
prevItem,
frontMatter,
// @ts-expect-error: we injected this
listPageLink,
} = metadata;
const {
hide_table_of_contents: hideTableOfContents,
toc_min_heading_level: tocMinHeadingLevel,
toc_max_heading_level: tocMaxHeadingLevel,
} = frontMatter;
return (
<>
<PageMetadata />
<BlogLayout
sidebar={sidebar}
toc={
!hideTableOfContents &&
BlogPostContents.toc &&
BlogPostContents.toc.length > 0 ? (
<TOC
toc={BlogPostContents.toc}
minHeadingLevel={tocMinHeadingLevel}
maxHeadingLevel={tocMaxHeadingLevel}
/>
) : undefined
}>
<Link to={listPageLink}> Back to index page</Link>
<ChangelogItem
@ -97,5 +100,22 @@ export default function BlogPostPage(props: Props): JSX.Element {
<BlogPostPaginator nextItem={nextItem} prevItem={prevItem} />
)}
</BlogLayout>
</>
);
}
// This page doesn't change anything. It's just swapping BlogPostItem with our
// own ChangelogItem. We don't want to apply the swizzled item to the actual
// blog.
export default function ChangelogPage(props: Props): JSX.Element {
return (
<HtmlClassNameProvider
className={clsx(
ThemeClassNames.wrapper.blogPages,
ThemeClassNames.page.blogPostPage,
)}>
<ChangelogPageMetadata {...props} />
<ChangelogPageContent {...props} />
</HtmlClassNameProvider>
);
}