Merge branch 'main' into tinyglobby

This commit is contained in:
Ben McCann 2025-04-19 12:06:31 -07:00
commit 86303b31eb
59 changed files with 786 additions and 337 deletions

View File

@ -30,7 +30,7 @@ jobs:
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: lts/*

View File

@ -24,7 +24,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Node
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: lts/*
cache: yarn

View File

@ -27,7 +27,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Node
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: lts/*
cache: yarn

View File

@ -43,7 +43,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Node
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: lts/*
cache: yarn
@ -75,7 +75,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Node
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: lts/*
cache: yarn

View File

@ -24,7 +24,7 @@ jobs:
with:
fetch-depth: 0 # Needed to get the commit number with "git rev-list --count HEAD"
- name: Set up Node
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: lts/*
cache: yarn

View File

@ -21,7 +21,7 @@ jobs:
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Node
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: lts/*
cache: yarn

View File

@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: lts/*
@ -64,7 +64,7 @@ jobs:
- name: Add Lighthouse stats as comment
id: comment_to_pr
uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # 2.9.1
uses: marocchino/sticky-pull-request-comment@67d0dec7b07ed060a405f9b2a64b8ab319fdd7db # 2.9.2
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
number: ${{ github.event.pull_request.number }}

View File

@ -22,7 +22,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Node
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: lts/*
cache: yarn

View File

@ -24,7 +24,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Node
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: lts/*
cache: yarn

View File

@ -43,7 +43,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ matrix.node }}
cache: yarn
@ -84,7 +84,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js LTS
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: lts/*
cache: yarn
@ -153,7 +153,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js LTS
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: lts/*
cache: yarn
@ -193,7 +193,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js LTS
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: lts/*
cache: yarn

View File

@ -28,7 +28,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up Node LTS
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: lts/*
cache: yarn

View File

@ -34,7 +34,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ matrix.node }}
- name: Installation

View File

@ -32,7 +32,7 @@ jobs:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ matrix.node }}
cache: yarn

View File

@ -13,7 +13,8 @@
"pr: polish": ":nail_care: Polish",
"pr: documentation": ":memo: Documentation",
"pr: dependencies": ":robot: Dependencies",
"pr: maintenance": ":wrench: Maintenance"
"pr: maintenance": ":wrench: Maintenance",
"pr: translations": ":globe_with_meridians: Translations"
},
"cacheDir": ".changelog"
}

View File

@ -25,6 +25,7 @@ const plugin: Plugin<unknown[], Root> = function plugin(): Transformer<Root> {
node.data.hProperties = node.data.hProperties || {};
node.data.hProperties.metastring = node.meta;
// TODO Docusaurus v4: remove special case
// Retrocompatible support for live codeblock metastring
// Not really the appropriate place to handle that :s
node.data.hProperties.live = node.meta?.split(' ').includes('live');

View File

@ -122,6 +122,15 @@ declare module '@theme/Root' {
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' {
import type {ReactNode} from 'react';

View File

@ -71,7 +71,7 @@
<div class="blog-posts">
<xsl:for-each select="atom:feed/atom:entry">
<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"
/></a></h3>
<div class="blog-post-date">

View File

@ -1,5 +1,7 @@
---
title: MDX page
description: my MDX page
slug: /custom-mdx/slug
---
MDX page

View File

@ -32,11 +32,12 @@ exports[`docusaurus-plugin-content-pages loads simple pages 1`] = `
"frontMatter": {
"custom_frontMatter": "added by parseFrontMatter",
"description": "my MDX page",
"slug": "/custom-mdx/slug",
"title": "MDX page",
},
"lastUpdatedAt": undefined,
"lastUpdatedBy": undefined,
"permalink": "/hello/mdxPage",
"permalink": "/custom-mdx/slug",
"source": "@site/src/pages/hello/mdxPage.mdx",
"title": "MDX page",
"type": "mdx",
@ -101,11 +102,12 @@ exports[`docusaurus-plugin-content-pages loads simple pages with french translat
"frontMatter": {
"custom_frontMatter": "added by parseFrontMatter",
"description": "my MDX page",
"slug": "/custom-mdx/slug",
"title": "MDX page",
},
"lastUpdatedAt": undefined,
"lastUpdatedBy": undefined,
"permalink": "/fr/hello/mdxPage",
"permalink": "/fr/custom-mdx/slug",
"source": "@site/src/pages/hello/mdxPage.mdx",
"title": "MDX page",
"type": "mdx",
@ -170,11 +172,12 @@ exports[`docusaurus-plugin-content-pages loads simple pages with last update 1`]
"frontMatter": {
"custom_frontMatter": "added by parseFrontMatter",
"description": "my MDX page",
"slug": "/custom-mdx/slug",
"title": "MDX page",
},
"lastUpdatedAt": 1539502055000,
"lastUpdatedBy": "Author",
"permalink": "/hello/mdxPage",
"permalink": "/custom-mdx/slug",
"source": "@site/src/pages/hello/mdxPage.mdx",
"title": "MDX page",
"type": "mdx",

View File

@ -106,12 +106,13 @@ async function processPageSourceFile(
const source = path.join(contentPath, relativeSource);
const aliasedSourcePath = aliasedSitePath(source, siteDir);
const permalink = normalizeUrl([
baseUrl,
options.routeBasePath,
encodePath(fileToPath(relativeSource)),
]);
const filenameSlug = encodePath(fileToPath(relativeSource));
if (!isMarkdownSource(relativeSource)) {
// For now, slug can't be customized for JSX pages
const slug = filenameSlug;
const permalink = normalizeUrl([baseUrl, options.routeBasePath, slug]);
return {
type: 'jsx',
permalink,
@ -131,6 +132,9 @@ async function processPageSourceFile(
});
const frontMatter = validatePageFrontMatter(unsafeFrontMatter);
const slug = frontMatter.slug ?? filenameSlug;
const permalink = normalizeUrl([baseUrl, options.routeBasePath, slug]);
const pagesDirPath = await getFolderContainingFile(
getContentPathList(contentPaths),
relativeSource,

View File

@ -22,6 +22,7 @@ const PageFrontMatterSchema = Joi.object<PageFrontMatter>({
description: Joi.string().allow(''),
keywords: Joi.array().items(Joi.string().required()),
image: URISchema,
slug: Joi.string(),
wrapperClassName: Joi.string(),
hide_table_of_contents: Joi.boolean(),
...FrontMatterTOCHeadingLevels,

View File

@ -37,6 +37,7 @@ declare module '@docusaurus/plugin-content-pages' {
readonly title?: string;
readonly description?: string;
readonly image?: string;
readonly slug?: string;
readonly keywords?: string[];
readonly wrapperClassName?: string;
readonly hide_table_of_contents?: string;

View File

@ -1569,6 +1569,17 @@ declare module '@theme/ThemedImage' {
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' {
import {Details, type DetailsProps} from '@docusaurus/theme-common/Details';

View File

@ -20,6 +20,7 @@ export default function Tag({
}: Props): ReactNode {
return (
<Link
rel="tag"
href={permalink}
title={description}
className={clsx(

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, {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>
);
}

View File

@ -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>;
}

View File

@ -43,7 +43,10 @@ export {
export {DEFAULT_SEARCH_TAG} from './utils/searchUtils';
export {useTitleFormatter} from './utils/generalUtils';
export {
TitleFormatterProvider,
useTitleFormatter,
} from './utils/titleFormatterUtils';
export {useLocationChange} from './utils/useLocationChange';

View File

@ -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');
});
});

View File

@ -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');
});
});

View File

@ -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;
}

View File

@ -10,7 +10,7 @@ import clsx from 'clsx';
import Head from '@docusaurus/Head';
import useRouteContext from '@docusaurus/useRouteContext';
import {useBaseUrlUtils} from '@docusaurus/useBaseUrl';
import {useTitleFormatter} from './generalUtils';
import {useTitleFormatter} from './titleFormatterUtils';
type PageMetadataProps = {
readonly title?: string;
@ -20,6 +20,55 @@ type PageMetadataProps = {
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.
* Works in the same way as Helmet.
@ -31,33 +80,14 @@ export function PageMetadata({
image,
children,
}: PageMetadataProps): ReactNode {
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={
// 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>
<>
{title && <TitleMetadata title={title} />}
{description && <DescriptionMetadata description={description} />}
{keywords && <KeywordsMetadata keywords={keywords} />}
{image && <ImageMetadata image={image} />}
{children && <Head>{children}</Head>}
</>
);
}

View File

@ -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,
}),
};
}

View File

@ -104,6 +104,7 @@ export type TableOfContents = {
maxHeadingLevel: number;
};
// TODO Docusaurus v4: use interface + declaration merging to enhance
// Theme config after validation/normalization
export type ThemeConfig = {
docs: {

View File

@ -16,6 +16,14 @@ declare module '@docusaurus/theme-live-codeblock' {
};
}
declare module '@theme/LiveCodeBlock' {
import type {Props as BaseProps} from '@theme/CodeBlock';
export interface Props extends BaseProps {}
export default function LiveCodeBlock(props: Props): ReactNode;
}
declare module '@theme/Playground' {
import type {ReactNode} from 'react';
import type {Props as BaseProps} from '@theme/CodeBlock';
@ -31,6 +39,64 @@ declare module '@theme/Playground' {
export default function Playground(props: LiveProviderProps): ReactNode;
}
declare module '@theme/Playground/Provider' {
import type {ReactNode} from 'react';
import type {Props as PlaygroundProps} from '@theme/Playground';
export interface Props extends Omit<PlaygroundProps, 'children'> {
code: string | undefined;
children: ReactNode;
}
export default function PlaygroundProvider(props: Props): ReactNode;
}
declare module '@theme/Playground/Container' {
import type {ReactNode} from 'react';
export interface Props {
children: ReactNode;
}
export default function PlaygroundContainer(props: Props): ReactNode;
}
declare module '@theme/Playground/Layout' {
import type {ReactNode} from 'react';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface Props {}
export default function PlaygroundLayout(props: Props): ReactNode;
}
declare module '@theme/Playground/Preview' {
import type {ReactNode} from 'react';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface Props {}
export default function PlaygroundPreview(props: Props): ReactNode;
}
declare module '@theme/Playground/Editor' {
import type {ReactNode} from 'react';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface Props {}
export default function PlaygroundEditor(props: Props): ReactNode;
}
declare module '@theme/Playground/Header' {
import type {ReactNode} from 'react';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface Props {}
export default function PlaygroundHeader(props: Props): ReactNode;
}
declare module '@theme/ReactLiveScope' {
type Scope = {
[key: string]: unknown;

View File

@ -5,21 +5,28 @@
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import Playground from '@theme/Playground';
import ReactLiveScope from '@theme/ReactLiveScope';
import CodeBlock, {type Props} from '@theme-init/CodeBlock';
import React, {type ReactNode} from 'react';
import type {Props as CodeBlockProps} from '@theme/CodeBlock';
import OriginalCodeBlock from '@theme-init/CodeBlock';
import LiveCodeBlock from '@theme/LiveCodeBlock';
const withLiveEditor = (Component: typeof CodeBlock) => {
function WrappedComponent(props: Props) {
if (props.live) {
return <Playground scope={ReactLiveScope} {...props} />;
}
return <Component {...props} />;
// TODO Docusaurus v4: remove special case
// see packages/docusaurus-mdx-loader/src/remark/mdx1Compat/codeCompatPlugin.ts
// we can just use the metastring instead
declare module '@theme/CodeBlock' {
interface Props {
live?: boolean;
}
}
return WrappedComponent;
};
function isLiveCodeBlock(props: CodeBlockProps): boolean {
return !!props.live;
}
export default withLiveEditor(CodeBlock);
export default function CodeBlockEnhancer(props: CodeBlockProps): ReactNode {
return isLiveCodeBlock(props) ? (
<LiveCodeBlock {...props} />
) : (
<OriginalCodeBlock {...props} />
);
}

View File

@ -0,0 +1,15 @@
/**
* 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 Playground from '@theme/Playground';
import ReactLiveScope from '@theme/ReactLiveScope';
import type {Props} from '@theme/LiveCodeBlock';
export default function LiveCodeBlock(props: Props): ReactNode {
return <Playground scope={ReactLiveScope} {...props} />;
}

View File

@ -0,0 +1,16 @@
/**
* 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/Playground/Container';
import styles from './styles.module.css';
export default function PlaygroundContainer({children}: Props): ReactNode {
return <div className={styles.playgroundContainer}>{children}</div>;
}

View File

@ -0,0 +1,13 @@
/**
* 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.
*/
.playgroundContainer {
margin-bottom: var(--ifm-leading);
border-radius: var(--ifm-global-radius);
box-shadow: var(--ifm-global-shadow-lw);
overflow: hidden;
}

View File

@ -0,0 +1,35 @@
/**
* 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 {LiveEditor} from 'react-live';
import useIsBrowser from '@docusaurus/useIsBrowser';
import Translate from '@docusaurus/Translate';
import PlaygroundHeader from '@theme/Playground/Header';
import styles from './styles.module.css';
export default function PlaygroundEditor(): ReactNode {
const isBrowser = useIsBrowser();
return (
<>
<PlaygroundHeader>
<Translate
id="theme.Playground.liveEditor"
description="The live editor label of the live codeblocks">
Live Editor
</Translate>
</PlaygroundHeader>
<LiveEditor
// We force remount the editor on hydration,
// otherwise dark prism theme is not applied
key={String(isBrowser)}
className={styles.playgroundEditor}
/>
</>
);
}

View File

@ -0,0 +1,17 @@
/**
* 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.
*/
.playgroundEditor {
font: var(--ifm-code-font-size) / var(--ifm-pre-line-height)
var(--ifm-font-family-monospace) !important;
/* rtl:ignore */
direction: ltr;
}
.playgroundEditor pre {
border-radius: 0;
}

View File

@ -0,0 +1,19 @@
/**
* 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 clsx from 'clsx';
import styles from './styles.module.css';
export default function PlaygroundHeader({
children,
}: {
children: ReactNode;
}): ReactNode {
return <div className={clsx(styles.playgroundHeader)}>{children}</div>;
}

View File

@ -5,13 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/
.playgroundContainer {
margin-bottom: var(--ifm-leading);
border-radius: var(--ifm-global-radius);
box-shadow: var(--ifm-global-shadow-lw);
overflow: hidden;
}
.playgroundHeader {
letter-spacing: 0.08rem;
padding: 0.75rem;
@ -26,19 +19,3 @@
background: var(--ifm-color-emphasis-600);
color: var(--ifm-color-content-inverse);
}
.playgroundEditor {
font: var(--ifm-code-font-size) / var(--ifm-pre-line-height)
var(--ifm-font-family-monospace) !important;
/* rtl:ignore */
direction: ltr;
}
.playgroundEditor pre {
border-radius: 0;
}
.playgroundPreview {
padding: 1rem;
background-color: var(--ifm-pre-background);
}

View File

@ -0,0 +1,37 @@
/**
* 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 {useThemeConfig} from '@docusaurus/theme-common';
import PlaygroundPreview from '@theme/Playground/Preview';
import PlaygroundEditor from '@theme/Playground/Editor';
import type {ThemeConfig} from '@docusaurus/theme-live-codeblock';
function useLiveCodeBlockThemeConfig() {
const themeConfig = useThemeConfig() as unknown as ThemeConfig;
return themeConfig.liveCodeBlock;
}
export default function PlaygroundLayout(): ReactNode {
const {playgroundPosition} = useLiveCodeBlockThemeConfig();
return (
<>
{playgroundPosition === 'top' ? (
<>
<PlaygroundPreview />
<PlaygroundEditor />
</>
) : (
<>
<PlaygroundEditor />
<PlaygroundPreview />
</>
)}
</>
);
}

View File

@ -0,0 +1,59 @@
/**
* 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 {LiveError, LivePreview} from 'react-live';
import BrowserOnly from '@docusaurus/BrowserOnly';
import {ErrorBoundaryErrorMessageFallback} from '@docusaurus/theme-common';
import ErrorBoundary from '@docusaurus/ErrorBoundary';
import Translate from '@docusaurus/Translate';
import PlaygroundHeader from '@theme/Playground/Header';
import styles from './styles.module.css';
function Loader() {
// Is it worth improving/translating?
// eslint-disable-next-line @docusaurus/no-untranslated-text
return <div>Loading...</div>;
}
function PlaygroundLivePreview(): ReactNode {
// No SSR for the live preview
// See https://github.com/facebook/docusaurus/issues/5747
return (
<BrowserOnly fallback={<Loader />}>
{() => (
<>
<ErrorBoundary
fallback={(params) => (
<ErrorBoundaryErrorMessageFallback {...params} />
)}>
<LivePreview />
</ErrorBoundary>
<LiveError />
</>
)}
</BrowserOnly>
);
}
export default function PlaygroundPreview(): ReactNode {
return (
<>
<PlaygroundHeader>
<Translate
id="theme.Playground.result"
description="The result label of the live codeblocks">
Result
</Translate>
</PlaygroundHeader>
<div className={styles.playgroundPreview}>
<PlaygroundLivePreview />
</div>
</>
);
}

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.
*/
.playgroundPreview {
padding: 1rem;
background-color: var(--ifm-pre-background);
}

View File

@ -0,0 +1,35 @@
/**
* 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 {LiveProvider} from 'react-live';
import {usePrismTheme} from '@docusaurus/theme-common';
import type {Props} from '@theme/Playground/Provider';
// this should rather be a stable function
// see https://github.com/facebook/docusaurus/issues/9630#issuecomment-1855682643
const DEFAULT_TRANSFORM_CODE = (code: string) => `${code};`;
export default function PlaygroundProvider({
code,
children,
...props
}: Props): ReactNode {
const prismTheme = usePrismTheme();
const noInline = props.metastring?.includes('noInline') ?? false;
return (
<LiveProvider
noInline={noInline}
theme={prismTheme}
{...props}
code={code?.replace(/\n$/, '')}
transformCode={props.transformCode ?? DEFAULT_TRANSFORM_CODE}>
{children}
</LiveProvider>
);
}

View File

@ -6,137 +6,22 @@
*/
import React, {type ReactNode} from 'react';
import clsx from 'clsx';
import useIsBrowser from '@docusaurus/useIsBrowser';
import {LiveProvider, LiveEditor, LiveError, LivePreview} from 'react-live';
import Translate from '@docusaurus/Translate';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import BrowserOnly from '@docusaurus/BrowserOnly';
import {
ErrorBoundaryErrorMessageFallback,
usePrismTheme,
} from '@docusaurus/theme-common';
import ErrorBoundary from '@docusaurus/ErrorBoundary';
import PlaygroundProvider from '@theme/Playground/Provider';
import PlaygroundContainer from '@theme/Playground/Container';
import PlaygroundLayout from '@theme/Playground/Layout';
import type {Props} from '@theme/Playground';
import type {ThemeConfig} from '@docusaurus/theme-live-codeblock';
import styles from './styles.module.css';
function Header({children}: {children: ReactNode}) {
return <div className={clsx(styles.playgroundHeader)}>{children}</div>;
}
function LivePreviewLoader() {
// Is it worth improving/translating?
// eslint-disable-next-line @docusaurus/no-untranslated-text
return <div>Loading...</div>;
}
function Preview() {
// No SSR for the live preview
// See https://github.com/facebook/docusaurus/issues/5747
return (
<BrowserOnly fallback={<LivePreviewLoader />}>
{() => (
<>
<ErrorBoundary
fallback={(params) => (
<ErrorBoundaryErrorMessageFallback {...params} />
)}>
<LivePreview />
</ErrorBoundary>
<LiveError />
</>
)}
</BrowserOnly>
);
}
function ResultWithHeader() {
return (
<>
<Header>
<Translate
id="theme.Playground.result"
description="The result label of the live codeblocks">
Result
</Translate>
</Header>
{/* https://github.com/facebook/docusaurus/issues/5747 */}
<div className={styles.playgroundPreview}>
<Preview />
</div>
</>
);
}
function ThemedLiveEditor() {
const isBrowser = useIsBrowser();
return (
<LiveEditor
// We force remount the editor on hydration,
// otherwise dark prism theme is not applied
key={String(isBrowser)}
className={styles.playgroundEditor}
/>
);
}
function EditorWithHeader() {
return (
<>
<Header>
<Translate
id="theme.Playground.liveEditor"
description="The live editor label of the live codeblocks">
Live Editor
</Translate>
</Header>
<ThemedLiveEditor />
</>
);
}
// this should rather be a stable function
// see https://github.com/facebook/docusaurus/issues/9630#issuecomment-1855682643
const DEFAULT_TRANSFORM_CODE = (code: string) => `${code};`;
export default function Playground({
children,
transformCode,
...props
}: Props): ReactNode {
const {
siteConfig: {themeConfig},
} = useDocusaurusContext();
const {
liveCodeBlock: {playgroundPosition},
} = themeConfig as ThemeConfig;
const prismTheme = usePrismTheme();
const noInline = props.metastring?.includes('noInline') ?? false;
return (
<div className={styles.playgroundContainer}>
<LiveProvider
code={children?.replace(/\n$/, '')}
noInline={noInline}
transformCode={transformCode ?? DEFAULT_TRANSFORM_CODE}
theme={prismTheme}
{...props}>
{playgroundPosition === 'top' ? (
<>
<ResultWithHeader />
<EditorWithHeader />
</>
) : (
<>
<EditorWithHeader />
<ResultWithHeader />
</>
)}
</LiveProvider>
</div>
<PlaygroundContainer>
<PlaygroundProvider code={children} {...props}>
<PlaygroundLayout />
</PlaygroundProvider>
</PlaygroundContainer>
);
}

View File

@ -1,20 +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.
*/
/// <reference types="@docusaurus/theme-classic" />
/// <reference types="@docusaurus/module-type-aliases" />
declare module '@theme-init/CodeBlock' {
import type CodeBlock from '@theme/CodeBlock';
import type {Props as BaseProps} from '@theme/CodeBlock';
export interface Props extends BaseProps {
live?: boolean;
}
const CodeBlockComp: typeof CodeBlock;
export default CodeBlockComp;
}

View File

@ -25,11 +25,11 @@ import Link from '@docusaurus/Link';
import {useAllDocsData} from '@docusaurus/plugin-content-docs/client';
import {
HtmlClassNameProvider,
PageMetadata,
useEvent,
usePluralForm,
useSearchQueryString,
} from '@docusaurus/theme-common';
import {useTitleFormatter} from '@docusaurus/theme-common/internal';
import Translate, {translate} from '@docusaurus/Translate';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import {
@ -160,6 +160,25 @@ type ResultDispatcher =
| {type: 'update'; value: ResultDispatcherState}
| {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 {
const {
i18n: {currentLocale},
@ -167,12 +186,13 @@ function SearchPageContent(): ReactNode {
const {
algolia: {appId, apiKey, indexName, contextualSearch},
} = useAlgoliaThemeConfig();
const processSearchResultUrl = useSearchResultUrlProcessor();
const documentsFoundPlural = useDocumentsFoundPlural();
const docsSearchVersionsHelpers = useDocsSearchVersionsHelpers();
const [searchQuery, setSearchQuery] = useSearchQueryString();
const pageTitle = getSearchPageTitle(searchQuery);
const initialSearchResultState: ResultDispatcherState = {
items: [],
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) => {
if (contextualSearch) {
algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', 'default');
@ -380,8 +382,9 @@ function SearchPageContent(): ReactNode {
return (
<Layout>
<PageMetadata title={pageTitle} />
<Head>
<title>{useTitleFormatter(getTitle())}</title>
{/*
We should not index search pages
See https://github.com/facebook/docusaurus/pull/3233
@ -390,7 +393,7 @@ function SearchPageContent(): ReactNode {
</Head>
<div className="container margin-vert--lg">
<Heading as="h1">{getTitle()}</Heading>
<Heading as="h1">{pageTitle}</Heading>
<form className="row" onSubmit={(e) => e.preventDefault()}>
<div

View File

@ -12,6 +12,7 @@ import routes from '@generated/routes';
import {useLocation} from '@docusaurus/router';
import renderRoutes from '@docusaurus/renderRoutes';
import Root from '@theme/Root';
import ThemeProvider from '@theme/ThemeProvider';
import SiteMetadata from '@theme/SiteMetadata';
import normalizeLocation from './normalizeLocation';
import {BrowserContextProvider} from './browserContext';
@ -43,10 +44,12 @@ export default function App(): ReactNode {
<DocusaurusContextProvider>
<BrowserContextProvider>
<Root>
<SiteMetadataDefaults />
<SiteMetadata />
<BaseUrlIssueBanner />
<AppNavigation />
<ThemeProvider>
<SiteMetadataDefaults />
<SiteMetadata />
<BaseUrlIssueBanner />
<AppNavigation />
</ThemeProvider>
</Root>
<HasHydratedDataAttribute />
</BrowserContextProvider>

View File

@ -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}</>;
}

View File

@ -35,6 +35,7 @@ exports[`base webpack config creates webpack aliases 1`] = `
"@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/ThemeProvider": "../../../../client/theme-fallback/ThemeProvider/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",
@ -45,6 +46,7 @@ exports[`base webpack config creates webpack aliases 1`] = `
"@theme/PluginThemeComponentOverriddenByUser": "src/theme/PluginThemeComponentOverriddenByUser.js",
"@theme/Root": "../../../../client/theme-fallback/Root/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/subfolder/PluginThemeComponent2": "pluginThemeFolder/subfolder/PluginThemeComponent2.js",
"@theme/subfolder/UserThemeComponent2": "src/theme/subfolder/UserThemeComponent2.js",

View File

@ -79,6 +79,10 @@ exports[`loadThemeAliases next alias can override the previous alias 1`] = `
"@theme-original/SiteMetadata",
"<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",
"<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",
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/SiteMetadata/index.tsx",
],
[
"@theme/ThemeProvider",
"<PROJECT_ROOT>/packages/docusaurus/src/client/theme-fallback/ThemeProvider/index.tsx",
],
]
`;

View File

@ -25,9 +25,6 @@ beforeinstallprompt
Bhatt
Blockquotes
blockquotes
BLUESKY
Bluesky
bluesky
Bokmål
bunx
caabernathy
@ -36,14 +33,12 @@ cdabcdab
cdpath
Cena
cena
changefreq
Chedeau
chedeau
Clément
Codegen
codegen
codesandbox
Codespaces
commonmark
contravariance
corejs
@ -61,9 +56,6 @@ dedup
devto
dingers
Dmitry
Docsearch
docsearch
Docsify
Docu
docu
docusuarus
@ -117,8 +109,6 @@ Héctor
héllô
IANAD
Infima
infima
Infima's
inlines
interactiveness
Interpolatable
@ -140,14 +130,11 @@ Knapen
Koyeb
Koyeb's
Lamana
Lastmod
lastmod
Lifecycles
lifecycles
lightningcss
Linkify
linkify
lockb
Lorber
lorber
Lorber's
@ -196,7 +183,6 @@ navigations
navlink
netrc
newtab
Nextra
ngryman
Nisarag
noflash
@ -230,16 +216,12 @@ paraiso
pathinfo
paularmstrong
philpl
Photoshop
photoshop
Pipeable
playbtn
Plushie
plushie
plushies
posthog
Precache
precache
precached
precaching
preconfigured
@ -310,11 +292,9 @@ stackoverflow
Stormkit
Strikethrough
strikethroughs
stylelintrc
sublabel
sublicensable
sublist
subpage
subsetting
subsubcategory
subsubfolder

View File

@ -37,7 +37,7 @@ import Readme from "../README.mdx"
- [Tabs tests](/tests/pages/tabs-tests)
- [z-index tests](/tests/pages/z-index-tests)
- [Head metadata tests](/tests/pages/head-metadata)
- [Unlisted page](/tests/pages/unlisted)
- [Unlisted page](/tests/pages/my-custom/unlisted/slug)
- [Analytics](/tests/pages/analytics)
- [History tests](/tests/pages/history-tests)
- [Embeds](/tests/pages/embeds)

View File

@ -1,5 +1,6 @@
---
unlisted: true
slug: /my-custom/unlisted/slug
---
# Unlisted page

View File

@ -113,6 +113,7 @@ Accepted fields:
| `description` | `string` | The first line of Markdown content | The description of your page, which will become the `<meta name="description" content="..."/>` and `<meta property="og:description" content="..."/>` in `<head>`, used by search engines. |
| `keywords` | `string[]` | `undefined` | Keywords meta tag, which will become the `<meta name="keywords" content="keyword1,keyword2,..."/>` in `<head>`, used by search engines. |
| `image` | `string` | `undefined` | Cover or thumbnail image that will be used as the `<meta property="og:image" content="..."/>` in the `<head>`, enhancing link previews on social media and messaging platforms. |
| `slug` | `string` | File path | Allows to customize the page URL (`/<routeBasePath>/<slug>`). Support multiple patterns: `slug: my-page`, `slug: /my/page`, slug: `/`. |
| `wrapperClassName` | `string` | | Class name to be added to the wrapper element to allow targeting specific page content. |
| `hide_table_of_contents` | `boolean` | `false` | Whether to hide the table of contents to the right. |
| `draft` | `boolean` | `false` | Draft pages will only be available during development. |
@ -131,6 +132,7 @@ description: Markdown page SEO description
wrapperClassName: markdown-page
hide_table_of_contents: false
draft: true
slug: /markdown-page
---
Markdown page content

View File

@ -67,11 +67,11 @@ image: https://i.imgur.com/mErPwqL.png
hide_table_of_contents: false
---
Welcome to this blog. This blog is created with [**Docusaurus 2**](https://docusaurus.io/).
Welcome to this blog. This blog is created with [**Docusaurus**](https://docusaurus.io/).
<!-- truncate -->
This is my first post on Docusaurus 2.
This is my first post on Docusaurus.
A whole bunch of exploration to follow.
```

View 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>
);
}