mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-05 21:27:24 +02:00
* Allow other routes than /docs in the URL siteConfig.js has a new mandatory field named *docsRoute* which default value is 'docs' and that can be customized by the user. This change will allow users who uses the library to host guides and tutorials to customize their websites by assign 'docsRoute' values like 'tutorials' or 'guides'. Fixes #879 * Make "docsRoute" field optional * Isolate docsRoute login in getDocsRoute function * Rename docsRoute to docsUrl * Run prettier * Remove old folders * fix: Restore docusaurus reference link * fix: Add `docsUrl` param fallback. Refactor multiple function calls * Fix linting errors * Update description for docsUrl field * Reduce redundant calls to getDocsUrl * Replace a missed use case for `docsUrl` instead of the function call * Move `getDocsUrl` out from `server/routing.js` to `server/utils.js` **Why?** Because `routing.js` is exporting all router RegEx's, and the `getDocsUrl` suffices more as a util * WiP: Align leading slashes and fix routing around `docsUrl` Checklist: - [x] Added `removeDuplicateLeadingSlashes` util to make sure there is only one leading slash - [-] Fix edge cases for routing: - [x] `docsUrl: ''` - [ ] `docsUrl: '/'` - [ ] make it work with languages - [ ] make it work with versioning * Make leading slashes canonical cross routing and generated links This ensures correct routing for customized `baseUrl` and `docsUrl`. - Changed all routing functions to take `siteConfig` instead of `siteConfig.baseUrl` - Updated tests accordingly * Alternative fallback for `docsUrl` * rework/ fix implementation * cleanup * refactor and add docs for config props * fix typo * fix broken url
339 lines
11 KiB
JavaScript
339 lines
11 KiB
JavaScript
/**
|
|
* Copyright (c) 2017-present, Facebook, Inc.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
const CWD = process.cwd();
|
|
|
|
const React = require('react');
|
|
const fs = require('fs');
|
|
const classNames = require('classnames');
|
|
|
|
const loadConfig = require('../../server/config');
|
|
|
|
const siteConfig = loadConfig(`${CWD}/siteConfig.js`);
|
|
const translation = require('../../server/translation.js');
|
|
const env = require('../../server/env.js');
|
|
|
|
const translate = require('../../server/translate.js').translate;
|
|
const setLanguage = require('../../server/translate.js').setLanguage;
|
|
|
|
const readMetadata = require('../../server/readMetadata.js');
|
|
|
|
readMetadata.generateMetadataDocs();
|
|
const Metadata = require('../metadata.js');
|
|
const {idx, getPath} = require('../utils.js');
|
|
|
|
const extension = siteConfig.cleanUrl ? '' : '.html';
|
|
|
|
// language dropdown nav item for when translations are enabled
|
|
class LanguageDropDown extends React.Component {
|
|
render() {
|
|
setLanguage(this.props.language || 'en');
|
|
const helpTranslateString = translate(
|
|
'Help Translate|recruit community translators for your project',
|
|
);
|
|
const docsPart = `${siteConfig.docsUrl ? `${siteConfig.docsUrl}/` : ''}`;
|
|
// add all enabled languages to dropdown
|
|
const enabledLanguages = env.translation
|
|
.enabledLanguages()
|
|
.filter(lang => lang.tag !== this.props.language)
|
|
.map(lang => {
|
|
// build the href so that we try to stay in current url but change the language.
|
|
let href = siteConfig.baseUrl + lang.tag;
|
|
if (
|
|
this.props.current &&
|
|
this.props.current.permalink &&
|
|
this.props.language
|
|
) {
|
|
href =
|
|
siteConfig.baseUrl +
|
|
this.props.current.permalink.replace(
|
|
new RegExp(`^${docsPart}${this.props.language}/`),
|
|
`${docsPart}${lang.tag}/`,
|
|
);
|
|
} else if (this.props.current.id && this.props.current.id !== 'index') {
|
|
href = `${siteConfig.baseUrl + lang.tag}/${this.props.current.id}`;
|
|
}
|
|
return (
|
|
<li key={lang.tag}>
|
|
<a href={getPath(href, this.props.cleanUrl)}>{lang.name}</a>
|
|
</li>
|
|
);
|
|
});
|
|
// if no languages are enabled besides English, return null
|
|
if (enabledLanguages.length < 1) {
|
|
return null;
|
|
}
|
|
|
|
// Get the current language full name for display in the header nav
|
|
const currentLanguage = env.translation
|
|
.enabledLanguages()
|
|
.filter(lang => lang.tag === this.props.language)
|
|
.map(lang => lang.name);
|
|
|
|
// add Crowdin project recruiting link
|
|
if (siteConfig.translationRecruitingLink) {
|
|
enabledLanguages.push(
|
|
<li key="recruiting">
|
|
<a
|
|
href={siteConfig.translationRecruitingLink}
|
|
target="_blank"
|
|
rel="noreferrer noopener">
|
|
{helpTranslateString}
|
|
</a>
|
|
</li>,
|
|
);
|
|
}
|
|
|
|
return (
|
|
<span>
|
|
<li key="languages">
|
|
<a id="languages-menu" href="#">
|
|
<img
|
|
className="languages-icon"
|
|
src={`${this.props.baseUrl}img/language.svg`}
|
|
alt="Languages icon"
|
|
/>
|
|
{currentLanguage}
|
|
</a>
|
|
<div id="languages-dropdown" className="hide">
|
|
<ul id="languages-dropdown-items">{enabledLanguages}</ul>
|
|
</div>
|
|
</li>
|
|
<script
|
|
dangerouslySetInnerHTML={{
|
|
__html: `
|
|
const languagesMenuItem = document.getElementById("languages-menu");
|
|
const languagesDropDown = document.getElementById("languages-dropdown");
|
|
languagesMenuItem.addEventListener("click", function(event) {
|
|
event.preventDefault();
|
|
|
|
if (languagesDropDown.className == "hide") {
|
|
languagesDropDown.className = "visible";
|
|
} else {
|
|
languagesDropDown.className = "hide";
|
|
}
|
|
});
|
|
`,
|
|
}}
|
|
/>
|
|
</span>
|
|
);
|
|
}
|
|
}
|
|
|
|
// header navbar used by all pages generated with docusaurus
|
|
class HeaderNav extends React.Component {
|
|
// function to generate each header link, used with each object in siteConfig.headerLinks
|
|
makeLinks(link) {
|
|
let href;
|
|
let docItemActive = false;
|
|
let docGroupActive = false;
|
|
if (link.search && this.props.config.algolia) {
|
|
// return algolia search bar
|
|
const placeholder = this.props.config.algolia.placeholder || 'Search';
|
|
return (
|
|
<li className="navSearchWrapper reactNavSearchWrapper" key="search">
|
|
<input
|
|
id="search_input_react"
|
|
type="text"
|
|
placeholder={placeholder}
|
|
title={placeholder}
|
|
/>
|
|
</li>
|
|
);
|
|
}
|
|
if (link.languages) {
|
|
if (
|
|
env.translation.enabled &&
|
|
env.translation.enabledLanguages().length > 1
|
|
) {
|
|
return (
|
|
<LanguageDropDown
|
|
baseUrl={this.props.baseUrl}
|
|
language={this.props.language}
|
|
current={this.props.current}
|
|
cleanUrl={this.props.config.cleanUrl}
|
|
key="languagedropdown"
|
|
/>
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
if (link.doc) {
|
|
// set link to document with current page's language/version
|
|
const langPart = env.translation.enabled
|
|
? `${this.props.language || 'en'}-`
|
|
: '';
|
|
const versionPart =
|
|
env.versioning.enabled && this.props.version !== 'next'
|
|
? `version-${this.props.version || env.versioning.defaultVersion}-`
|
|
: '';
|
|
const id = langPart + versionPart + link.doc;
|
|
if (!Metadata[id]) {
|
|
let errorStr = `Processing the following \`doc\` field in \`headerLinks\` within \`siteConfig.js\`: '${
|
|
link.doc
|
|
}'`;
|
|
if (id === link.doc) {
|
|
errorStr +=
|
|
' It looks like there is no document with that id that exists in your docs directory. Please double check the spelling of your `doc` field and the `id` fields of your docs.';
|
|
} else {
|
|
errorStr += `${'. Check the spelling of your `doc` field. If that seems sane, and a document in your docs folder exists with that `id` value, \nthen this is likely a bug in Docusaurus.' +
|
|
' Docusaurus thinks one or both of translations (currently set to: '}${
|
|
env.translation.enabled
|
|
}) or versioning (currently set to: ${
|
|
env.versioning.enabled
|
|
}) is enabled when maybe they should not be. \nThus my internal id for this doc is: '${id}'. Please file an issue for this possible bug on GitHub.`;
|
|
}
|
|
throw new Error(errorStr);
|
|
}
|
|
href =
|
|
this.props.config.baseUrl +
|
|
getPath(Metadata[id].permalink, this.props.config.cleanUrl);
|
|
|
|
const {id: currentID, sidebar} = this.props.current;
|
|
docItemActive = currentID && currentID === id;
|
|
docGroupActive = sidebar && sidebar === Metadata[id].sidebar;
|
|
} else if (link.page) {
|
|
// set link to page with current page's language if appropriate
|
|
const language = this.props.language || '';
|
|
if (fs.existsSync(`${CWD}/pages/en/${link.page}.js`)) {
|
|
href =
|
|
siteConfig.baseUrl +
|
|
(env.translation.enabled ? `${language}/` : '') +
|
|
link.page +
|
|
extension;
|
|
} else {
|
|
href = siteConfig.baseUrl + link.page + extension;
|
|
}
|
|
} else if (link.href) {
|
|
// set link to specified href
|
|
href = link.href;
|
|
} else if (link.blog) {
|
|
// set link to blog url
|
|
href = `${this.props.baseUrl}blog/`;
|
|
}
|
|
const itemClasses = classNames({
|
|
siteNavGroupActive:
|
|
(link.doc && docGroupActive) || (link.blog && this.props.current.blog),
|
|
siteNavItemActive:
|
|
docItemActive ||
|
|
(link.blog && this.props.current.blogListing) ||
|
|
(link.page && link.page === this.props.current.id),
|
|
});
|
|
const i18n = translation[this.props.language];
|
|
return (
|
|
<li key={`${link.label}page`} className={itemClasses}>
|
|
<a href={href} target={link.external ? '_blank' : '_self'}>
|
|
{idx(i18n, ['localized-strings', 'links', link.label]) || link.label}
|
|
</a>
|
|
</li>
|
|
);
|
|
}
|
|
|
|
renderResponsiveNav() {
|
|
const headerLinks = this.props.config.headerLinks;
|
|
// add language drop down to end if location not specified
|
|
let languages = false;
|
|
headerLinks.forEach(link => {
|
|
if (link.languages) {
|
|
languages = true;
|
|
}
|
|
});
|
|
if (!languages) {
|
|
headerLinks.push({languages: true});
|
|
}
|
|
let search = false;
|
|
headerLinks.forEach(link => {
|
|
if (
|
|
link.doc &&
|
|
!fs.existsSync(`${CWD}/../${readMetadata.getDocsPath()}/`)
|
|
) {
|
|
throw new Error(
|
|
`You have 'doc' in your headerLinks, but no '${readMetadata.getDocsPath()}' folder exists one level up from ` +
|
|
`'website' folder. Did you run \`docusaurus-init\` or \`npm run examples\`? If so, ` +
|
|
`make sure you rename 'docs-examples-from-docusaurus' to 'docs'.`,
|
|
);
|
|
}
|
|
if (link.blog && !fs.existsSync(`${CWD}/blog/`)) {
|
|
throw new Error(
|
|
"You have 'blog' in your headerLinks, but no 'blog' folder exists in your " +
|
|
"'website' folder. Did you run `docusaurus-init` or `npm run examples`? If so, " +
|
|
"make sure you rename 'blog-examples-from-docusaurus' to 'blog'.",
|
|
);
|
|
}
|
|
if (link.page && !fs.existsSync(`${CWD}/pages/`)) {
|
|
throw new Error(
|
|
"You have 'page' in your headerLinks, but no 'pages' folder exists in your " +
|
|
"'website' folder.",
|
|
);
|
|
}
|
|
// We will add search bar to end if location not specified
|
|
if (link.search) {
|
|
search = true;
|
|
}
|
|
});
|
|
if (!search && this.props.config.algolia) {
|
|
headerLinks.push({search: true});
|
|
}
|
|
return (
|
|
<div className="navigationWrapper navigationSlider">
|
|
<nav className="slidingNav">
|
|
<ul className="nav-site nav-site-internal">
|
|
{headerLinks.map(this.makeLinks, this)}
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
render() {
|
|
const headerClass = siteConfig.headerIcon
|
|
? 'headerTitleWithLogo'
|
|
: 'headerTitle';
|
|
const versionsLink =
|
|
this.props.baseUrl +
|
|
(env.translation.enabled
|
|
? `${this.props.language}/versions${extension}`
|
|
: `versions${extension}`);
|
|
return (
|
|
<div className="fixedHeaderContainer">
|
|
<div className="headerWrapper wrapper">
|
|
<header>
|
|
<a
|
|
href={
|
|
this.props.baseUrl +
|
|
(env.translation.enabled ? this.props.language : '')
|
|
}>
|
|
{siteConfig.headerIcon && (
|
|
<img
|
|
className="logo"
|
|
src={this.props.baseUrl + siteConfig.headerIcon}
|
|
alt={siteConfig.title}
|
|
/>
|
|
)}
|
|
{!this.props.config.disableHeaderTitle && (
|
|
<h2 className={headerClass}>{this.props.title}</h2>
|
|
)}
|
|
</a>
|
|
{env.versioning.enabled && (
|
|
<a href={versionsLink}>
|
|
<h3>{this.props.version || env.versioning.defaultVersion}</h3>
|
|
</a>
|
|
)}
|
|
{this.renderResponsiveNav()}
|
|
</header>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
HeaderNav.defaultProps = {
|
|
current: {},
|
|
};
|
|
|
|
module.exports = HeaderNav;
|