mirror of
https://github.com/facebook/docusaurus.git
synced 2025-12-25 17:22:50 +00:00
Compare commits
13 Commits
227ebce44d
...
8be3094178
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8be3094178 | ||
|
|
5bc5c90dc7 | ||
|
|
ee9dfd5d0b | ||
|
|
7f5d6122d2 | ||
|
|
47a98a1d6e | ||
|
|
75a529bb8f | ||
|
|
acd96cb3f0 | ||
|
|
d53136d59c | ||
|
|
6206a99c10 | ||
|
|
67c3c20db0 | ||
|
|
47cc3e62e8 | ||
|
|
628995e14e | ||
|
|
948ef8e4f3 |
|
|
@ -44,20 +44,20 @@ jobs:
|
|||
# BASE_URL: '/docusaurus/' # hash router +
|
||||
|
||||
- name: Upload Website artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: website-hash-router-archive
|
||||
path: website/build
|
||||
|
||||
#- name: Upload Website Pages artifact
|
||||
# uses: actions/upload-pages-artifact@v4
|
||||
# uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0
|
||||
# with:
|
||||
# path: website/build
|
||||
|
||||
# Deploy to https://facebook.github.io/docusaurus/
|
||||
- name: Deploy to GitHub Pages
|
||||
if: ${{ github.event_name != 'pull_request' && github.ref_name == 'main' }}
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: website/build
|
||||
|
|
@ -81,4 +81,4 @@ jobs:
|
|||
# steps:
|
||||
# - name: Deploy to GitHub Pages
|
||||
# id: deployment
|
||||
# uses: actions/deploy-pages@v4
|
||||
# uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ jobs:
|
|||
node-version: lts/*
|
||||
cache: yarn
|
||||
- name: Track build size changes
|
||||
uses: preactjs/compressed-size-action@946a292cd35bd1088e0d7eb92b69d1a8d5b5d76a # v2
|
||||
uses: preactjs/compressed-size-action@8518045ed95e94e971b83333085e1cb99aa18aa8 # v2.9.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
build-script: build:website:fast
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ jobs:
|
|||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.1
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
ref: ${{ github.head_ref }}
|
||||
|
|
@ -42,6 +42,6 @@ jobs:
|
|||
- name: Print Diff
|
||||
run: git diff
|
||||
|
||||
- uses: stefanzweifel/git-auto-commit-action@v7
|
||||
- uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
|
||||
with:
|
||||
commit_message: 'refactor: apply lint autofix'
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ jobs:
|
|||
DOCUSAURUS_PERF_LOGGER: 'true'
|
||||
working-directory: test-website-in-workspace
|
||||
- name: Upload Website artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: website-e2e-windows
|
||||
path: test-website-in-workspace/build
|
||||
|
|
|
|||
|
|
@ -273,7 +273,10 @@ async function getSiteName(
|
|||
return 'A website name is required.';
|
||||
}
|
||||
const dest = path.resolve(rootDir, siteName);
|
||||
if (await fs.pathExists(dest)) {
|
||||
if (siteName === '.' && (await fs.readdir(dest)).length > 0) {
|
||||
return logger.interpolate`Directory not empty at path=${dest}!`;
|
||||
}
|
||||
if (siteName !== '.' && (await fs.pathExists(dest))) {
|
||||
return logger.interpolate`Directory already exists at path=${dest}!`;
|
||||
}
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -568,13 +568,28 @@ describe('useSidebarBreadcrumbs', () => {
|
|||
|
||||
it('returns first level link', () => {
|
||||
const pathname = '/somePathName';
|
||||
const sidebar = [testCategory(), testLink({href: pathname})];
|
||||
const sidebar = [testCategory(), testLink({href: pathname, docId: 'doc1'})];
|
||||
|
||||
expect(createUseSidebarBreadcrumbsMock(sidebar)(pathname)).toEqual([
|
||||
sidebar[1],
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns doc links only', () => {
|
||||
const pathname = '/somePathName';
|
||||
|
||||
// A link that is not a doc link should not appear in the breadcrumbs
|
||||
// See https://github.com/facebook/docusaurus/pull/11616
|
||||
const nonDocLink = testLink({href: pathname});
|
||||
const docLink = testLink({href: pathname, docId: 'doc1'});
|
||||
|
||||
const sidebar = [testCategory(), nonDocLink, docLink];
|
||||
|
||||
expect(createUseSidebarBreadcrumbsMock(sidebar)(pathname)).toEqual([
|
||||
docLink,
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns nested category', () => {
|
||||
const pathname = '/somePathName';
|
||||
|
||||
|
|
@ -613,7 +628,7 @@ describe('useSidebarBreadcrumbs', () => {
|
|||
it('returns nested link', () => {
|
||||
const pathname = '/somePathName';
|
||||
|
||||
const link = testLink({href: pathname});
|
||||
const link = testLink({href: pathname, docId: 'docNested'});
|
||||
|
||||
const categoryLevel3 = testCategory({
|
||||
items: [testLink(), link, testLink()],
|
||||
|
|
@ -657,6 +672,35 @@ describe('useSidebarBreadcrumbs', () => {
|
|||
createUseSidebarBreadcrumbsMock(undefined, false)('/foo'),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/facebook/docusaurus/issues/11612
|
||||
it('returns the category that owns the URL, not a category with a link pointing to it', () => {
|
||||
const categoryA: PropSidebarItemCategory = testCategory({
|
||||
label: 'Category A',
|
||||
href: '/category-a',
|
||||
items: [
|
||||
testLink({href: '/category-a/doc1', label: 'Doc 1'}),
|
||||
testLink({href: '/category-a/doc2', label: 'Doc 2'}),
|
||||
// This link points to Category B's generated-index
|
||||
testLink({href: '/category-b', label: 'Go to Category B'}),
|
||||
],
|
||||
});
|
||||
|
||||
const categoryB: PropSidebarItemCategory = testCategory({
|
||||
label: 'Category B',
|
||||
href: '/category-b',
|
||||
items: [
|
||||
testLink({href: '/category-b/item1', label: 'Item 1'}),
|
||||
testLink({href: '/category-b/item2', label: 'Item 2'}),
|
||||
],
|
||||
});
|
||||
|
||||
const sidebar: PropSidebar = [categoryA, categoryB];
|
||||
|
||||
expect(createUseSidebarBreadcrumbsMock(sidebar)('/category-b')).toEqual([
|
||||
categoryB,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCurrentSidebarCategory', () => {
|
||||
|
|
@ -708,12 +752,16 @@ describe('useCurrentSidebarCategory', () => {
|
|||
expect(mockUseCurrentSidebarCategory('/cat2')).toEqual(category2);
|
||||
});
|
||||
|
||||
it('works for category link item', () => {
|
||||
const link = testLink({href: '/my/link/path'});
|
||||
it('works for category doc link item', () => {
|
||||
const pathname = '/my/link/path';
|
||||
const nonDocLink = testLink({href: pathname});
|
||||
const docLink = testLink({href: pathname, docId: 'doc1'});
|
||||
|
||||
const category: PropSidebarItemCategory = testCategory({
|
||||
href: '/cat1',
|
||||
items: [testLink(), testLink(), link, testCategory()],
|
||||
items: [testLink(), testLink(), nonDocLink, docLink, testCategory()],
|
||||
});
|
||||
|
||||
const sidebar: PropSidebar = [
|
||||
testLink(),
|
||||
testLink(),
|
||||
|
|
@ -724,18 +772,28 @@ describe('useCurrentSidebarCategory', () => {
|
|||
const mockUseCurrentSidebarCategory =
|
||||
createUseCurrentSidebarCategoryMock(sidebar);
|
||||
|
||||
expect(mockUseCurrentSidebarCategory('/my/link/path')).toEqual(category);
|
||||
expect(mockUseCurrentSidebarCategory(pathname)).toEqual(category);
|
||||
});
|
||||
|
||||
it('works for nested category link item', () => {
|
||||
const link = testLink({href: '/my/link/path'});
|
||||
const pathname = '/my/link/path';
|
||||
const nonDocLink = testLink({href: pathname});
|
||||
const docLink = testLink({href: pathname, docId: 'doc1'});
|
||||
|
||||
const category2: PropSidebarItemCategory = testCategory({
|
||||
href: '/cat2',
|
||||
items: [testLink(), testLink(), link, testCategory()],
|
||||
items: [
|
||||
testLink(),
|
||||
testLink(),
|
||||
testCategory({items: [nonDocLink]}),
|
||||
nonDocLink,
|
||||
docLink,
|
||||
testCategory(),
|
||||
],
|
||||
});
|
||||
const category1: PropSidebarItemCategory = testCategory({
|
||||
href: '/cat1',
|
||||
items: [testLink(), testLink(), category2, testCategory()],
|
||||
items: [testLink(), nonDocLink, testLink(), category2, testCategory()],
|
||||
});
|
||||
const sidebar: PropSidebar = [
|
||||
testLink(),
|
||||
|
|
@ -780,6 +838,38 @@ describe('useCurrentSidebarCategory', () => {
|
|||
`"Unexpected: cant find current sidebar in context"`,
|
||||
);
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/facebook/docusaurus/issues/11612
|
||||
it('returns the category that owns the URL, not a category with a link pointing to it', () => {
|
||||
const categoryA: PropSidebarItemCategory = testCategory({
|
||||
label: 'Category A',
|
||||
href: '/category-a',
|
||||
items: [
|
||||
testLink({href: '/category-a/doc1', label: 'Doc 1'}),
|
||||
testLink({href: '/category-a/doc2', label: 'Doc 2'}),
|
||||
// This link points to Category B's generated-index
|
||||
testLink({href: '/category-b', label: 'Go to Category B'}),
|
||||
],
|
||||
});
|
||||
|
||||
const categoryB: PropSidebarItemCategory = testCategory({
|
||||
label: 'Category B',
|
||||
href: '/category-b',
|
||||
items: [
|
||||
testLink({href: '/category-b/item1', label: 'Item 1'}),
|
||||
testLink({href: '/category-b/item2', label: 'Item 2'}),
|
||||
],
|
||||
});
|
||||
|
||||
const sidebar: PropSidebar = [categoryA, categoryB];
|
||||
|
||||
const mockUseCurrentSidebarCategory =
|
||||
createUseCurrentSidebarCategoryMock(sidebar);
|
||||
|
||||
// When visiting /category-b, we should get Category B (the owner),
|
||||
// not Category A (which just has a link to it)
|
||||
expect(mockUseCurrentSidebarCategory('/category-b')).toEqual(categoryB);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCurrentSidebarSiblings', () => {
|
||||
|
|
@ -805,10 +895,10 @@ describe('useCurrentSidebarSiblings', () => {
|
|||
testCategory(),
|
||||
];
|
||||
|
||||
const mockUseCurrentSidebarCategory =
|
||||
const mockUseCurrentSidebarSiblings =
|
||||
createUseCurrentSidebarSiblingsMock(sidebar);
|
||||
|
||||
expect(mockUseCurrentSidebarCategory('/cat')).toEqual(category.items);
|
||||
expect(mockUseCurrentSidebarSiblings('/cat')).toEqual(category.items);
|
||||
});
|
||||
|
||||
it('works for sidebar root', () => {
|
||||
|
|
@ -823,10 +913,10 @@ describe('useCurrentSidebarSiblings', () => {
|
|||
testCategory(),
|
||||
];
|
||||
|
||||
const mockUseCurrentSidebarCategory =
|
||||
const mockUseCurrentSidebarSiblings =
|
||||
createUseCurrentSidebarSiblingsMock(sidebar);
|
||||
|
||||
expect(mockUseCurrentSidebarCategory('/rootLink')).toEqual(sidebar);
|
||||
expect(mockUseCurrentSidebarSiblings('/rootLink')).toEqual(sidebar);
|
||||
});
|
||||
|
||||
it('works for nested sidebar category', () => {
|
||||
|
|
@ -852,10 +942,13 @@ describe('useCurrentSidebarSiblings', () => {
|
|||
});
|
||||
|
||||
it('works for category link item', () => {
|
||||
const link = testLink({href: '/my/link/path'});
|
||||
const pathname = '/my/link/path';
|
||||
const nonDocLink = testLink({href: pathname});
|
||||
const docLink = testLink({href: pathname, docId: 'doc1'});
|
||||
|
||||
const category: PropSidebarItemCategory = testCategory({
|
||||
href: '/cat1',
|
||||
items: [testLink(), testLink(), link, testCategory()],
|
||||
items: [testLink(), testLink(), nonDocLink, docLink, testCategory()],
|
||||
});
|
||||
const sidebar: PropSidebar = [
|
||||
testLink(),
|
||||
|
|
@ -864,23 +957,24 @@ describe('useCurrentSidebarSiblings', () => {
|
|||
testCategory(),
|
||||
];
|
||||
|
||||
const mockUseCurrentSidebarCategory =
|
||||
const mockUseCurrentSidebarSiblings =
|
||||
createUseCurrentSidebarSiblingsMock(sidebar);
|
||||
|
||||
expect(mockUseCurrentSidebarCategory('/my/link/path')).toEqual(
|
||||
category.items,
|
||||
);
|
||||
expect(mockUseCurrentSidebarSiblings(pathname)).toEqual(category.items);
|
||||
});
|
||||
|
||||
it('works for nested category link item', () => {
|
||||
const link = testLink({href: '/my/link/path'});
|
||||
const pathname = '/my/link/path';
|
||||
const nonDocLink = testLink({href: pathname});
|
||||
const docLink = testLink({href: pathname, docId: 'doc1'});
|
||||
|
||||
const category2: PropSidebarItemCategory = testCategory({
|
||||
href: '/cat2',
|
||||
items: [testLink(), testLink(), link, testCategory()],
|
||||
items: [testLink(), testLink(), nonDocLink, testCategory()],
|
||||
});
|
||||
const category1: PropSidebarItemCategory = testCategory({
|
||||
href: '/cat1',
|
||||
items: [testLink(), testLink(), category2, testCategory()],
|
||||
items: [testLink(), testLink(), category2, docLink, testCategory()],
|
||||
});
|
||||
const sidebar: PropSidebar = [
|
||||
testLink(),
|
||||
|
|
@ -889,18 +983,16 @@ describe('useCurrentSidebarSiblings', () => {
|
|||
testCategory(),
|
||||
];
|
||||
|
||||
const mockUseCurrentSidebarCategory =
|
||||
const mockUseCurrentSidebarSiblings =
|
||||
createUseCurrentSidebarSiblingsMock(sidebar);
|
||||
|
||||
expect(mockUseCurrentSidebarCategory('/my/link/path')).toEqual(
|
||||
category2.items,
|
||||
);
|
||||
expect(mockUseCurrentSidebarSiblings(pathname)).toEqual(category1.items);
|
||||
});
|
||||
|
||||
it('throws when sidebar is missing', () => {
|
||||
const mockUseCurrentSidebarCategory = createUseCurrentSidebarSiblingsMock();
|
||||
const mockUseCurrentSidebarSiblings = createUseCurrentSidebarSiblingsMock();
|
||||
expect(() =>
|
||||
mockUseCurrentSidebarCategory('/cat'),
|
||||
mockUseCurrentSidebarSiblings('/cat'),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unexpected: cant find current sidebar in context"`,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -234,15 +234,22 @@ function getSidebarBreadcrumbs({
|
|||
}): PropSidebarBreadcrumbsItem[] {
|
||||
const breadcrumbs: PropSidebarBreadcrumbsItem[] = [];
|
||||
|
||||
function extract(items: PropSidebarItem[]) {
|
||||
function extract(items: PropSidebarItem[]): boolean {
|
||||
for (const item of items) {
|
||||
if (
|
||||
(item.type === 'category' &&
|
||||
(isSamePath(item.href, pathname) || extract(item.items))) ||
|
||||
(item.type === 'link' && isSamePath(item.href, pathname))
|
||||
// Extract category item
|
||||
if (item.type === 'category') {
|
||||
if (isSamePath(item.href, pathname) || extract(item.items)) {
|
||||
breadcrumbs.unshift(item);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Extract doc item
|
||||
else if (
|
||||
item.type === 'link' &&
|
||||
item.docId &&
|
||||
isSamePath(item.href, pathname)
|
||||
) {
|
||||
const filtered = onlyCategories && item.type !== 'category';
|
||||
if (!filtered) {
|
||||
if (!onlyCategories) {
|
||||
breadcrumbs.unshift(item);
|
||||
}
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ See the <a href={require('@docusaurus/useBaseUrl').default('showcase')}>showcase
|
|||
- [docusaurus-i18n](https://github.com/moonrailgun/docusaurus-i18n) - Auto-translate docusaurus documents with openai.
|
||||
- [docusaurus-plugin-glossary](https://github.com/mcclowes/docusaurus-plugin-glossary) - A docusaurus plugin for helping users understand key terms.
|
||||
- [docusaurus-plugin-cookie-consent](https://github.com/mcclowes/docusaurus-plugin-cookie-consent) - A Docusaurus plugin for allowing users to opt in/out of cookies, and accessing those settings in code.
|
||||
- [expose-markdown-docusaurus-plugin](https://github.com/FlyNumber/markdown_docusaurus_plugin) - A Docusaurus plugin that exposes your /docs Markdown files as raw .md URLs. (For LLM's and such).
|
||||
|
||||
## Enterprise usage {#enterprise-usage}
|
||||
|
||||
|
|
|
|||
|
|
@ -177,18 +177,103 @@ While you may expect that `BrowserOnly` hides away the children during server-si
|
|||
|
||||
### `useIsBrowser` {#useisbrowser}
|
||||
|
||||
You can also use the `useIsBrowser()` hook to test if the component is currently in a browser environment. It returns `false` in SSR and `true` is CSR, after first client render. Use this hook if you only need to perform certain conditional operations on client-side, but not render an entirely different UI.
|
||||
Returns `true` when the React app has successfully hydrated in the browser.
|
||||
|
||||
:::caution
|
||||
|
||||
Use this hook instead of `typeof windows !== 'undefined'` in React rendering logic because `window` may be defined but hydration may not necessarily have been completed yet.
|
||||
|
||||
The first client-side render output (in the browser) **must be exactly the same** as the server-side render output (Node.js). Not following this rule can lead to unexpected hydration behaviors, as described in [The Perils of Rehydration](https://www.joshwcomeau.com/react/the-perils-of-rehydration/).
|
||||
|
||||
:::
|
||||
|
||||
Usage example:
|
||||
|
||||
```jsx
|
||||
import React from // useEffect // useState,
|
||||
'react';
|
||||
import useIsBrowser from '@docusaurus/useIsBrowser';
|
||||
|
||||
function MyComponent() {
|
||||
const isFetchingLocationMessage = 'fetching location...';
|
||||
|
||||
const MyComponent = () => {
|
||||
// highlight-start
|
||||
// Recommended
|
||||
const isBrowser = useIsBrowser();
|
||||
const location = isBrowser ? window.location.href : 'fetching location...';
|
||||
return <span>{location}</span>;
|
||||
const location = isBrowser ? window.location.href : isFetchingLocationMessage;
|
||||
|
||||
// Not Recommended
|
||||
// using typeof window !== 'undefined' will still work in this example
|
||||
// but not recommended as it may cause issues depending on your business logic
|
||||
const isWindowDefined = typeof window !== 'undefined';
|
||||
const thisWillWorkButNotRecommended = isWindowDefined
|
||||
? window.location.href
|
||||
: isFetchingLocationMessage;
|
||||
|
||||
// highlight-end
|
||||
return (
|
||||
<div>
|
||||
{/* Recommended */}
|
||||
{location}
|
||||
|
||||
{/* Not Recommended */}
|
||||
{thisWillWorkButNotRecommended}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
:::caution If your business logic in the component relies on browser specifics to be functional at all, we recommend using [`<BrowserOnly>`](../docusaurus-core.mdx#browseronly). The following example will cause business logic issues when used with `useIsBrowser`:
|
||||
|
||||
```jsx
|
||||
import React, {useState} from 'react';
|
||||
import useIsBrowser from '@docusaurus/useIsBrowser';
|
||||
|
||||
const isFetchingLocationMessage = 'fetching location...';
|
||||
|
||||
const MyComponent = () => {
|
||||
const isBrowser = useIsBrowser();
|
||||
const location = isBrowser ? window.location.href : isFetchingLocationMessage;
|
||||
// highlight-start
|
||||
const [isFetchingLocation, setIsFetchingLocation] = useState(
|
||||
location === isFetchingLocationMessage,
|
||||
);
|
||||
|
||||
// highlight-end
|
||||
return (
|
||||
<div>
|
||||
{/*
|
||||
This will always print true and will not update.
|
||||
Component already rendered once and useState referenced the initial value as `true`
|
||||
*/}
|
||||
{isFetchingLocation}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
To solve this, you can add a `useEffect` to run when hydration has completed:
|
||||
|
||||
```js
|
||||
useEffect(() => {
|
||||
setIsFetchingLocation(location === isFetchingLocationMessage);
|
||||
}, [isBrowser]);
|
||||
```
|
||||
|
||||
Or use [`<BrowserOnly>`](../docusaurus-core.mdx#browseronly):
|
||||
|
||||
```
|
||||
const ParentComponent = () => {
|
||||
return (
|
||||
<BrowserOnly fallback={<div>Loading...</div>}>
|
||||
{() => <MyComponent />}
|
||||
</BrowserOnly>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### `useEffect` {#useeffect}
|
||||
|
||||
Lastly, you can put your logic in `useEffect()` to delay its execution until after first CSR. This is most appropriate if you are only performing side-effects but don't _get_ data from the client state.
|
||||
|
|
|
|||
|
|
@ -413,7 +413,7 @@ Returns `true` when the React app has successfully hydrated in the browser.
|
|||
|
||||
:::warning
|
||||
|
||||
Use this hook instead of `typeof windows !== 'undefined'` in React rendering logic.
|
||||
Use this hook instead of `typeof windows !== 'undefined'` in React rendering logic because `window` may be defined but hydration may not necessarily have been completed yet.
|
||||
|
||||
The first client-side render output (in the browser) **must be exactly the same** as the server-side render output (Node.js). Not following this rule can lead to unexpected hydration behaviors, as described in [The Perils of Rehydration](https://www.joshwcomeau.com/react/the-perils-of-rehydration/).
|
||||
|
||||
|
|
@ -427,9 +427,24 @@ import useIsBrowser from '@docusaurus/useIsBrowser';
|
|||
|
||||
const MyComponent = () => {
|
||||
// highlight-start
|
||||
// Recommended
|
||||
const isBrowser = useIsBrowser();
|
||||
|
||||
// Not Recommended
|
||||
// using typeof window !== 'undefined' will lead to mismatching render output
|
||||
const isWindowDefined = typeof window !== 'undefined';
|
||||
// highlight-end
|
||||
return <div>{isBrowser ? 'Client' : 'Server'}</div>;
|
||||
return (
|
||||
<div>
|
||||
{/* Recommended */}
|
||||
{isBrowser ? 'Client (hydration completed)' : 'Server'}
|
||||
|
||||
{/* Not Recommended */}
|
||||
{isWindowDefined
|
||||
? 'Client (hydration NOT completed, will mismatch)'
|
||||
: 'Server'}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -110,6 +110,9 @@ html[data-theme='dark'] {
|
|||
--docsearch-container-background: rgb(94 100 112 / 70%);
|
||||
/* Modal */
|
||||
--docsearch-modal-background: var(--ifm-color-secondary-lighter);
|
||||
/* Button */
|
||||
--docsearch-search-button-background: var(--ifm-color-secondary);
|
||||
--docsearch-search-button-text-color: var(--docsearch-muted-color);
|
||||
/* Search box */
|
||||
--docsearch-searchbox-background: var(--ifm-color-secondary);
|
||||
--docsearch-searchbox-focus-background: var(--ifm-color-white);
|
||||
|
|
@ -127,6 +130,9 @@ html[data-theme='dark'] {
|
|||
--docsearch-container-background: rgb(47 55 69 / 70%);
|
||||
/* Modal */
|
||||
--docsearch-modal-background: var(--ifm-background-color);
|
||||
/* Button */
|
||||
--docsearch-search-button-background: var(--ifm-background-color);
|
||||
--docsearch-search-button-text-color: var(--docsearch-muted-color);
|
||||
/* Search box */
|
||||
--docsearch-searchbox-background: var(--ifm-background-color);
|
||||
--docsearch-searchbox-focus-background: var(--ifm-color-black);
|
||||
|
|
|
|||
26
yarn.lock
26
yarn.lock
|
|
@ -2069,25 +2069,25 @@
|
|||
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
|
||||
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
|
||||
|
||||
"@docsearch/core@4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@docsearch/core/-/core-4.3.1.tgz#88a97a6fe4d4025269b6dee8b9d070b76758ad82"
|
||||
integrity sha512-ktVbkePE+2h9RwqCUMbWXOoebFyDOxHqImAqfs+lC8yOU+XwEW4jgvHGJK079deTeHtdhUNj0PXHSnhJINvHzQ==
|
||||
"@docsearch/core@4.4.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@docsearch/core/-/core-4.4.0.tgz#206c0df38ee08cf0d6e33c4eaee140706931b311"
|
||||
integrity sha512-kiwNo5KEndOnrf5Kq/e5+D9NBMCFgNsDoRpKQJ9o/xnSlheh6b8AXppMuuUVVdAUIhIfQFk/07VLjjk/fYyKmw==
|
||||
|
||||
"@docsearch/css@4.3.2":
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-4.3.2.tgz#d47d25336c9516b419245fa74e8dd5ae84a17492"
|
||||
integrity sha512-K3Yhay9MgkBjJJ0WEL5MxnACModX9xuNt3UlQQkDEDZJZ0+aeWKtOkxHNndMRkMBnHdYvQjxkm6mdlneOtU1IQ==
|
||||
"@docsearch/css@4.4.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-4.4.0.tgz#b8eebd21a1f79720bf037fda5242b910367f157e"
|
||||
integrity sha512-e9vPgtih6fkawakmYo0Y6V4BKBmDV7Ykudn7ADWXUs5b6pmtBRwDbpSG/WiaUG63G28OkJDEnsMvgIAnZgGwYw==
|
||||
|
||||
"@docsearch/react@^3.9.0 || ^4.3.2":
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-4.3.2.tgz#450b8341cb5cca03737a00075d4dfd3a904a3e3e"
|
||||
integrity sha512-74SFD6WluwvgsOPqifYOviEEVwDxslxfhakTlra+JviaNcs7KK/rjsPj89kVEoQc9FUxRkAofaJnHIR7pb4TSQ==
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-4.4.0.tgz#f69bd533305a07247f4850ee74af11e784b99658"
|
||||
integrity sha512-z12zeg1mV7WD4Ag4pKSuGukETJLaucVFwszDXL/qLaEgRqxEaVacO9SR1qqnCXvZztlvz2rt7cMqryi/7sKfjA==
|
||||
dependencies:
|
||||
"@ai-sdk/react" "^2.0.30"
|
||||
"@algolia/autocomplete-core" "1.19.2"
|
||||
"@docsearch/core" "4.3.1"
|
||||
"@docsearch/css" "4.3.2"
|
||||
"@docsearch/core" "4.4.0"
|
||||
"@docsearch/css" "4.4.0"
|
||||
ai "^5.0.30"
|
||||
algoliasearch "^5.28.0"
|
||||
marked "^16.3.0"
|
||||
|
|
|
|||
Loading…
Reference in New Issue