mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-28 17:57:48 +02:00
feat(theme): make it possible to provide your own page title formatter (#11090)
This commit is contained in:
parent
5b944d6b64
commit
730ce485ba
17 changed files with 386 additions and 107 deletions
|
@ -122,6 +122,15 @@ declare module '@theme/Root' {
|
||||||
export default function Root({children}: Props): ReactNode;
|
export default function Root({children}: Props): ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '@theme/ThemeProvider' {
|
||||||
|
import type {ReactNode} from 'react';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
readonly children: ReactNode;
|
||||||
|
}
|
||||||
|
export default function ThemeProvider({children}: Props): ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
declare module '@theme/SiteMetadata' {
|
declare module '@theme/SiteMetadata' {
|
||||||
import type {ReactNode} from 'react';
|
import type {ReactNode} from 'react';
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,7 @@
|
||||||
<div class="blog-posts">
|
<div class="blog-posts">
|
||||||
<xsl:for-each select="atom:feed/atom:entry">
|
<xsl:for-each select="atom:feed/atom:entry">
|
||||||
<div class="blog-post">
|
<div class="blog-post">
|
||||||
<h3><a href="{atom:link[@rel='alternate']/@href}"><xsl:value-of
|
<h3><a href="{atom:link/@href}"><xsl:value-of
|
||||||
select="atom:title"
|
select="atom:title"
|
||||||
/></a></h3>
|
/></a></h3>
|
||||||
<div class="blog-post-date">
|
<div class="blog-post-date">
|
||||||
|
|
|
@ -1569,6 +1569,17 @@ declare module '@theme/ThemedImage' {
|
||||||
export default function ThemedImage(props: Props): ReactNode;
|
export default function ThemedImage(props: Props): ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '@theme/ThemeProvider/TitleFormatter' {
|
||||||
|
import type {ReactNode} from 'react';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
readonly children: ReactNode;
|
||||||
|
}
|
||||||
|
export default function ThemeProviderTitleFormatter({
|
||||||
|
children,
|
||||||
|
}: Props): ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
declare module '@theme/Details' {
|
declare module '@theme/Details' {
|
||||||
import {Details, type DetailsProps} from '@docusaurus/theme-common/Details';
|
import {Details, type DetailsProps} from '@docusaurus/theme-common/Details';
|
||||||
|
|
||||||
|
|
|
@ -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, {type ComponentProps, type ReactNode} from 'react';
|
||||||
|
import {TitleFormatterProvider} from '@docusaurus/theme-common/internal';
|
||||||
|
import type {Props} from '@theme/ThemeProvider/TitleFormatter';
|
||||||
|
|
||||||
|
type FormatterProp = ComponentProps<typeof TitleFormatterProvider>['formatter'];
|
||||||
|
|
||||||
|
const formatter: FormatterProp = (params) => {
|
||||||
|
// Add your own title formatting logic here!
|
||||||
|
return params.defaultFormatter(params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ThemeProviderTitleFormatter({
|
||||||
|
children,
|
||||||
|
}: Props): ReactNode {
|
||||||
|
return (
|
||||||
|
<TitleFormatterProvider formatter={formatter}>
|
||||||
|
{children}
|
||||||
|
</TitleFormatterProvider>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* 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, {type ReactNode} from 'react';
|
||||||
|
import TitleFormatterProvider from '@theme/ThemeProvider/TitleFormatter';
|
||||||
|
import type {Props} from '@theme/ThemeProvider';
|
||||||
|
|
||||||
|
export default function ThemeProvider({children}: Props): ReactNode {
|
||||||
|
return <TitleFormatterProvider>{children}</TitleFormatterProvider>;
|
||||||
|
}
|
|
@ -43,7 +43,10 @@ export {
|
||||||
|
|
||||||
export {DEFAULT_SEARCH_TAG} from './utils/searchUtils';
|
export {DEFAULT_SEARCH_TAG} from './utils/searchUtils';
|
||||||
|
|
||||||
export {useTitleFormatter} from './utils/generalUtils';
|
export {
|
||||||
|
TitleFormatterProvider,
|
||||||
|
useTitleFormatter,
|
||||||
|
} from './utils/titleFormatterUtils';
|
||||||
|
|
||||||
export {useLocationChange} from './utils/useLocationChange';
|
export {useLocationChange} from './utils/useLocationChange';
|
||||||
|
|
||||||
|
|
|
@ -1,33 +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 {renderHook} from '@testing-library/react-hooks';
|
|
||||||
import {Context} from '@docusaurus/core/src/client/docusaurusContext';
|
|
||||||
import {useTitleFormatter} from '../generalUtils';
|
|
||||||
import type {DocusaurusContext} from '@docusaurus/types';
|
|
||||||
|
|
||||||
describe('useTitleFormatter', () => {
|
|
||||||
const createUseTitleFormatterMock =
|
|
||||||
(context: DocusaurusContext) => (title?: string) =>
|
|
||||||
renderHook(() => useTitleFormatter(title), {
|
|
||||||
wrapper: ({children}) => (
|
|
||||||
<Context.Provider value={context}>{children}</Context.Provider>
|
|
||||||
),
|
|
||||||
}).result.current;
|
|
||||||
it('works', () => {
|
|
||||||
const mockUseTitleFormatter = createUseTitleFormatterMock({
|
|
||||||
siteConfig: {
|
|
||||||
title: 'my site',
|
|
||||||
titleDelimiter: '·',
|
|
||||||
},
|
|
||||||
} as DocusaurusContext);
|
|
||||||
expect(mockUseTitleFormatter('a page')).toBe('a page · my site');
|
|
||||||
expect(mockUseTitleFormatter(undefined)).toBe('my site');
|
|
||||||
expect(mockUseTitleFormatter(' ')).toBe('my site');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
/**
|
||||||
|
* 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 {TitleFormatterFnDefault} from '../titleFormatterUtils';
|
||||||
|
|
||||||
|
describe('TitleFormatterFnDefault', () => {
|
||||||
|
it('works', () => {
|
||||||
|
expect(
|
||||||
|
TitleFormatterFnDefault({
|
||||||
|
title: 'a page',
|
||||||
|
siteTitle: 'my site',
|
||||||
|
titleDelimiter: '·',
|
||||||
|
}),
|
||||||
|
).toBe('a page · my site');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores empty title', () => {
|
||||||
|
expect(
|
||||||
|
TitleFormatterFnDefault({
|
||||||
|
title: ' ',
|
||||||
|
siteTitle: 'my site',
|
||||||
|
titleDelimiter: '·',
|
||||||
|
}),
|
||||||
|
).toBe('my site');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not duplicate site title', () => {
|
||||||
|
// Users may pass <Layout title={siteTitle}> leading to duplicate titles
|
||||||
|
// By default it's preferable to avoid duplicate siteTitle
|
||||||
|
// See also https://github.com/facebook/docusaurus/issues/5878#issuecomment-961505856
|
||||||
|
expect(
|
||||||
|
TitleFormatterFnDefault({
|
||||||
|
title: 'my site',
|
||||||
|
siteTitle: 'my site',
|
||||||
|
titleDelimiter: '·',
|
||||||
|
}),
|
||||||
|
).toBe('my site');
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,19 +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 useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats the page's title based on relevant site config and other contexts.
|
|
||||||
*/
|
|
||||||
export function useTitleFormatter(title?: string | undefined): string {
|
|
||||||
const {siteConfig} = useDocusaurusContext();
|
|
||||||
const {title: siteTitle, titleDelimiter} = siteConfig;
|
|
||||||
return title?.trim().length
|
|
||||||
? `${title.trim()} ${titleDelimiter} ${siteTitle}`
|
|
||||||
: siteTitle;
|
|
||||||
}
|
|
|
@ -10,7 +10,7 @@ import clsx from 'clsx';
|
||||||
import Head from '@docusaurus/Head';
|
import Head from '@docusaurus/Head';
|
||||||
import useRouteContext from '@docusaurus/useRouteContext';
|
import useRouteContext from '@docusaurus/useRouteContext';
|
||||||
import {useBaseUrlUtils} from '@docusaurus/useBaseUrl';
|
import {useBaseUrlUtils} from '@docusaurus/useBaseUrl';
|
||||||
import {useTitleFormatter} from './generalUtils';
|
import {useTitleFormatter} from './titleFormatterUtils';
|
||||||
|
|
||||||
type PageMetadataProps = {
|
type PageMetadataProps = {
|
||||||
readonly title?: string;
|
readonly title?: string;
|
||||||
|
@ -20,6 +20,55 @@ type PageMetadataProps = {
|
||||||
readonly children?: ReactNode;
|
readonly children?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function TitleMetadata({title}: {title: string}) {
|
||||||
|
const titleFormatter = useTitleFormatter();
|
||||||
|
const formattedTitle = titleFormatter.format(title);
|
||||||
|
return (
|
||||||
|
<Head>
|
||||||
|
<title>{formattedTitle}</title>
|
||||||
|
<meta property="og:title" content={formattedTitle} />
|
||||||
|
</Head>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DescriptionMetadata({description}: {description: string}) {
|
||||||
|
return (
|
||||||
|
<Head>
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
<meta property="og:description" content={description} />
|
||||||
|
</Head>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ImageMetadata({image}: {image: string}) {
|
||||||
|
const {withBaseUrl} = useBaseUrlUtils();
|
||||||
|
const pageImage = withBaseUrl(image, {absolute: true});
|
||||||
|
return (
|
||||||
|
<Head>
|
||||||
|
<meta property="og:image" content={pageImage} />
|
||||||
|
<meta name="twitter:image" content={pageImage} />
|
||||||
|
</Head>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function KeywordsMetadata({
|
||||||
|
keywords,
|
||||||
|
}: {
|
||||||
|
keywords: PageMetadataProps['keywords'];
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Head>
|
||||||
|
<meta
|
||||||
|
name="keywords"
|
||||||
|
content={
|
||||||
|
// https://github.com/microsoft/TypeScript/issues/17002
|
||||||
|
(Array.isArray(keywords) ? keywords.join(',') : keywords) as string
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Head>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper component to manipulate page metadata and override site defaults.
|
* Helper component to manipulate page metadata and override site defaults.
|
||||||
* Works in the same way as Helmet.
|
* Works in the same way as Helmet.
|
||||||
|
@ -31,33 +80,14 @@ export function PageMetadata({
|
||||||
image,
|
image,
|
||||||
children,
|
children,
|
||||||
}: PageMetadataProps): ReactNode {
|
}: PageMetadataProps): ReactNode {
|
||||||
const pageTitle = useTitleFormatter(title);
|
|
||||||
const {withBaseUrl} = useBaseUrlUtils();
|
|
||||||
const pageImage = image ? withBaseUrl(image, {absolute: true}) : undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Head>
|
<>
|
||||||
{title && <title>{pageTitle}</title>}
|
{title && <TitleMetadata title={title} />}
|
||||||
{title && <meta property="og:title" content={pageTitle} />}
|
{description && <DescriptionMetadata description={description} />}
|
||||||
|
{keywords && <KeywordsMetadata keywords={keywords} />}
|
||||||
{description && <meta name="description" content={description} />}
|
{image && <ImageMetadata image={image} />}
|
||||||
{description && <meta property="og:description" content={description} />}
|
{children && <Head>{children}</Head>}
|
||||||
|
</>
|
||||||
{keywords && (
|
|
||||||
<meta
|
|
||||||
name="keywords"
|
|
||||||
content={
|
|
||||||
// https://github.com/microsoft/TypeScript/issues/17002
|
|
||||||
(Array.isArray(keywords) ? keywords.join(',') : keywords) as string
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{pageImage && <meta property="og:image" content={pageImage} />}
|
|
||||||
{pageImage && <meta name="twitter:image" content={pageImage} />}
|
|
||||||
|
|
||||||
{children}
|
|
||||||
</Head>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
/**
|
||||||
|
* 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 {createContext, useContext} from 'react';
|
||||||
|
import type {ReactNode} from 'react';
|
||||||
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||||
|
import useRouteContext from '@docusaurus/useRouteContext';
|
||||||
|
import {ReactContextError} from './reactUtils';
|
||||||
|
|
||||||
|
type TitleFormatterParams = {
|
||||||
|
/**
|
||||||
|
* The page title to format
|
||||||
|
* Usually provided with these APIs:
|
||||||
|
* - <PageMetadata title={title}>
|
||||||
|
* - useTitleFormatter().format(title)
|
||||||
|
*/
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The siteConfig.title value
|
||||||
|
*/
|
||||||
|
siteTitle: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The siteConfig.titleDelimiter value
|
||||||
|
*/
|
||||||
|
titleDelimiter: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The plugin that created the page you are on
|
||||||
|
*/
|
||||||
|
plugin: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the full formatting function, including all useful params
|
||||||
|
* Can be customized through React context with the provider
|
||||||
|
*/
|
||||||
|
export type TitleFormatterFn = (params: TitleFormatterParams) => string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default formatter is provided in params for convenience
|
||||||
|
*/
|
||||||
|
export type TitleFormatterFnWithDefault = (
|
||||||
|
params: TitleFormatterParams & {
|
||||||
|
defaultFormatter: (params: TitleFormatterParams) => string;
|
||||||
|
},
|
||||||
|
) => string;
|
||||||
|
|
||||||
|
export const TitleFormatterFnDefault: TitleFormatterFn = ({
|
||||||
|
title,
|
||||||
|
siteTitle,
|
||||||
|
titleDelimiter,
|
||||||
|
}): string => {
|
||||||
|
const trimmedTitle = title?.trim();
|
||||||
|
if (!trimmedTitle || trimmedTitle === siteTitle) {
|
||||||
|
return siteTitle;
|
||||||
|
}
|
||||||
|
return `${trimmedTitle} ${titleDelimiter} ${siteTitle}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the simpler API exposed to theme/users
|
||||||
|
*/
|
||||||
|
type TitleFormatter = {format: (title: string) => string};
|
||||||
|
|
||||||
|
const TitleFormatterContext = createContext<TitleFormatterFnWithDefault | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
export function TitleFormatterProvider({
|
||||||
|
formatter,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
formatter: TitleFormatterFnWithDefault;
|
||||||
|
}): ReactNode {
|
||||||
|
return (
|
||||||
|
<TitleFormatterContext.Provider value={formatter}>
|
||||||
|
{children}
|
||||||
|
</TitleFormatterContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useTitleFormatterContext() {
|
||||||
|
const value = useContext(TitleFormatterContext);
|
||||||
|
if (value === null) {
|
||||||
|
throw new ReactContextError('TitleFormatterProvider');
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a function to format the page title
|
||||||
|
*/
|
||||||
|
export function useTitleFormatter(): TitleFormatter {
|
||||||
|
const formatter = useTitleFormatterContext();
|
||||||
|
const {siteConfig} = useDocusaurusContext();
|
||||||
|
const {title: siteTitle, titleDelimiter} = siteConfig;
|
||||||
|
|
||||||
|
// Unfortunately we can only call this hook here, not in the provider
|
||||||
|
// Route context can't be accessed in any provider applied above the router
|
||||||
|
const {plugin} = useRouteContext();
|
||||||
|
|
||||||
|
return {
|
||||||
|
format: (title: string) =>
|
||||||
|
formatter({
|
||||||
|
title,
|
||||||
|
siteTitle,
|
||||||
|
titleDelimiter,
|
||||||
|
plugin,
|
||||||
|
defaultFormatter: TitleFormatterFnDefault,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
|
@ -25,11 +25,11 @@ import Link from '@docusaurus/Link';
|
||||||
import {useAllDocsData} from '@docusaurus/plugin-content-docs/client';
|
import {useAllDocsData} from '@docusaurus/plugin-content-docs/client';
|
||||||
import {
|
import {
|
||||||
HtmlClassNameProvider,
|
HtmlClassNameProvider,
|
||||||
|
PageMetadata,
|
||||||
useEvent,
|
useEvent,
|
||||||
usePluralForm,
|
usePluralForm,
|
||||||
useSearchQueryString,
|
useSearchQueryString,
|
||||||
} from '@docusaurus/theme-common';
|
} from '@docusaurus/theme-common';
|
||||||
import {useTitleFormatter} from '@docusaurus/theme-common/internal';
|
|
||||||
import Translate, {translate} from '@docusaurus/Translate';
|
import Translate, {translate} from '@docusaurus/Translate';
|
||||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||||
import {
|
import {
|
||||||
|
@ -160,6 +160,25 @@ type ResultDispatcher =
|
||||||
| {type: 'update'; value: ResultDispatcherState}
|
| {type: 'update'; value: ResultDispatcherState}
|
||||||
| {type: 'advance'; value?: undefined};
|
| {type: 'advance'; value?: undefined};
|
||||||
|
|
||||||
|
function getSearchPageTitle(searchQuery: string | undefined): string {
|
||||||
|
return searchQuery
|
||||||
|
? translate(
|
||||||
|
{
|
||||||
|
id: 'theme.SearchPage.existingResultsTitle',
|
||||||
|
message: 'Search results for "{query}"',
|
||||||
|
description: 'The search page title for non-empty query',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: searchQuery,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: translate({
|
||||||
|
id: 'theme.SearchPage.emptyResultsTitle',
|
||||||
|
message: 'Search the documentation',
|
||||||
|
description: 'The search page title for empty query',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function SearchPageContent(): ReactNode {
|
function SearchPageContent(): ReactNode {
|
||||||
const {
|
const {
|
||||||
i18n: {currentLocale},
|
i18n: {currentLocale},
|
||||||
|
@ -167,12 +186,13 @@ function SearchPageContent(): ReactNode {
|
||||||
const {
|
const {
|
||||||
algolia: {appId, apiKey, indexName, contextualSearch},
|
algolia: {appId, apiKey, indexName, contextualSearch},
|
||||||
} = useAlgoliaThemeConfig();
|
} = useAlgoliaThemeConfig();
|
||||||
|
|
||||||
const processSearchResultUrl = useSearchResultUrlProcessor();
|
const processSearchResultUrl = useSearchResultUrlProcessor();
|
||||||
const documentsFoundPlural = useDocumentsFoundPlural();
|
const documentsFoundPlural = useDocumentsFoundPlural();
|
||||||
|
|
||||||
const docsSearchVersionsHelpers = useDocsSearchVersionsHelpers();
|
const docsSearchVersionsHelpers = useDocsSearchVersionsHelpers();
|
||||||
const [searchQuery, setSearchQuery] = useSearchQueryString();
|
const [searchQuery, setSearchQuery] = useSearchQueryString();
|
||||||
|
const pageTitle = getSearchPageTitle(searchQuery);
|
||||||
|
|
||||||
const initialSearchResultState: ResultDispatcherState = {
|
const initialSearchResultState: ResultDispatcherState = {
|
||||||
items: [],
|
items: [],
|
||||||
query: null,
|
query: null,
|
||||||
|
@ -310,24 +330,6 @@ function SearchPageContent(): ReactNode {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const getTitle = () =>
|
|
||||||
searchQuery
|
|
||||||
? translate(
|
|
||||||
{
|
|
||||||
id: 'theme.SearchPage.existingResultsTitle',
|
|
||||||
message: 'Search results for "{query}"',
|
|
||||||
description: 'The search page title for non-empty query',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
query: searchQuery,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: translate({
|
|
||||||
id: 'theme.SearchPage.emptyResultsTitle',
|
|
||||||
message: 'Search the documentation',
|
|
||||||
description: 'The search page title for empty query',
|
|
||||||
});
|
|
||||||
|
|
||||||
const makeSearch = useEvent((page: number = 0) => {
|
const makeSearch = useEvent((page: number = 0) => {
|
||||||
if (contextualSearch) {
|
if (contextualSearch) {
|
||||||
algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', 'default');
|
algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', 'default');
|
||||||
|
@ -380,8 +382,9 @@ function SearchPageContent(): ReactNode {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
|
<PageMetadata title={pageTitle} />
|
||||||
|
|
||||||
<Head>
|
<Head>
|
||||||
<title>{useTitleFormatter(getTitle())}</title>
|
|
||||||
{/*
|
{/*
|
||||||
We should not index search pages
|
We should not index search pages
|
||||||
See https://github.com/facebook/docusaurus/pull/3233
|
See https://github.com/facebook/docusaurus/pull/3233
|
||||||
|
@ -390,7 +393,7 @@ function SearchPageContent(): ReactNode {
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<div className="container margin-vert--lg">
|
<div className="container margin-vert--lg">
|
||||||
<Heading as="h1">{getTitle()}</Heading>
|
<Heading as="h1">{pageTitle}</Heading>
|
||||||
|
|
||||||
<form className="row" onSubmit={(e) => e.preventDefault()}>
|
<form className="row" onSubmit={(e) => e.preventDefault()}>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -12,6 +12,7 @@ import routes from '@generated/routes';
|
||||||
import {useLocation} from '@docusaurus/router';
|
import {useLocation} from '@docusaurus/router';
|
||||||
import renderRoutes from '@docusaurus/renderRoutes';
|
import renderRoutes from '@docusaurus/renderRoutes';
|
||||||
import Root from '@theme/Root';
|
import Root from '@theme/Root';
|
||||||
|
import ThemeProvider from '@theme/ThemeProvider';
|
||||||
import SiteMetadata from '@theme/SiteMetadata';
|
import SiteMetadata from '@theme/SiteMetadata';
|
||||||
import normalizeLocation from './normalizeLocation';
|
import normalizeLocation from './normalizeLocation';
|
||||||
import {BrowserContextProvider} from './browserContext';
|
import {BrowserContextProvider} from './browserContext';
|
||||||
|
@ -43,10 +44,12 @@ export default function App(): ReactNode {
|
||||||
<DocusaurusContextProvider>
|
<DocusaurusContextProvider>
|
||||||
<BrowserContextProvider>
|
<BrowserContextProvider>
|
||||||
<Root>
|
<Root>
|
||||||
|
<ThemeProvider>
|
||||||
<SiteMetadataDefaults />
|
<SiteMetadataDefaults />
|
||||||
<SiteMetadata />
|
<SiteMetadata />
|
||||||
<BaseUrlIssueBanner />
|
<BaseUrlIssueBanner />
|
||||||
<AppNavigation />
|
<AppNavigation />
|
||||||
|
</ThemeProvider>
|
||||||
</Root>
|
</Root>
|
||||||
<HasHydratedDataAttribute />
|
<HasHydratedDataAttribute />
|
||||||
</BrowserContextProvider>
|
</BrowserContextProvider>
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* 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, {type ReactNode} from 'react';
|
||||||
|
import type {Props} from '@theme/ThemeProvider';
|
||||||
|
|
||||||
|
// Wrapper component expected to be implemented by a theme
|
||||||
|
// Unlike <Layout>, it applies to all sites routes and never unmounts
|
||||||
|
//
|
||||||
|
// Unlike <Root>, the theme is expected to provide an implementation
|
||||||
|
// <Root> is empty and the implementation is expected to be provided by the user
|
||||||
|
//
|
||||||
|
// Tree order: Root > ThemeProvider > Layout
|
||||||
|
export default function ThemeProvider({children}: Props): ReactNode {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
|
@ -35,6 +35,7 @@ exports[`base webpack config creates webpack aliases 1`] = `
|
||||||
"@theme-original/PluginThemeComponentOverriddenByUser": "pluginThemeFolder/PluginThemeComponentOverriddenByUser.js",
|
"@theme-original/PluginThemeComponentOverriddenByUser": "pluginThemeFolder/PluginThemeComponentOverriddenByUser.js",
|
||||||
"@theme-original/Root": "../../../../client/theme-fallback/Root/index.tsx",
|
"@theme-original/Root": "../../../../client/theme-fallback/Root/index.tsx",
|
||||||
"@theme-original/SiteMetadata": "../../../../client/theme-fallback/SiteMetadata/index.tsx",
|
"@theme-original/SiteMetadata": "../../../../client/theme-fallback/SiteMetadata/index.tsx",
|
||||||
|
"@theme-original/ThemeProvider": "../../../../client/theme-fallback/ThemeProvider/index.tsx",
|
||||||
"@theme-original/subfolder/PluginThemeComponent2": "pluginThemeFolder/subfolder/PluginThemeComponent2.js",
|
"@theme-original/subfolder/PluginThemeComponent2": "pluginThemeFolder/subfolder/PluginThemeComponent2.js",
|
||||||
"@theme/Error": "../../../../client/theme-fallback/Error/index.tsx",
|
"@theme/Error": "../../../../client/theme-fallback/Error/index.tsx",
|
||||||
"@theme/Layout": "../../../../client/theme-fallback/Layout/index.tsx",
|
"@theme/Layout": "../../../../client/theme-fallback/Layout/index.tsx",
|
||||||
|
@ -45,6 +46,7 @@ exports[`base webpack config creates webpack aliases 1`] = `
|
||||||
"@theme/PluginThemeComponentOverriddenByUser": "src/theme/PluginThemeComponentOverriddenByUser.js",
|
"@theme/PluginThemeComponentOverriddenByUser": "src/theme/PluginThemeComponentOverriddenByUser.js",
|
||||||
"@theme/Root": "../../../../client/theme-fallback/Root/index.tsx",
|
"@theme/Root": "../../../../client/theme-fallback/Root/index.tsx",
|
||||||
"@theme/SiteMetadata": "../../../../client/theme-fallback/SiteMetadata/index.tsx",
|
"@theme/SiteMetadata": "../../../../client/theme-fallback/SiteMetadata/index.tsx",
|
||||||
|
"@theme/ThemeProvider": "../../../../client/theme-fallback/ThemeProvider/index.tsx",
|
||||||
"@theme/UserThemeComponent1": "src/theme/UserThemeComponent1.js",
|
"@theme/UserThemeComponent1": "src/theme/UserThemeComponent1.js",
|
||||||
"@theme/subfolder/PluginThemeComponent2": "pluginThemeFolder/subfolder/PluginThemeComponent2.js",
|
"@theme/subfolder/PluginThemeComponent2": "pluginThemeFolder/subfolder/PluginThemeComponent2.js",
|
||||||
"@theme/subfolder/UserThemeComponent2": "src/theme/subfolder/UserThemeComponent2.js",
|
"@theme/subfolder/UserThemeComponent2": "src/theme/subfolder/UserThemeComponent2.js",
|
||||||
|
|
|
@ -79,6 +79,10 @@ exports[`loadThemeAliases next alias can override the previous alias 1`] = `
|
||||||
"@theme-original/SiteMetadata",
|
"@theme-original/SiteMetadata",
|
||||||
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/SiteMetadata/index.tsx",
|
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/SiteMetadata/index.tsx",
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"@theme-original/ThemeProvider",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/ThemeProvider/index.tsx",
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"@theme/Error",
|
"@theme/Error",
|
||||||
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/Error/index.tsx",
|
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/Error/index.tsx",
|
||||||
|
@ -127,5 +131,9 @@ exports[`loadThemeAliases next alias can override the previous alias 1`] = `
|
||||||
"@theme/SiteMetadata",
|
"@theme/SiteMetadata",
|
||||||
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/SiteMetadata/index.tsx",
|
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/SiteMetadata/index.tsx",
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"@theme/ThemeProvider",
|
||||||
|
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/ThemeProvider/index.tsx",
|
||||||
|
],
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
36
website/src/theme/ThemeProvider/TitleFormatter/index.tsx
Normal file
36
website/src/theme/ThemeProvider/TitleFormatter/index.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* 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, {type ComponentProps, type ReactNode} from 'react';
|
||||||
|
import {TitleFormatterProvider} from '@docusaurus/theme-common/internal';
|
||||||
|
import type {Props} from '@theme/ThemeProvider/TitleFormatter';
|
||||||
|
|
||||||
|
type FormatterProp = ComponentProps<typeof TitleFormatterProvider>['formatter'];
|
||||||
|
|
||||||
|
const formatter: FormatterProp = (params) => {
|
||||||
|
// Custom title for dogfood plugin instances
|
||||||
|
if (params.plugin.id.endsWith('tests')) {
|
||||||
|
const pluginLabel = `${params.plugin.name.replace(
|
||||||
|
'docusaurus-plugin-content-',
|
||||||
|
'',
|
||||||
|
)} plugin`;
|
||||||
|
return `🐕 Dogfood - ${pluginLabel}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default title otherwise
|
||||||
|
return params.defaultFormatter(params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ThemeProviderTitleFormatter({
|
||||||
|
children,
|
||||||
|
}: Props): ReactNode {
|
||||||
|
return (
|
||||||
|
<TitleFormatterProvider formatter={formatter}>
|
||||||
|
{children}
|
||||||
|
</TitleFormatterProvider>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue