mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-23 14:06:59 +02:00
feat(theme-classic): new configuration syntax for a simple footer (#6132)
Co-authored-by: Joshua Chen <sidachen2003@gmail.com> Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
This commit is contained in:
parent
cb4265253a
commit
d987c22996
9 changed files with 364 additions and 91 deletions
|
@ -55,6 +55,49 @@ Array [
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`getTranslationFiles should return translation files matching snapshot 2`] = `
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"content": Object {
|
||||||
|
"item.label.Dropdown": Object {
|
||||||
|
"description": "Navbar item with label Dropdown",
|
||||||
|
"message": "Dropdown",
|
||||||
|
},
|
||||||
|
"item.label.Dropdown item 1": Object {
|
||||||
|
"description": "Navbar item with label Dropdown item 1",
|
||||||
|
"message": "Dropdown item 1",
|
||||||
|
},
|
||||||
|
"item.label.Dropdown item 2": Object {
|
||||||
|
"description": "Navbar item with label Dropdown item 2",
|
||||||
|
"message": "Dropdown item 2",
|
||||||
|
},
|
||||||
|
"title": Object {
|
||||||
|
"description": "The title in the navbar",
|
||||||
|
"message": "navbar title",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"path": "navbar",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"content": Object {
|
||||||
|
"copyright": Object {
|
||||||
|
"description": "The footer copyright",
|
||||||
|
"message": "Copyright FB",
|
||||||
|
},
|
||||||
|
"link.item.label.Link 1": Object {
|
||||||
|
"description": "The label of footer link with label=Link 1 linking to https://facebook.com",
|
||||||
|
"message": "Link 1",
|
||||||
|
},
|
||||||
|
"link.item.label.Link 2": Object {
|
||||||
|
"description": "The label of footer link with label=Link 2 linking to https://facebook.com",
|
||||||
|
"message": "Link 2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"path": "footer",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`translateThemeConfig should return translated themeConfig matching snapshot 1`] = `
|
exports[`translateThemeConfig should return translated themeConfig matching snapshot 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"announcementBar": Object {},
|
"announcementBar": Object {},
|
||||||
|
|
|
@ -50,14 +50,26 @@ const ThemeConfigSample: ThemeConfig = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function getSampleTranslationFiles() {
|
const ThemeConfigSampleSimpleFooter: ThemeConfig = {
|
||||||
|
...ThemeConfigSample,
|
||||||
|
footer: {
|
||||||
|
copyright: 'Copyright FB',
|
||||||
|
style: 'light',
|
||||||
|
links: [
|
||||||
|
{label: 'Link 1', to: 'https://facebook.com'},
|
||||||
|
{label: 'Link 2', to: 'https://facebook.com'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getSampleTranslationFiles(themeConfig: ThemeConfig) {
|
||||||
return getTranslationFiles({
|
return getTranslationFiles({
|
||||||
themeConfig: ThemeConfigSample,
|
themeConfig,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSampleTranslationFilesTranslated() {
|
function getSampleTranslationFilesTranslated(themeConfig: ThemeConfig) {
|
||||||
const translationFiles = getSampleTranslationFiles();
|
const translationFiles = getSampleTranslationFiles(themeConfig);
|
||||||
return translationFiles.map((translationFile) =>
|
return translationFiles.map((translationFile) =>
|
||||||
updateTranslationFileMessages(
|
updateTranslationFileMessages(
|
||||||
translationFile,
|
translationFile,
|
||||||
|
@ -68,27 +80,29 @@ function getSampleTranslationFilesTranslated() {
|
||||||
|
|
||||||
describe('getTranslationFiles', () => {
|
describe('getTranslationFiles', () => {
|
||||||
test('should return translation files matching snapshot', () => {
|
test('should return translation files matching snapshot', () => {
|
||||||
expect(getSampleTranslationFiles()).toMatchSnapshot();
|
expect(getSampleTranslationFiles(ThemeConfigSample)).toMatchSnapshot();
|
||||||
|
expect(
|
||||||
|
getSampleTranslationFiles(ThemeConfigSampleSimpleFooter),
|
||||||
|
).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('translateThemeConfig', () => {
|
describe('translateThemeConfig', () => {
|
||||||
test('should not translate anything if translation files are untranslated', () => {
|
test('should not translate anything if translation files are untranslated', () => {
|
||||||
const translationFiles = getSampleTranslationFiles();
|
|
||||||
expect(
|
expect(
|
||||||
translateThemeConfig({
|
translateThemeConfig({
|
||||||
themeConfig: ThemeConfigSample,
|
themeConfig: ThemeConfigSample,
|
||||||
translationFiles,
|
translationFiles: getSampleTranslationFiles(ThemeConfigSample),
|
||||||
}),
|
}),
|
||||||
).toEqual(ThemeConfigSample);
|
).toEqual(ThemeConfigSample);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return translated themeConfig matching snapshot', () => {
|
test('should return translated themeConfig matching snapshot', () => {
|
||||||
const translationFiles = getSampleTranslationFilesTranslated();
|
|
||||||
expect(
|
expect(
|
||||||
translateThemeConfig({
|
translateThemeConfig({
|
||||||
themeConfig: ThemeConfigSample,
|
themeConfig: ThemeConfigSample,
|
||||||
translationFiles,
|
translationFiles:
|
||||||
|
getSampleTranslationFilesTranslated(ThemeConfigSample),
|
||||||
}),
|
}),
|
||||||
).toMatchSnapshot();
|
).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
@ -96,18 +110,21 @@ describe('translateThemeConfig', () => {
|
||||||
|
|
||||||
describe('getTranslationFiles and translateThemeConfig isomorphism', () => {
|
describe('getTranslationFiles and translateThemeConfig isomorphism', () => {
|
||||||
function verifyIsomorphism(themeConfig: ThemeConfig) {
|
function verifyIsomorphism(themeConfig: ThemeConfig) {
|
||||||
const translationFiles = getTranslationFiles({themeConfig});
|
|
||||||
const translatedThemeConfig = translateThemeConfig({
|
const translatedThemeConfig = translateThemeConfig({
|
||||||
themeConfig,
|
themeConfig,
|
||||||
translationFiles,
|
translationFiles: getTranslationFiles({themeConfig}),
|
||||||
});
|
});
|
||||||
expect(translatedThemeConfig).toEqual(themeConfig);
|
expect(translatedThemeConfig).toEqual(themeConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
test('should be verified for main sample', () => {
|
test('should be verified for sample', () => {
|
||||||
verifyIsomorphism(ThemeConfigSample);
|
verifyIsomorphism(ThemeConfigSample);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should be verified for sample with simple footer', () => {
|
||||||
|
verifyIsomorphism(ThemeConfigSampleSimpleFooter);
|
||||||
|
});
|
||||||
|
|
||||||
// undefined footer should not make the translation code crash
|
// undefined footer should not make the translation code crash
|
||||||
// See https://github.com/facebook/docusaurus/issues/3936
|
// See https://github.com/facebook/docusaurus/issues/3936
|
||||||
test('should be verified for sample without footer', () => {
|
test('should be verified for sample without footer', () => {
|
||||||
|
|
|
@ -345,6 +345,104 @@ describe('themeConfig', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should allow simple links in footer', () => {
|
||||||
|
const partialConfig = {
|
||||||
|
footer: {
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: 'Privacy',
|
||||||
|
href: 'https://opensource.facebook.com/legal/privacy/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Terms',
|
||||||
|
href: 'https://opensource.facebook.com/legal/terms/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Data Policy',
|
||||||
|
href: 'https://opensource.facebook.com/legal/data-policy/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cookie Policy',
|
||||||
|
href: 'https://opensource.facebook.com/legal/cookie-policy/',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const normalizedConfig = testValidateThemeConfig(partialConfig);
|
||||||
|
|
||||||
|
expect(normalizedConfig).toEqual({
|
||||||
|
...normalizedConfig,
|
||||||
|
footer: {
|
||||||
|
...normalizedConfig.footer,
|
||||||
|
...partialConfig.footer,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow footer column with no title', () => {
|
||||||
|
const partialConfig = {
|
||||||
|
footer: {
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Data Policy',
|
||||||
|
href: 'https://opensource.facebook.com/legal/data-policy/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cookie Policy',
|
||||||
|
href: 'https://opensource.facebook.com/legal/cookie-policy/',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const normalizedConfig = testValidateThemeConfig(partialConfig);
|
||||||
|
|
||||||
|
expect(normalizedConfig).toEqual({
|
||||||
|
...normalizedConfig,
|
||||||
|
footer: {
|
||||||
|
...normalizedConfig.footer,
|
||||||
|
...partialConfig.footer,
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
title: null, // Default value is important to distinguish simple footer from multi-column footer
|
||||||
|
items: partialConfig.footer.links[0].items,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should reject mix of simple and multi-column links in footer', () => {
|
||||||
|
const partialConfig = {
|
||||||
|
footer: {
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
title: 'Learn',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Introduction',
|
||||||
|
to: 'docs',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Privacy',
|
||||||
|
href: 'https://opensource.facebook.com/legal/privacy/',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
testValidateThemeConfig(partialConfig),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"The footer must be either simple or multi-column, and not a mix of the two. See: https://docusaurus.io/docs/api/themes/configuration#footer-links"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('should allow width and height specification for logo ', () => {
|
test('should allow width and height specification for logo ', () => {
|
||||||
const altTagConfig = {
|
const altTagConfig = {
|
||||||
navbar: {
|
navbar: {
|
||||||
|
|
|
@ -9,7 +9,12 @@ import React from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import Link from '@docusaurus/Link';
|
import Link from '@docusaurus/Link';
|
||||||
import {FooterLinkItem, useThemeConfig} from '@docusaurus/theme-common';
|
import {
|
||||||
|
FooterLinkItem,
|
||||||
|
useThemeConfig,
|
||||||
|
MultiColumnFooter,
|
||||||
|
SimpleFooter,
|
||||||
|
} from '@docusaurus/theme-common';
|
||||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||||
import isInternalUrl from '@docusaurus/isInternalUrl';
|
import isInternalUrl from '@docusaurus/isInternalUrl';
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
|
@ -66,35 +71,12 @@ function FooterLogo({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Footer(): JSX.Element | null {
|
function MultiColumnLinks({links}: {links: MultiColumnFooter['links']}) {
|
||||||
const {footer} = useThemeConfig();
|
|
||||||
|
|
||||||
const {copyright, links = [], logo = {}} = footer || {};
|
|
||||||
const sources = {
|
|
||||||
light: useBaseUrl(logo.src),
|
|
||||||
dark: useBaseUrl(logo.srcDark || logo.src),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!footer) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer
|
<>
|
||||||
className={clsx('footer', {
|
|
||||||
'footer--dark': footer.style === 'dark',
|
|
||||||
})}>
|
|
||||||
<div className="container">
|
|
||||||
{links && links.length > 0 && (
|
|
||||||
<div className="row footer__links">
|
|
||||||
{links.map((linkItem, i) => (
|
{links.map((linkItem, i) => (
|
||||||
<div key={i} className="col footer__col">
|
<div key={i} className="col footer__col">
|
||||||
{linkItem.title != null ? (
|
|
||||||
<div className="footer__title">{linkItem.title}</div>
|
<div className="footer__title">{linkItem.title}</div>
|
||||||
) : null}
|
|
||||||
{linkItem.items != null &&
|
|
||||||
Array.isArray(linkItem.items) &&
|
|
||||||
linkItem.items.length > 0 ? (
|
|
||||||
<ul className="footer__items">
|
<ul className="footer__items">
|
||||||
{linkItem.items.map((item, key) =>
|
{linkItem.items.map((item, key) =>
|
||||||
item.html ? (
|
item.html ? (
|
||||||
|
@ -114,11 +96,75 @@ function Footer(): JSX.Element | null {
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SimpleLinks({links}: {links: SimpleFooter['links']}) {
|
||||||
|
return (
|
||||||
|
<div className="footer__links">
|
||||||
|
{links.map((item, key) => (
|
||||||
|
<>
|
||||||
|
{item.html ? (
|
||||||
|
<span
|
||||||
|
key={key}
|
||||||
|
className="footer__link-item"
|
||||||
|
// Developer provided the HTML, so assume it's safe.
|
||||||
|
// eslint-disable-next-line react/no-danger
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: item.html,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FooterLink {...item} />
|
||||||
)}
|
)}
|
||||||
|
{links.length !== key + 1 && (
|
||||||
|
<span className="footer__link-separator">·</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMultiColumnFooterLinks(
|
||||||
|
links: MultiColumnFooter['links'] | SimpleFooter['links'],
|
||||||
|
): links is MultiColumnFooter['links'] {
|
||||||
|
return 'title' in links[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function Footer(): JSX.Element | null {
|
||||||
|
const {footer} = useThemeConfig();
|
||||||
|
|
||||||
|
const {copyright, links = [], logo = {}} = footer || {};
|
||||||
|
const sources = {
|
||||||
|
light: useBaseUrl(logo.src),
|
||||||
|
dark: useBaseUrl(logo.srcDark || logo.src),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!footer) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer
|
||||||
|
className={clsx('footer', {
|
||||||
|
'footer--dark': footer.style === 'dark',
|
||||||
|
})}>
|
||||||
|
<div className="container container-fluid">
|
||||||
|
{links &&
|
||||||
|
links.length > 0 &&
|
||||||
|
(isMultiColumnFooterLinks(links) ? (
|
||||||
|
<div className="row footer__links">
|
||||||
|
<MultiColumnLinks links={links} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="footer__links text--center">
|
||||||
|
<SimpleLinks links={links} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
{(logo || copyright) && (
|
{(logo || copyright) && (
|
||||||
<div className="footer__bottom text--center">
|
<div className="footer__bottom text--center">
|
||||||
{logo && (logo.src || logo.srcDark) && (
|
{logo && (logo.src || logo.srcDark) && (
|
||||||
|
@ -154,4 +200,4 @@ function Footer(): JSX.Element | null {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Footer;
|
export default React.memo(Footer);
|
||||||
|
|
|
@ -11,6 +11,8 @@ import {
|
||||||
Navbar,
|
Navbar,
|
||||||
NavbarItem,
|
NavbarItem,
|
||||||
Footer,
|
Footer,
|
||||||
|
MultiColumnFooter,
|
||||||
|
SimpleFooter,
|
||||||
} from '@docusaurus/theme-common';
|
} from '@docusaurus/theme-common';
|
||||||
|
|
||||||
import {keyBy, chain} from 'lodash';
|
import {keyBy, chain} from 'lodash';
|
||||||
|
@ -69,20 +71,31 @@ function translateNavbar(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isMultiColumnFooterLinks(
|
||||||
|
links: MultiColumnFooter['links'] | SimpleFooter['links'],
|
||||||
|
): links is MultiColumnFooter['links'] {
|
||||||
|
return 'title' in links[0];
|
||||||
|
}
|
||||||
|
|
||||||
function getFooterTranslationFile(footer: Footer): TranslationFileContent {
|
function getFooterTranslationFile(footer: Footer): TranslationFileContent {
|
||||||
// TODO POC code
|
const footerLinkTitles: TranslationFileContent = isMultiColumnFooterLinks(
|
||||||
const footerLinkTitles: TranslationFileContent = chain(
|
footer.links,
|
||||||
footer.links.filter((link) => !!link.title),
|
|
||||||
)
|
)
|
||||||
|
? chain(footer.links.filter((link) => !!link.title))
|
||||||
.keyBy((link) => `link.title.${link.title}`)
|
.keyBy((link) => `link.title.${link.title}`)
|
||||||
.mapValues((link) => ({
|
.mapValues((link) => ({
|
||||||
message: link.title!,
|
message: link.title!,
|
||||||
description: `The title of the footer links column with title=${link.title} in the footer`,
|
description: `The title of the footer links column with title=${link.title} in the footer`,
|
||||||
}))
|
}))
|
||||||
.value();
|
.value()
|
||||||
|
: {};
|
||||||
|
|
||||||
const footerLinkLabels: TranslationFileContent = chain(
|
const footerLinkLabels: TranslationFileContent = chain(
|
||||||
footer.links.flatMap((link) => link.items).filter((link) => !!link.label),
|
isMultiColumnFooterLinks(footer.links)
|
||||||
|
? footer.links
|
||||||
|
.flatMap((link) => link.items)
|
||||||
|
.filter((link) => !!link.label)
|
||||||
|
: footer.links.filter((link) => !!link.label),
|
||||||
)
|
)
|
||||||
.keyBy((linkItem) => `link.item.label.${linkItem.label}`)
|
.keyBy((linkItem) => `link.item.label.${linkItem.label}`)
|
||||||
.mapValues((linkItem) => ({
|
.mapValues((linkItem) => ({
|
||||||
|
@ -108,7 +121,8 @@ function translateFooter(
|
||||||
footer: Footer,
|
footer: Footer,
|
||||||
footerTranslations: TranslationFileContent,
|
footerTranslations: TranslationFileContent,
|
||||||
): Footer {
|
): Footer {
|
||||||
const links = footer.links.map((link) => ({
|
const links = isMultiColumnFooterLinks(footer.links)
|
||||||
|
? footer.links.map((link) => ({
|
||||||
...link,
|
...link,
|
||||||
title:
|
title:
|
||||||
footerTranslations[`link.title.${link.title}`]?.message ?? link.title,
|
footerTranslations[`link.title.${link.title}`]?.message ?? link.title,
|
||||||
|
@ -118,6 +132,12 @@ function translateFooter(
|
||||||
footerTranslations[`link.item.label.${linkItem.label}`]?.message ??
|
footerTranslations[`link.item.label.${linkItem.label}`]?.message ??
|
||||||
linkItem.label,
|
linkItem.label,
|
||||||
})),
|
})),
|
||||||
|
}))
|
||||||
|
: footer.links.map((link) => ({
|
||||||
|
...link,
|
||||||
|
label:
|
||||||
|
footerTranslations[`link.item.label.${link.label}`]?.message ??
|
||||||
|
link.label,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const copyright = footerTranslations.copyright?.message ?? footer.copyright;
|
const copyright = footerTranslations.copyright?.message ?? footer.copyright;
|
||||||
|
|
|
@ -311,14 +311,19 @@ const ThemeConfigSchema = Joi.object({
|
||||||
href: Joi.string(),
|
href: Joi.string(),
|
||||||
}),
|
}),
|
||||||
copyright: Joi.string(),
|
copyright: Joi.string(),
|
||||||
links: Joi.array()
|
links: Joi.alternatives(
|
||||||
|
Joi.array()
|
||||||
.items(
|
.items(
|
||||||
Joi.object({
|
Joi.object({
|
||||||
title: Joi.string().allow(null),
|
title: Joi.string().allow(null).default(null),
|
||||||
items: Joi.array().items(FooterLinkItemSchema).default([]),
|
items: Joi.array().items(FooterLinkItemSchema).default([]),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.default([]),
|
.default([]),
|
||||||
|
Joi.array().items(FooterLinkItemSchema).default([]),
|
||||||
|
).messages({
|
||||||
|
'alternatives.match': `The footer must be either simple or multi-column, and not a mix of the two. See: https://docusaurus.io/docs/api/themes/configuration#footer-links`,
|
||||||
|
}),
|
||||||
}).optional(),
|
}).optional(),
|
||||||
prism: Joi.object({
|
prism: Joi.object({
|
||||||
theme: Joi.object({
|
theme: Joi.object({
|
||||||
|
|
|
@ -13,8 +13,9 @@ export type {
|
||||||
Navbar,
|
Navbar,
|
||||||
NavbarItem,
|
NavbarItem,
|
||||||
NavbarLogo,
|
NavbarLogo,
|
||||||
|
MultiColumnFooter,
|
||||||
|
SimpleFooter,
|
||||||
Footer,
|
Footer,
|
||||||
FooterLinks,
|
|
||||||
FooterLinkItem,
|
FooterLinkItem,
|
||||||
ColorModeConfig,
|
ColorModeConfig,
|
||||||
} from './utils/useThemeConfig';
|
} from './utils/useThemeConfig';
|
||||||
|
|
|
@ -73,11 +73,8 @@ export type FooterLinkItem = {
|
||||||
html?: string;
|
html?: string;
|
||||||
prependBaseUrlToHref?: string;
|
prependBaseUrlToHref?: string;
|
||||||
};
|
};
|
||||||
export type FooterLinks = {
|
|
||||||
title?: string;
|
export type FooterBase = {
|
||||||
items: FooterLinkItem[];
|
|
||||||
};
|
|
||||||
export type Footer = {
|
|
||||||
style: 'light' | 'dark';
|
style: 'light' | 'dark';
|
||||||
logo?: {
|
logo?: {
|
||||||
alt?: string;
|
alt?: string;
|
||||||
|
@ -88,9 +85,21 @@ export type Footer = {
|
||||||
href?: string;
|
href?: string;
|
||||||
};
|
};
|
||||||
copyright?: string;
|
copyright?: string;
|
||||||
links: FooterLinks[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MultiColumnFooter = FooterBase & {
|
||||||
|
links: Array<{
|
||||||
|
title: string | null;
|
||||||
|
items: FooterLinkItem[];
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SimpleFooter = FooterBase & {
|
||||||
|
links: FooterLinkItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Footer = MultiColumnFooter | SimpleFooter;
|
||||||
|
|
||||||
export type TableOfContents = {
|
export type TableOfContents = {
|
||||||
minHeadingLevel: number;
|
minHeadingLevel: number;
|
||||||
maxHeadingLevel: number;
|
maxHeadingLevel: number;
|
||||||
|
|
|
@ -672,7 +672,7 @@ Accepted fields:
|
||||||
| `logo` | `Logo` | `undefined` | Customization of the logo object. See [Navbar logo](#navbar-logo) for details. |
|
| `logo` | `Logo` | `undefined` | Customization of the logo object. See [Navbar logo](#navbar-logo) for details. |
|
||||||
| `copyright` | `string` | `undefined` | The copyright message to be displayed at the bottom. |
|
| `copyright` | `string` | `undefined` | The copyright message to be displayed at the bottom. |
|
||||||
| `style` | <code>'dark' \| 'light'</code> | `'light'` | The color theme of the footer component. |
|
| `style` | <code>'dark' \| 'light'</code> | `'light'` | The color theme of the footer component. |
|
||||||
| `items` | `FooterItem[]` | `[]` | The link groups to be present. |
|
| `links` | <code>(Column \| FooterLink)[]</code> | `[]` | The link groups to be present. |
|
||||||
|
|
||||||
</APITable>
|
</APITable>
|
||||||
|
|
||||||
|
@ -699,20 +699,20 @@ module.exports = {
|
||||||
|
|
||||||
### Footer Links {#footer-links}
|
### Footer Links {#footer-links}
|
||||||
|
|
||||||
You can add links to the footer via `themeConfig.footer.links`.
|
You can add links to the footer via `themeConfig.footer.links`. There are two types of footer configurations: **multi-column footers** and **simple footers**.
|
||||||
|
|
||||||
Accepted fields of each link section:
|
Multi-column footer links have a `title` and a list of `FooterItem`s for each column.
|
||||||
|
|
||||||
<APITable name="footer-links">
|
<APITable name="footer-links">
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `title` | `string` | `undefined` | Label of the section of these links. |
|
| `title` | `string` | `undefined` | Label of the section of these links. |
|
||||||
| `items` | `FooterLink[]` | `[]` | Links in this section. |
|
| `items` | `FooterItem[]` | `[]` | Links in this section. |
|
||||||
|
|
||||||
</APITable>
|
</APITable>
|
||||||
|
|
||||||
Accepted fields of each item in `items`:
|
Accepted fields of each `FooterItem`:
|
||||||
|
|
||||||
<APITable name="footer-items">
|
<APITable name="footer-items">
|
||||||
|
|
||||||
|
@ -725,7 +725,7 @@ Accepted fields of each item in `items`:
|
||||||
|
|
||||||
</APITable>
|
</APITable>
|
||||||
|
|
||||||
Example configuration:
|
Example multi-column configuration:
|
||||||
|
|
||||||
```js title="docusaurus.config.js"
|
```js title="docusaurus.config.js"
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -775,6 +775,40 @@ module.exports = {
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
A simple footer just has a list of `FooterItem`s displayed in a row.
|
||||||
|
|
||||||
|
Example simple configuration:
|
||||||
|
|
||||||
|
```js title="docusaurus.config.js"
|
||||||
|
module.exports = {
|
||||||
|
footer: {
|
||||||
|
// highlight-start
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: 'Stack Overflow',
|
||||||
|
href: 'https://stackoverflow.com/questions/tagged/docusaurus',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Discord',
|
||||||
|
href: 'https://discordapp.com/invite/docusaurus',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Twitter',
|
||||||
|
href: 'https://twitter.com/docusaurus',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
html: `
|
||||||
|
<a href="https://www.netlify.com" target="_blank" rel="noreferrer noopener" aria-label="Deploys by Netlify">
|
||||||
|
<img src="https://www.netlify.com/img/global/badges/netlify-color-accent.svg" alt="Deploys by Netlify" />
|
||||||
|
</a>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// highlight-end
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
## Table of Contents {#table-of-contents}
|
## Table of Contents {#table-of-contents}
|
||||||
|
|
||||||
You can adjust the default table of contents via `themeConfig.tableOfContents`.
|
You can adjust the default table of contents via `themeConfig.tableOfContents`.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue