diff --git a/docs/api-site-config.md b/docs/api-site-config.md index b706c69a92..95fbe89495 100644 --- a/docs/api-site-config.md +++ b/docs/api-site-config.md @@ -74,6 +74,8 @@ headerLinks: [ `blogSidebarCount` - Control the number of blog posts that show up in the sidebar. See the [adding a blog docs](guides-blog.md#changing-how-many-blog-posts-show-on-sidebar) for more information. +`cleanUrl` - If `true`, allow URLs with no `html` extension. Example: request to URL https://docusaurus.io/docs/installation will returns the same result as https://docusaurus.io/docs/installation.html. + `cname` - The CNAME for your website. It will go into a `CNAME` file when your site it built. `customDocsPath` - By default, Docusaurus expects your documentation to be in a directory called `docs`. This directory is at the same level as the `website` directory (i.e., not inside the `website` directory). You can specify a custom path to your documentation with this field. **Note that all of your documentation `*.md` files must still reside in a flat hierarchy. You cannot have your documents in nested directories**. @@ -250,6 +252,7 @@ const siteConfig = { twitterUsername: 'docusaurus', twitterImage: 'img/docusaurus.png', ogImage: 'img/docusaurus.png', + cleanUrl: true, scrollToTop: true, scrollToTopOptions: { zIndex: 100 diff --git a/lib/core/BlogPageLayout.js b/lib/core/BlogPageLayout.js index 77cf2116de..e3a815f4e7 100644 --- a/lib/core/BlogPageLayout.js +++ b/lib/core/BlogPageLayout.js @@ -11,6 +11,7 @@ const Container = require('./Container.js'); const MetadataBlog = require('./MetadataBlog.js'); const React = require('react'); const Site = require('./Site.js'); +const utils = require('./utils.js'); // used to generate entire blog pages, i.e. collection of truncated blog posts class BlogPageLayout extends React.Component { @@ -45,7 +46,10 @@ class BlogPageLayout extends React.Component { post={post} content={post.content} truncate={true} - key={post.path + post.title} + key={ + utils.getPath(post.path, this.props.config.cleanUrl) + + post.title + } config={this.props.config} /> ); diff --git a/lib/core/BlogPost.js b/lib/core/BlogPost.js index 09c051b4f7..19126dcb94 100644 --- a/lib/core/BlogPost.js +++ b/lib/core/BlogPost.js @@ -8,7 +8,7 @@ const MarkdownBlock = require('./MarkdownBlock.js'); const React = require('react'); -const utils = require('./utils'); +const utils = require('./utils.js'); // inner blog component for the article itself, without sidebar/header/footer class BlogPost extends React.Component { @@ -24,7 +24,12 @@ class BlogPost extends React.Component { Read More @@ -73,7 +78,12 @@ class BlogPost extends React.Component { const post = this.props.post; return (

- + {post.title}

diff --git a/lib/core/BlogPostLayout.js b/lib/core/BlogPostLayout.js index 9b1e0c6f98..b0a12dd472 100644 --- a/lib/core/BlogPostLayout.js +++ b/lib/core/BlogPostLayout.js @@ -10,11 +10,13 @@ const BlogPost = require('./BlogPost.js'); const BlogSidebar = require('./BlogSidebar.js'); const Container = require('./Container.js'); const Site = require('./Site.js'); +const utils = require('./utils.js'); // used for entire blog posts, i.e., each written blog article with sidebar with site header/footer class BlogPostLayout extends React.Component { renderSocialButtons() { - const post = this.props.metadata; + let post = this.props.metadata; + post.path = utils.getPath(post.path, this.props.config.cleanUrl); const fbComment = this.props.config.facebookAppId && this.props.config.facebookComments && ( @@ -92,10 +94,12 @@ class BlogPostLayout extends React.Component { } render() { + let post = this.props.metadata; + post.path = utils.getPath(post.path, this.props.config.cleanUrl); return (
+ href={metadata.previous_id + extension}> ←{' '} {i18n ? translation[this.props.metadata.language][ @@ -70,7 +71,7 @@ class DocsLayout extends React.Component { {metadata.next_id && ( + href={metadata.next_id + extension}> {i18n ? translation[this.props.metadata.language][ 'localized-strings' diff --git a/lib/core/nav/HeaderNav.js b/lib/core/nav/HeaderNav.js index d9d8a1cab9..4da7047c44 100644 --- a/lib/core/nav/HeaderNav.js +++ b/lib/core/nav/HeaderNav.js @@ -20,6 +20,7 @@ const setLanguage = require('../../server/translate.js').setLanguage; const readMetadata = require('../../server/readMetadata.js'); readMetadata.generateMetadataDocs(); const Metadata = require('../metadata.js'); +const utils = require('../utils.js'); // language dropdown nav item for when translations are enabled class LanguageDropDown extends React.Component { @@ -169,7 +170,9 @@ class HeaderNav extends React.Component { } throw new Error(errorStr); } - href = this.props.config.baseUrl + Metadata[id].permalink; + href = + this.props.config.baseUrl + + utils.getPath(Metadata[id].permalink, this.props.config.cleanUrl); const {id: currentID, sidebar} = this.props.current; docItemActive = currentID && currentID === id; @@ -177,14 +180,15 @@ class HeaderNav extends React.Component { } else if (link.page) { // set link to page with current page's language if appropriate const language = this.props.language || ''; + const extension = siteConfig.cleanUrl ? '' : '.html'; if (fs.existsSync(CWD + '/pages/en/' + link.page + '.js')) { href = siteConfig.baseUrl + (language ? language + '/' : '') + link.page + - '.html'; + extension; } else { - href = siteConfig.baseUrl + link.page + '.html'; + href = siteConfig.baseUrl + link.page + extension; } } else if (link.href) { // set link to specified href diff --git a/lib/core/nav/SideNav.js b/lib/core/nav/SideNav.js index 8f27c4c4a5..35448cf5de 100644 --- a/lib/core/nav/SideNav.js +++ b/lib/core/nav/SideNav.js @@ -10,6 +10,7 @@ const classNames = require('classnames'); const siteConfig = require(process.cwd() + '/siteConfig.js'); const translation = require('../../server/translation.js'); +const utils = require('../utils.js'); class SideNav extends React.Component { render() { @@ -81,16 +82,22 @@ class SideNav extends React.Component { } return localizedString; } + // return link to doc in sidebar getLink(metadata) { if (metadata.permalink) { - if (metadata.permalink.match(/^https?:/)) { - return metadata.permalink; + const targetLink = utils.getPath(metadata.permalink, siteConfig.cleanUrl); + if (targetLink.match(/^https?:/)) { + return targetLink; } - return siteConfig.baseUrl + metadata.permalink; + return siteConfig.baseUrl + targetLink; } if (metadata.path) { - return siteConfig.baseUrl + 'blog/' + metadata.path; + return ( + siteConfig.baseUrl + + 'blog/' + + utils.getPath(metadata.path, siteConfig.cleanUrl) + ); } return null; } diff --git a/lib/core/utils.js b/lib/core/utils.js index 4d880763b9..0f99620eb3 100644 --- a/lib/core/utils.js +++ b/lib/core/utils.js @@ -20,8 +20,20 @@ function extractBlogPostSummary(content) { return content.substring(0, BLOG_POST_SUMMARY_LENGTH); } +function getPath(path, cleanUrl = false) { + if (cleanUrl) { + if (path.endsWith('/index.html')) { + return path.replace(/\/index.html$/, ''); + } else { + return path.replace(/\.html$/, ''); + } + } + return path; +} + module.exports = { blogPostHasTruncateMarker, extractBlogPostBeforeTruncate, extractBlogPostSummary, + getPath, }; diff --git a/lib/server/generate.js b/lib/server/generate.js index 2d547edd7d..93b5104209 100644 --- a/lib/server/generate.js +++ b/lib/server/generate.js @@ -41,8 +41,14 @@ async function execute() { // create the folder path for a file if it does not exist, then write the file function writeFileAndCreateFolder(file, content) { mkdirp.sync(path.dirname(file)); - fs.writeFileSync(file, content); + + // build extra file for extension-less url if "cleanUrl" siteConfig is true + if (siteConfig.cleanUrl && file.indexOf('index.html') === -1) { + const extraFile = file.replace(/\.html$/, '/index.html'); + mkdirp.sync(path.dirname(extraFile)); + fs.writeFileSync(extraFile, content); + } } const TABLE_OF_CONTENTS_TOKEN = ''; @@ -156,6 +162,7 @@ async function execute() { // replace any links to markdown files to their website html links Object.keys(mdToHtml).forEach(function(key, index) { let link = mdToHtml[key]; + link = siteConfig.cleanUrl ? link.replace(/\.html$/, '') : link; link = link.replace('/en/', '/' + language + '/'); link = link.replace( '/VERSION/', @@ -196,12 +203,15 @@ async function execute() { env.translation.enabled && metadata.permalink.indexOf('docs/en') !== -1 ) { + const redirectlink = siteConfig.cleanUrl + ? metadata.permalink.replace(/\.html$/, '') + : metadata.permalink; const redirectComp = ( ); const redirectStr = renderToStaticMarkupWithDoctype(redirectComp); diff --git a/lib/server/server.js b/lib/server/server.js index 47e0db30db..4be3d78dca 100644 --- a/lib/server/server.js +++ b/lib/server/server.js @@ -199,6 +199,7 @@ function execute(port) { // replace any links to markdown files to their website html links Object.keys(mdToHtml).forEach(function(key, index) { let link = mdToHtml[key]; + link = siteConfig.cleanUrl ? link.replace(/\.html$/, '') : link; link = link.replace('/en/', '/' + language + '/'); link = link.replace( '/VERSION/', @@ -271,7 +272,7 @@ function execute(port) { }); // Handle all requests for blog pages and posts. - app.get(/blog\/.*html$/, (req, res) => { + app.get(/^\/blog\/.*html$/, (req, res) => { // Regenerate the blog metadata in case it has changed. Consider improving // this to regenerate on file save rather than on page request. reloadMetadataBlog(); @@ -305,7 +306,7 @@ function execute(port) { // send corresponding blog page if appropriate if (parts[1] === 'index.html') { res.send(blogPages['/index.html']); - } else if (parts[1].endsWith('/index.html')) { + } else if (parts[1].endsWith('/index.html') && blogPages[parts[1]]) { res.send(blogPages[parts[1]]); } else if (parts[1].match(/page([0-9]+)/)) { if (parts[1].endsWith('/')) { @@ -314,9 +315,14 @@ function execute(port) { res.send(blogPages[parts[1] + '/index.html']); } } else { - // else send corresponding blog post + // send corresponding blog post. Ex: request to "blog/test/index.html" or + // "blog/test.html" will return html rendered version of "blog/test.md" let file = parts[1]; - file = file.replace(/\.html$/, '.md'); + if (file.endsWith('/index.html')) { + file = file.replace(/\/index.html$/, '.md'); + } else { + file = file.replace(/\.html$/, '.md'); + } file = file.replace(new RegExp('/', 'g'), '-'); file = join(CWD, 'blog', file); @@ -527,23 +533,35 @@ function execute(port) { app.use(siteConfig.baseUrl, express.static(join(__dirname, '..', 'static'))); // "redirect" requests to pages ending with "/" or no extension so that, - // for example, request to "blog" returns same result as "blog/index.html" + // for example, request to "blog" returns "blog/index.html" or "blog.html" app.get(/\/[^\.]*\/?$/, (req, res) => { - let slash = req.path.toString().endsWith('/') ? '' : '/'; - request.get( - 'http://localhost:' + port + req.path + slash + 'index.html', - (err, response, body) => { - if (!err) { + const requestFile = (url, notFoundCallback) => { + request.get(url, (error, response, body) => { + if (!error) { if (response) { - res.status(response.statusCode).send(body); + if (response.statusCode === 404 && notFoundCallback) { + notFoundCallback(); + } else { + res.status(response.statusCode).send(body); + } } else { console.error('No response'); } } else { - console.error('Request failed:', err); + console.error('Request failed:', error); } - } - ); + }); + }; + let slash = req.path.toString().endsWith('/') ? '' : '/'; + let requestUrl = 'http://localhost:' + port + req.path; + requestFile(requestUrl + slash + 'index.html', () => { + requestFile( + slash === '/' + ? requestUrl + '.html' + : requestUrl.replace(/\/$/, '.html'), + null + ); + }); }); // Start LiveReload server. diff --git a/website/pages/en/help.js b/website/pages/en/help.js index c99f1af83c..7edd14a4f8 100755 --- a/website/pages/en/help.js +++ b/website/pages/en/help.js @@ -18,7 +18,7 @@ class Help extends React.Component { { title: Browse the docs, content: ( - `Learn more about Docusaurus using the [official documentation](${siteConfig.baseUrl}docs/${this.props.language}/installation.html).` + `Learn more about Docusaurus using the [official documentation](${siteConfig.baseUrl}docs/${this.props.language}/installation).` ), }, { diff --git a/website/pages/en/index.js b/website/pages/en/index.js index 73587e4d6f..0450d565e5 100755 --- a/website/pages/en/index.js +++ b/website/pages/en/index.js @@ -48,7 +48,7 @@ class HomeSplash extends React.Component {
@@ -92,7 +92,7 @@ class Index extends React.Component { { content: ( `Save time and focus on your project's documentation. Simply - write docs and blog posts with [Markdown](${siteConfig.baseUrl}docs/${this.props.language}/doc-markdown.html) + write docs and blog posts with [Markdown](${siteConfig.baseUrl}docs/${this.props.language}/doc-markdown) and Docusaurus will publish a set of static html files ready to serve.` ), @@ -103,7 +103,7 @@ class Index extends React.Component { }, { content: ( - `[Extend or customize](${siteConfig.baseUrl}docs/${this.props.language}/api-pages.html) + `[Extend or customize](${siteConfig.baseUrl}docs/${this.props.language}/api-pages) your project's layout by reusing React. Docusaurus can be extended while reusing the same header and footer.` ), @@ -114,7 +114,7 @@ class Index extends React.Component { }, { content: ( - `[Localization](${siteConfig.baseUrl}docs/${this.props.language}/translation.html) + `[Localization](${siteConfig.baseUrl}docs/${this.props.language}/translation) comes pre-configured. Use [Crowdin](https://crowdin.com/) to translate your docs into over 70 languages.` ), @@ -134,7 +134,7 @@ class Index extends React.Component { { content: ( `Support users on all versions of your project. Document - [versioning](${siteConfig.baseUrl}docs/${this.props.language}/versioning.html) + [versioning](${siteConfig.baseUrl}docs/${this.props.language}/versioning) helps you keep documentation in sync with project releases.` ), image: `${siteConfig.baseUrl}img/versioning.svg`, @@ -144,7 +144,7 @@ class Index extends React.Component { }, { content: ( - `Make it easy for your community to [find](${siteConfig.baseUrl}docs/${this.props.language}/search.html) what they need in your documentation. + `Make it easy for your community to [find](${siteConfig.baseUrl}docs/${this.props.language}/search) what they need in your documentation. We proudly support [Algolia documentation search](https://www.algolia.com/).` ), image: `${siteConfig.baseUrl}img/search.svg`, @@ -161,7 +161,7 @@ class Index extends React.Component { contents={[ { content: ( - `Get [up and running](${siteConfig.baseUrl}docs/${this.props.language}/site-creation.html) + `Get [up and running](${siteConfig.baseUrl}docs/${this.props.language}/site-creation) quickly without having to worry about site design.` ), imageAlign: "right", @@ -180,7 +180,7 @@ class Index extends React.Component { content: ( `Make design and documentation changes by using the included [live server](${siteConfig.baseUrl}docs/${this.props.language}/site-preparation#verifying-installation). - [Publish](${siteConfig.baseUrl}docs/${this.props.language}/publishing.html) + [Publish](${siteConfig.baseUrl}docs/${this.props.language}/publishing) your site to GitHub pages or other static file hosts manually, using a script, or with continuous integration like CircleCI.` @@ -200,10 +200,10 @@ class Index extends React.Component { { content: ( `Docusaurus currently provides support to help your website - use [translations](${siteConfig.baseUrl}docs/${this.props.language}/translation.html), - [search](${siteConfig.baseUrl}docs/${this.props.language}/search.html), - and [versioning](${siteConfig.baseUrl}docs/${this.props.language}/versioning.html), - along with some other special [documentation markdown features](${siteConfig.baseUrl}docs/${this.props.language}/doc-markdown.html). + use [translations](${siteConfig.baseUrl}docs/${this.props.language}/translation), + [search](${siteConfig.baseUrl}docs/${this.props.language}/search), + and [versioning](${siteConfig.baseUrl}docs/${this.props.language}/versioning), + along with some other special [documentation markdown features](${siteConfig.baseUrl}docs/${this.props.language}/doc-markdown). If you have ideas for useful features, feel free to contribute on [GitHub](https://github.com/facebook/docusaurus)!` ), @@ -223,7 +223,7 @@ class Index extends React.Component { diff --git a/website/pages/en/versions.js b/website/pages/en/versions.js index 5992e420fb..a26c82c96b 100644 --- a/website/pages/en/versions.js +++ b/website/pages/en/versions.js @@ -33,7 +33,7 @@ class Versions extends React.Component { {latestVersion} - Documentation + Documentation Release Notes @@ -51,7 +51,7 @@ class Versions extends React.Component { Documentation @@ -75,7 +75,7 @@ class Versions extends React.Component { Documentation diff --git a/website/siteConfig.js b/website/siteConfig.js index 210a9b145f..d32ef5ff86 100644 --- a/website/siteConfig.js +++ b/website/siteConfig.js @@ -62,6 +62,7 @@ const siteConfig = { ogImage: 'img/docusaurus.png', twitterImage: 'img/docusaurus.png', onPageNav: 'separate', + cleanUrl: true, scrollToTop: true, scrollToTopOptions: { zIndex: 100