From 869ebe7b53af414894fbabf77607d35c064d0b2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Fri, 22 Jan 2021 21:26:42 +0100 Subject: [PATCH] fix(v2): fix/enhance minor i18n issues reported (#4092) * fix comment * allow to pass custom classname in navbar items * Add IconLanguage comp to dropdown * do not trim htmlLang * Add initial hreflang SEO support * doc hreflang --- .../src/theme/IconLanguage/index.tsx | 31 ++++++++++++ .../src/theme/LayoutHead/index.tsx | 38 +++++++++++++- .../theme/NavbarItem/DefaultNavbarItem.tsx | 4 +- .../src/theme/NavbarItem/DocNavbarItem.tsx | 2 +- .../DocsVersionDropdownNavbarItem.tsx | 2 +- .../NavbarItem/DocsVersionNavbarItem.tsx | 2 +- .../NavbarItem/LocaleDropdownNavbarItem.tsx | 40 +++++++-------- .../docusaurus-theme-classic/src/types.d.ts | 13 ++++- .../src/validateThemeConfig.js | 4 ++ packages/docusaurus-theme-common/src/index.ts | 2 + .../src/utils/useAlternatePageUtils.ts | 50 +++++++++++++++++++ website/docs/i18n/i18n-introduction.md | 2 +- 12 files changed, 158 insertions(+), 32 deletions(-) create mode 100644 packages/docusaurus-theme-classic/src/theme/IconLanguage/index.tsx create mode 100644 packages/docusaurus-theme-common/src/utils/useAlternatePageUtils.ts diff --git a/packages/docusaurus-theme-classic/src/theme/IconLanguage/index.tsx b/packages/docusaurus-theme-classic/src/theme/IconLanguage/index.tsx new file mode 100644 index 0000000000..52ba2ce999 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/IconLanguage/index.tsx @@ -0,0 +1,31 @@ +/** + * 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 {Props} from '@theme/IconLanguage'; + +const IconLanguage = ({ + width = 20, + height = 20, + ...props +}: Props): JSX.Element => { + return ( + + + + ); +}; + +export default IconLanguage; diff --git a/packages/docusaurus-theme-classic/src/theme/LayoutHead/index.tsx b/packages/docusaurus-theme-classic/src/theme/LayoutHead/index.tsx index 0fe78ae88e..c0cfc29cf6 100644 --- a/packages/docusaurus-theme-classic/src/theme/LayoutHead/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/LayoutHead/index.tsx @@ -11,7 +11,36 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import useBaseUrl from '@docusaurus/useBaseUrl'; import type {Props} from '@theme/Layout'; import SearchMetadatas from '@theme/SearchMetadatas'; -import {DEFAULT_SEARCH_TAG, useTitleFormatter} from '@docusaurus/theme-common'; +import { + DEFAULT_SEARCH_TAG, + useTitleFormatter, + useAlternatePageUtils, +} from '@docusaurus/theme-common'; + +// Useful for SEO +// See https://developers.google.com/search/docs/advanced/crawling/localized-versions +// See https://github.com/facebook/docusaurus/issues/3317 +function AlternateLangHeaders(): JSX.Element { + const { + i18n: {defaultLocale, locales}, + } = useDocusaurusContext(); + const alternatePageUtils = useAlternatePageUtils(); + return ( + + {locales.map((locale) => ( + + ))} + + ); +} export default function LayoutHead(props: Props): JSX.Element { const { @@ -36,7 +65,10 @@ export default function LayoutHead(props: Props): JSX.Element { const metaImageUrl = useBaseUrl(metaImage, {absolute: true}); const faviconUrl = useBaseUrl(favicon); - const htmlLang = currentLocale.split('-')[0]; + // See https://github.com/facebook/docusaurus/issues/3317#issuecomment-754661855 + // const htmlLang = currentLocale.split('-')[0]; + const htmlLang = currentLocale; // should we allow the user to override htmlLang with localeConfig? + return ( <> @@ -61,6 +93,8 @@ export default function LayoutHead(props: Props): JSX.Element { + + - {props.label} + {props.children ?? props.label}
    {items.map(({className: childItemClassName, ...childItemProps}, i) => ( @@ -195,7 +195,7 @@ function NavItemMobile({ onClick={() => { setCollapsed((state) => !state); }}> - {props.label} + {props.children ?? props.label}
      { - const to = `${getLocalizedBaseUrl(locale)}${pathnameSuffix}`; + const to = `pathname://${alternatePageUtils.createUrl({ + locale, + fullyQualified: false, + })}`; return { isNavLink: true, label: getLocaleLabel(locale), - to: `pathname://${to}`, + to, target: '_self', autoAddBaseUrl: false, className: locale === currentLocale ? 'dropdown__link--active' : '', @@ -62,7 +51,14 @@ export default function LocaleDropdownNavbarItem({ + + {dropdownLabel} + + } items={items} /> ); diff --git a/packages/docusaurus-theme-classic/src/types.d.ts b/packages/docusaurus-theme-classic/src/types.d.ts index 78a045e384..d3e889ade8 100644 --- a/packages/docusaurus-theme-classic/src/types.d.ts +++ b/packages/docusaurus-theme-classic/src/types.d.ts @@ -276,7 +276,7 @@ declare module '@theme/Navbar' { } declare module '@theme/NavbarItem/DefaultNavbarItem' { - import type {ComponentProps} from 'react'; + import type {ComponentProps, ReactNode} from 'react'; export type NavLinkProps = { activeBasePath?: string; @@ -284,7 +284,7 @@ declare module '@theme/NavbarItem/DefaultNavbarItem' { to?: string; exact?: boolean; href?: string; - label?: string; + label?: ReactNode; activeClassName?: string; prependBaseUrlToHref?: string; isActive?: () => boolean; @@ -529,3 +529,12 @@ declare module '@theme/IconMenu' { const IconMenu: (props: Props) => JSX.Element; export default IconMenu; } + +declare module '@theme/IconLanguage' { + import type {ComponentProps} from 'react'; + + export type Props = ComponentProps<'svg'>; + + const IconLanguage: (props: Props) => JSX.Element; + export default IconLanguage; +} diff --git a/packages/docusaurus-theme-classic/src/validateThemeConfig.js b/packages/docusaurus-theme-classic/src/validateThemeConfig.js index c88436c082..0c6a4b2907 100644 --- a/packages/docusaurus-theme-classic/src/validateThemeConfig.js +++ b/packages/docusaurus-theme-classic/src/validateThemeConfig.js @@ -76,6 +76,7 @@ const DocsVersionNavbarItemSchema = Joi.object({ label: Joi.string(), to: Joi.string(), docsPluginId: Joi.string(), + className: Joi.string(), }); const DocsVersionDropdownNavbarItemSchema = Joi.object({ @@ -85,6 +86,7 @@ const DocsVersionDropdownNavbarItemSchema = Joi.object({ dropdownActiveClassDisabled: Joi.boolean(), dropdownItemsBefore: Joi.array().items(BaseNavbarItemSchema).default([]), dropdownItemsAfter: Joi.array().items(BaseNavbarItemSchema).default([]), + className: Joi.string(), }); const DocItemSchema = Joi.object({ @@ -94,6 +96,7 @@ const DocItemSchema = Joi.object({ label: Joi.string(), docsPluginId: Joi.string(), activeSidebarClassName: Joi.string().default('navbar__link--active'), + className: Joi.string(), }); const LocaleDropdownNavbarItemSchema = Joi.object({ @@ -101,6 +104,7 @@ const LocaleDropdownNavbarItemSchema = Joi.object({ position: NavbarItemPosition, dropdownItemsBefore: Joi.array().items(BaseNavbarItemSchema).default([]), dropdownItemsAfter: Joi.array().items(BaseNavbarItemSchema).default([]), + className: Joi.string(), }); // Can this be made easier? :/ diff --git a/packages/docusaurus-theme-common/src/index.ts b/packages/docusaurus-theme-common/src/index.ts index 078a0dbd66..d0ae395b5b 100644 --- a/packages/docusaurus-theme-common/src/index.ts +++ b/packages/docusaurus-theme-common/src/index.ts @@ -16,6 +16,8 @@ export { FooterLinkItem, } from './utils/useThemeConfig'; +export {useAlternatePageUtils} from './utils/useAlternatePageUtils'; + export {docVersionSearchTag, DEFAULT_SEARCH_TAG} from './utils/searchUtils'; export {isDocsPluginEnabled} from './utils/docsUtils'; diff --git a/packages/docusaurus-theme-common/src/utils/useAlternatePageUtils.ts b/packages/docusaurus-theme-common/src/utils/useAlternatePageUtils.ts new file mode 100644 index 0000000000..22f1e62781 --- /dev/null +++ b/packages/docusaurus-theme-common/src/utils/useAlternatePageUtils.ts @@ -0,0 +1,50 @@ +/** + * 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'; +import {useLocation} from '@docusaurus/router'; + +// Permits to obtain the url of the current page in another locale +// Useful to generate hreflang meta headers etc... +// See https://developers.google.com/search/docs/advanced/crawling/localized-versions +export function useAlternatePageUtils() { + const { + siteConfig: {baseUrl, url}, + i18n: {defaultLocale, currentLocale}, + } = useDocusaurusContext(); + const {pathname} = useLocation(); + + const baseUrlUnlocalized = + currentLocale === defaultLocale + ? baseUrl + : baseUrl.replace(`/${currentLocale}/`, '/'); + + const pathnameSuffix = pathname.replace(baseUrl, ''); + + function getLocalizedBaseUrl(locale: string) { + return locale === defaultLocale + ? `${baseUrlUnlocalized}` + : `${baseUrlUnlocalized}${locale}/`; + } + + // TODO support correct alternate url when localized site is deployed on another domain + function createUrl({ + locale, + fullyQualified, + }: { + locale: string; + // For hreflang SEO headers, we need it to be fully qualified (full protocol/domain/path...) + // For locale dropdown, using a path is good enough + fullyQualified: boolean; + }) { + return `${fullyQualified ? url : ''}${getLocalizedBaseUrl( + locale, + )}${pathnameSuffix}`; + } + + return {createUrl}; +} diff --git a/website/docs/i18n/i18n-introduction.md b/website/docs/i18n/i18n-introduction.md index ef6130c5a5..55057403e8 100644 --- a/website/docs/i18n/i18n-introduction.md +++ b/website/docs/i18n/i18n-introduction.md @@ -32,12 +32,12 @@ The goals of the Docusaurus i18n system are: - **Localize assets**: an image of your site might contain text that should be translated. - **No coupling**: not forced to use any SaaS, yet the integration is possible. - **Easy to use with [Crowdin](http://crowdin.com/)**: multiple Docusaurus v1 sites use Crowdin, and should be able to migrate to v2. +- **Good SEO defaults**: setting useful SEO headers like [`hreflang`](https://developers.google.com/search/docs/advanced/crawling/localized-versions) for you. ### i18n goals (TODO) Features that are **not yet implemented**: -- **Good SEO defaults**: setting useful html meta headers like `hreflang` for you. - **RTL support**: one locale should not be harder to use than another. - **Contextual translations**: reduce friction to contribute to the translation effort. - **Anchor links**: linking should not break when you localize headings.