feat: custom pages (#994)

This commit is contained in:
Endilie Yacop Sucipto 2018-09-29 00:18:38 +08:00 committed by GitHub
parent 7d4d9fe961
commit 8691a2525c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 263 additions and 128 deletions

View file

@ -83,7 +83,6 @@ module.exports = async function start(siteDir, cliOptions = {}) {
const compiler = webpack(config); const compiler = webpack(config);
// webpack-serve // webpack-serve
setTimeout(async () => {
await serve( await serve(
{}, {},
{ {
@ -120,5 +119,4 @@ module.exports = async function start(siteDir, cliOptions = {}) {
}, },
}, },
); );
}, 1000);
}; };

View file

@ -49,7 +49,7 @@ module.exports = async function load(siteDir) {
// pages // pages
const pagesDir = path.resolve(siteDir, 'pages'); const pagesDir = path.resolve(siteDir, 'pages');
const pagesMetadatas = await loadPages(pagesDir); const pagesMetadatas = await loadPages({pagesDir, env, siteConfig});
await generate( await generate(
'pagesMetadatas.js', 'pagesMetadatas.js',
`export default ${JSON.stringify(pagesMetadatas, null, 2)};`, `export default ${JSON.stringify(pagesMetadatas, null, 2)};`,

View file

@ -1,16 +1,57 @@
const globby = require('globby'); const globby = require('globby');
const {encodePath, fileToPath} = require('./utils'); const path = require('path');
const {encodePath, fileToPath, idx} = require('./utils');
async function loadPages(pagesDir) { async function loadPages({pagesDir, env, siteConfig}) {
const pagesFiles = await globby(['**/*.js'], { const pagesFiles = await globby(['**/*.js'], {
cwd: pagesDir, cwd: pagesDir,
}); });
const pagesMetadatas = await Promise.all( const {baseUrl} = siteConfig;
pagesFiles.map(async source => ({
path: encodePath(fileToPath(source)), /* Prepare metadata container */
const pagesMetadatas = [];
/* Translation */
const translationEnabled = idx(env, ['translation', 'enabled']);
const enabledLanguages =
translationEnabled && idx(env, ['translation', 'enabledLanguages']);
const enabledLangTags =
(enabledLanguages && enabledLanguages.map(lang => lang.tag)) || [];
const defaultLangTag = idx(env, ['translation', 'defaultLanguage', 'tag']);
await Promise.all(
pagesFiles.map(async relativeSource => {
const source = path.join(pagesDir, relativeSource);
const pathName = encodePath(fileToPath(relativeSource));
if (translationEnabled && enabledLangTags.length > 0) {
enabledLangTags.forEach(langTag => {
/* default lang should also be available. E.g: /en/users and /users is the same */
if (langTag === defaultLangTag) {
pagesMetadatas.push({
permalink: pathName.replace(/^\//, baseUrl),
language: langTag,
source, source,
})), });
}
const metadata = {
permalink: pathName.replace(/^\//, `${baseUrl}${langTag}/`),
language: langTag,
source,
};
pagesMetadatas.push(metadata);
});
// for defaultLanguage
} else {
const metadata = {
permalink: pathName.replace(/^\//, baseUrl),
source,
};
pagesMetadatas.push(metadata);
}
}),
); );
return pagesMetadatas; return pagesMetadatas;
} }

View file

@ -20,14 +20,23 @@ async function genRoutesConfig({docsMetadatas = {}, pagesMetadatas = []}) {
}`; }`;
} }
function genPagesRoute({path: pagesPath, source}) { function genPagesRoute(metadata) {
const {permalink, source} = metadata;
return ` return `
{ {
path: ${JSON.stringify(pagesPath)}, path: ${JSON.stringify(permalink)},
exact: true, exact: true,
component: Loadable({ component: Loadable({
loader: () => import('@pages/${source}'), loader: () => import(${JSON.stringify(source)}),
loading: Loading loading: Loading,
render(loaded, props) {
let Content = loaded.default;
return (
<Pages {...props} metadata={${JSON.stringify(metadata)}}>
<Content {...props} metadata={${JSON.stringify(metadata)}} />
</Pages>
);
}
}) })
}`; }`;
} }
@ -47,6 +56,7 @@ async function genRoutesConfig({docsMetadatas = {}, pagesMetadatas = []}) {
`import Loadable from 'react-loadable';\n` + `import Loadable from 'react-loadable';\n` +
`import Loading from '@theme/Loading';\n` + `import Loading from '@theme/Loading';\n` +
`import Docs from '@theme/Docs';\n` + `import Docs from '@theme/Docs';\n` +
`import Pages from '@theme/Pages';\n` +
`import NotFound from '@theme/NotFound';\n` + `import NotFound from '@theme/NotFound';\n` +
`const routes = [${docsRoutes},${pagesMetadatas `const routes = [${docsRoutes},${pagesMetadatas
.map(genPagesRoute) .map(genPagesRoute)

View file

@ -7,7 +7,7 @@ module.exports = function loadConfig(siteDir) {
? customThemePath ? customThemePath
: path.resolve(__dirname, '../theme'); : path.resolve(__dirname, '../theme');
const themeComponents = ['Docs', 'Loading', 'NotFound', 'Markdown']; const themeComponents = ['Docs', 'Pages', 'Loading', 'NotFound', 'Markdown'];
themeComponents.forEach(component => { themeComponents.forEach(component => {
if (!require.resolve(path.join(themePath, component))) { if (!require.resolve(path.join(themePath, component))) {
throw new Error( throw new Error(

View file

@ -52,10 +52,14 @@ export default class Docs extends React.Component {
docsSidebars, docsSidebars,
metadata, metadata,
} = this.props; } = this.props;
const {language, version} = metadata;
return ( return (
<Layout {...this.props}> <Layout {...this.props}>
<Helmet> <Helmet>
<title>{(metadata && metadata.title) || siteConfig.title}</title> <title>{(metadata && metadata.title) || siteConfig.title}</title>
{language && <html lang={language} />}
{language && <meta name="docsearch:language" content={language} />}
{version && <meta name="docsearch:version" content={version} />}
</Helmet> </Helmet>
<div>{this.renderSidebar(metadata, docsSidebars, docsMetadatas)}</div> <div>{this.renderSidebar(metadata, docsSidebars, docsMetadatas)}</div>
<div> <div>

View file

@ -5,15 +5,18 @@ import styles from './styles.css';
/* eslint-disable react/prefer-stateless-function */ /* eslint-disable react/prefer-stateless-function */
export default class Layout extends React.Component { export default class Layout extends React.Component {
render() { render() {
const {children, pagesMetadatas, docsMetadatas = {}, location} = this.props; const {
const docsLinks = Object.values(docsMetadatas).map(data => ({ children,
path: `${data.permalink}`, pagesMetadatas = [],
})); docsMetadatas = {},
const routeLinks = [...pagesMetadatas, ...docsLinks].map( location,
} = this.props;
const docsFlatMetadatas = Object.values(docsMetadatas);
const routeLinks = [...pagesMetadatas, ...docsFlatMetadatas].map(
data => data =>
data.path !== location.pathname && ( data.permalink !== location.pathname && (
<li key={data.path}> <li key={data.permalink}>
<Link to={data.path}>{data.path}</Link> <Link to={data.permalink}>{data.permalink}</Link>
</li> </li>
), ),
); );

View file

@ -0,0 +1,21 @@
/* eslint-disable */
import React from 'react';
import {Link} from 'react-router-dom';
import Helmet from 'react-helmet';
import Layout from '@theme/Layout'; // eslint-disable-line
export default class Pages extends React.Component {
render() {
const {metadata, children, siteConfig} = this.props;
const {language} = metadata;
return (
<Layout {...this.props}>
<Helmet defaultTitle={siteConfig.title}>
{language && <html lang={language} />}
{language && <meta name="docsearch:language" content={language} />}
</Helmet>
{children}
</Layout>
);
}
}

View file

@ -17,10 +17,10 @@ module.exports = function createServerConfig(props) {
const {siteConfig, docsMetadatas, pagesMetadatas} = props; const {siteConfig, docsMetadatas, pagesMetadatas} = props;
// static site generator webpack plugin // static site generator webpack plugin
const docsLinks = Object.values(docsMetadatas).map(data => ({ const docsFlatMetadatas = Object.values(docsMetadatas);
path: `${data.permalink}`, const paths = [...docsFlatMetadatas, ...pagesMetadatas].map(
})); data => data.permalink,
const paths = [...docsLinks, ...pagesMetadatas].map(data => data.path); );
config.plugin('siteGenerator').use(staticSiteGenerator, [ config.plugin('siteGenerator').use(staticSiteGenerator, [
{ {
entry: 'main', entry: 'main',

View file

@ -1,17 +1,16 @@
import React from 'react'; import React from 'react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import Layout from '@theme/Layout';
export default class World extends React.Component { export default class World extends React.Component {
render() { render() {
return ( return (
<Layout {...this.props}> <div>
<Helmet> <Helmet>
<title>World</title> <title>World</title>
<link rel="stylesheet" type="text/css" href="/css/basic.css" /> <link rel="stylesheet" type="text/css" href="/css/basic.css" />
</Helmet> </Helmet>
<div>Hello World </div> <div>Hello World </div>
</Layout> </div>
); );
} }
} }

View file

@ -1,17 +1,16 @@
import React from 'react'; import React from 'react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import Layout from '@theme/Layout';
export default class Home extends React.Component { export default class Home extends React.Component {
render() { render() {
return ( return (
<Layout {...this.props}> <div>
<Helmet> <Helmet>
<title>Home</title> <title>Home</title>
<link rel="stylesheet" type="text/css" href="/css/basic.css" /> <link rel="stylesheet" type="text/css" href="/css/basic.css" />
</Helmet> </Helmet>
<div>Home ... </div> <div>Home ... </div>
</Layout> </div>
); );
} }
} }

View file

@ -1,17 +1,16 @@
import React from 'react'; import React from 'react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import Layout from '@theme/Layout';
export default class World extends React.Component { export default class World extends React.Component {
render() { render() {
return ( return (
<Layout {...this.props}> <div>
<Helmet> <Helmet>
<title>World</title> <title>World</title>
<link rel="stylesheet" type="text/css" href="/css/basic.css" /> <link rel="stylesheet" type="text/css" href="/css/basic.css" />
</Helmet> </Helmet>
<div>Hello World </div> <div>Hello World </div>
</Layout> </div>
); );
} }
} }

View file

@ -1,17 +1,16 @@
import React from 'react'; import React from 'react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import Layout from '@theme/Layout';
export default class Home extends React.Component { export default class Home extends React.Component {
render() { render() {
return ( return (
<Layout {...this.props}> <div>
<Helmet> <Helmet>
<title>Home</title> <title>Home</title>
<link rel="stylesheet" type="text/css" href="/css/basic.css" /> <link rel="stylesheet" type="text/css" href="/css/basic.css" />
</Helmet> </Helmet>
<div>Home ... </div> <div>Home ... </div>
</Layout> </div>
); );
} }
} }

View file

@ -1,17 +1,16 @@
import React from 'react'; import React from 'react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import Layout from '@theme/Layout';
export default class World extends React.Component { export default class World extends React.Component {
render() { render() {
return ( return (
<Layout {...this.props}> <div>
<Helmet> <Helmet>
<title>World</title> <title>World</title>
<link rel="stylesheet" type="text/css" href="/css/basic.css" /> <link rel="stylesheet" type="text/css" href="/css/basic.css" />
</Helmet> </Helmet>
<div>Hello World </div> <div>Hello World </div>
</Layout> </div>
); );
} }
} }

View file

@ -1,17 +1,16 @@
import React from 'react'; import React from 'react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import Layout from '@theme/Layout';
export default class Home extends React.Component { export default class Home extends React.Component {
render() { render() {
return ( return (
<Layout {...this.props}> <div>
<Helmet> <Helmet>
<title>Home</title> <title>Home</title>
<link rel="stylesheet" type="text/css" href="/css/basic.css" /> <link rel="stylesheet" type="text/css" href="/css/basic.css" />
</Helmet> </Helmet>
<div>Home ... </div> <div>Home ... </div>
</Layout> </div>
); );
} }
} }

View file

@ -1,17 +1,16 @@
import React from 'react'; import React from 'react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import Layout from '@theme/Layout';
export default class World extends React.Component { export default class World extends React.Component {
render() { render() {
return ( return (
<Layout {...this.props}> <div>
<Helmet> <Helmet>
<title>World</title> <title>World</title>
<link rel="stylesheet" type="text/css" href="/css/basic.css" /> <link rel="stylesheet" type="text/css" href="/css/basic.css" />
</Helmet> </Helmet>
<div>Hello World </div> <div>Hello World </div>
</Layout> </div>
); );
} }
} }

View file

@ -1,17 +1,16 @@
import React from 'react'; import React from 'react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import Layout from '@theme/Layout';
export default class Home extends React.Component { export default class Home extends React.Component {
render() { render() {
return ( return (
<Layout {...this.props}> <div>
<Helmet> <Helmet>
<title>Home</title> <title>Home</title>
<link rel="stylesheet" type="text/css" href="/css/basic.css" /> <link rel="stylesheet" type="text/css" href="/css/basic.css" />
</Helmet> </Helmet>
<div>Home ... </div> <div>Home ... </div>
</Layout> </div>
); );
} }
} }

View file

@ -1,3 +0,0 @@
import React from 'react';
export default () => <div>Baz</div>;

View file

@ -1,3 +0,0 @@
import React from 'react';
export default () => <div>Foo</div>;

View file

@ -1,3 +0,0 @@
import React from 'react';
export default () => <div>Foo in subfolder</div>;

View file

@ -1,3 +0,0 @@
import React from 'react';
export default () => <div>Index</div>;

View file

@ -1,35 +1,116 @@
import loadPages from '@lib/load/pages'; import loadPages from '@lib/load/pages';
import path from 'path'; import path from 'path';
import loadSetup from '../loadSetup';
describe('loadPages', () => { describe('loadPages', () => {
test('valid pages', async () => { test('simple website', async () => {
const pagesDir = path.join(__dirname, '__fixtures__', 'simple-pages'); const {pagesDir, env, siteConfig} = await loadSetup('simple');
const pagesMetadatas = await loadPages(pagesDir); const pagesMetadatas = await loadPages({pagesDir, env, siteConfig});
pagesMetadatas.sort((a, b) => a.path > b.path); // because it was unordered
expect(pagesMetadatas).toEqual([ expect(pagesMetadatas).toEqual([
{ {
path: '/', permalink: '/',
source: 'index.js', source: path.join(pagesDir, 'index.js'),
}, },
{ {
path: '/bar/baz', permalink: '/hello/world',
source: 'bar/baz.js', source: path.join(pagesDir, 'hello', 'world.js'),
}, },
{ ]);
path: '/foo', });
source: 'foo.js',
}, test('versioned website', async () => {
{ const {pagesDir, env, siteConfig} = await loadSetup('versioned');
path: '/foo/', const pagesMetadatas = await loadPages({pagesDir, env, siteConfig});
source: 'foo/index.js', expect(pagesMetadatas).toEqual([
{
permalink: '/',
source: path.join(pagesDir, 'index.js'),
},
{
permalink: '/hello/world',
source: path.join(pagesDir, 'hello', 'world.js'),
},
]);
});
test('versioned & translated website', async () => {
const {pagesDir, env, siteConfig} = await loadSetup('transversioned');
const pagesMetadatas = await loadPages({pagesDir, env, siteConfig});
expect(pagesMetadatas).toEqual([
{
language: 'en',
permalink: '/',
source: path.join(pagesDir, 'index.js'),
},
{
language: 'en',
permalink: '/en/',
source: path.join(pagesDir, 'index.js'),
},
{
language: 'ko',
permalink: '/ko/',
source: path.join(pagesDir, 'index.js'),
},
{
language: 'en',
permalink: '/hello/world',
source: path.join(pagesDir, 'hello', 'world.js'),
},
{
language: 'en',
permalink: '/en/hello/world',
source: path.join(pagesDir, 'hello', 'world.js'),
},
{
language: 'ko',
permalink: '/ko/hello/world',
source: path.join(pagesDir, 'hello', 'world.js'),
},
]);
});
test('translated website', async () => {
const {pagesDir, env, siteConfig} = await loadSetup('translated');
const pagesMetadatas = await loadPages({pagesDir, env, siteConfig});
expect(pagesMetadatas).toEqual([
{
language: 'en',
permalink: '/',
source: path.join(pagesDir, 'index.js'),
},
{
language: 'en',
permalink: '/en/',
source: path.join(pagesDir, 'index.js'),
},
{
language: 'ko',
permalink: '/ko/',
source: path.join(pagesDir, 'index.js'),
},
{
language: 'en',
permalink: '/hello/world',
source: path.join(pagesDir, 'hello', 'world.js'),
},
{
language: 'en',
permalink: '/en/hello/world',
source: path.join(pagesDir, 'hello', 'world.js'),
},
{
language: 'ko',
permalink: '/ko/hello/world',
source: path.join(pagesDir, 'hello', 'world.js'),
}, },
]); ]);
expect(pagesMetadatas).not.toBeNull();
}); });
test('invalid pages', async () => { test('invalid pages', async () => {
const nonExistingDir = path.join(__dirname, '__fixtures__', 'nonExisting'); const {env, siteConfig} = await loadSetup('simple');
const pagesMetadatas = await loadPages(nonExistingDir); const pagesDir = path.join(__dirname, '__fixtures__', 'nonExisting');
const pagesMetadatas = await loadPages({pagesDir, env, siteConfig});
expect(pagesMetadatas).toEqual([]); expect(pagesMetadatas).toEqual([]);
}); });
}); });

View file

@ -1,18 +1,17 @@
import React from 'react'; import React from 'react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import Layout from '@theme/Layout';
import Todo from '@site/components/Todo'; import Todo from '@site/components/Todo';
export default class Home extends React.Component { export default class Home extends React.Component {
render() { render() {
return ( return (
<Layout {...this.props}> <div>
<Helmet> <Helmet>
<title>Todo App</title> <title>Todo App</title>
<link rel="stylesheet" type="text/css" href="/css/basic.css" /> <link rel="stylesheet" type="text/css" href="/css/basic.css" />
</Helmet> </Helmet>
<Todo /> <Todo />
</Layout> </div>
); );
} }
} }

View file

@ -1,17 +1,16 @@
import React from 'react'; import React from 'react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import Layout from '@theme/Layout';
import Tictactoe from '@site/components/Tictactoe'; import Tictactoe from '@site/components/Tictactoe';
export default class Home extends React.Component { export default class Home extends React.Component {
render() { render() {
return ( return (
<Layout {...this.props}> <div>
<Helmet> <Helmet>
<title>Tic Tac Toe</title> <title>Tic Tac Toe</title>
</Helmet> </Helmet>
<Tictactoe /> <Tictactoe />
</Layout> </div>
); );
} }
} }

View file

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import YouTube from 'react-youtube'; import YouTube from 'react-youtube';
import Layout from '@theme/Layout';
export default class Player extends React.Component { export default class Player extends React.Component {
render() { render() {
@ -14,15 +13,15 @@ export default class Player extends React.Component {
}; };
return ( return (
<Layout {...this.props}> <div>
<Helmet> <Helmet>
<title>My Youtube</title> <title>My Youtube</title>
</Helmet> </Helmet>
<p align="center"> <div align="center">
{/* this is a React-youtube component */} {/* this is a React-youtube component */}
<YouTube videoId="d9IxdwEFk1c" opts={opts} onReady={this._onReady} /> <YouTube videoId="d9IxdwEFk1c" opts={opts} onReady={this._onReady} />
</p> </div>
</Layout> </div>
); );
} }