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);
// webpack-serve
setTimeout(async () => {
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
const pagesDir = path.resolve(siteDir, 'pages');
const pagesMetadatas = await loadPages(pagesDir);
const pagesMetadatas = await loadPages({pagesDir, env, siteConfig});
await generate(
'pagesMetadatas.js',
`export default ${JSON.stringify(pagesMetadatas, null, 2)};`,

View file

@ -1,16 +1,57 @@
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'], {
cwd: pagesDir,
});
const pagesMetadatas = await Promise.all(
pagesFiles.map(async source => ({
path: encodePath(fileToPath(source)),
const {baseUrl} = siteConfig;
/* 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,
})),
});
}
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;
}

View file

@ -20,14 +20,23 @@ async function genRoutesConfig({docsMetadatas = {}, pagesMetadatas = []}) {
}`;
}
function genPagesRoute({path: pagesPath, source}) {
function genPagesRoute(metadata) {
const {permalink, source} = metadata;
return `
{
path: ${JSON.stringify(pagesPath)},
path: ${JSON.stringify(permalink)},
exact: true,
component: Loadable({
loader: () => import('@pages/${source}'),
loading: Loading
loader: () => import(${JSON.stringify(source)}),
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 Loading from '@theme/Loading';\n` +
`import Docs from '@theme/Docs';\n` +
`import Pages from '@theme/Pages';\n` +
`import NotFound from '@theme/NotFound';\n` +
`const routes = [${docsRoutes},${pagesMetadatas
.map(genPagesRoute)

View file

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

View file

@ -52,10 +52,14 @@ export default class Docs extends React.Component {
docsSidebars,
metadata,
} = this.props;
const {language, version} = metadata;
return (
<Layout {...this.props}>
<Helmet>
<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>
<div>{this.renderSidebar(metadata, docsSidebars, docsMetadatas)}</div>
<div>

View file

@ -5,15 +5,18 @@ import styles from './styles.css';
/* eslint-disable react/prefer-stateless-function */
export default class Layout extends React.Component {
render() {
const {children, pagesMetadatas, docsMetadatas = {}, location} = this.props;
const docsLinks = Object.values(docsMetadatas).map(data => ({
path: `${data.permalink}`,
}));
const routeLinks = [...pagesMetadatas, ...docsLinks].map(
const {
children,
pagesMetadatas = [],
docsMetadatas = {},
location,
} = this.props;
const docsFlatMetadatas = Object.values(docsMetadatas);
const routeLinks = [...pagesMetadatas, ...docsFlatMetadatas].map(
data =>
data.path !== location.pathname && (
<li key={data.path}>
<Link to={data.path}>{data.path}</Link>
data.permalink !== location.pathname && (
<li key={data.permalink}>
<Link to={data.permalink}>{data.permalink}</Link>
</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;
// static site generator webpack plugin
const docsLinks = Object.values(docsMetadatas).map(data => ({
path: `${data.permalink}`,
}));
const paths = [...docsLinks, ...pagesMetadatas].map(data => data.path);
const docsFlatMetadatas = Object.values(docsMetadatas);
const paths = [...docsFlatMetadatas, ...pagesMetadatas].map(
data => data.permalink,
);
config.plugin('siteGenerator').use(staticSiteGenerator, [
{
entry: 'main',

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,17 +1,16 @@
import React from 'react';
import Helmet from 'react-helmet';
import Layout from '@theme/Layout';
export default class Home extends React.Component {
render() {
return (
<Layout {...this.props}>
<div>
<Helmet>
<title>Home</title>
<link rel="stylesheet" type="text/css" href="/css/basic.css" />
</Helmet>
<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 path from 'path';
import loadSetup from '../loadSetup';
describe('loadPages', () => {
test('valid pages', async () => {
const pagesDir = path.join(__dirname, '__fixtures__', 'simple-pages');
const pagesMetadatas = await loadPages(pagesDir);
pagesMetadatas.sort((a, b) => a.path > b.path); // because it was unordered
test('simple website', async () => {
const {pagesDir, env, siteConfig} = await loadSetup('simple');
const pagesMetadatas = await loadPages({pagesDir, env, siteConfig});
expect(pagesMetadatas).toEqual([
{
path: '/',
source: 'index.js',
permalink: '/',
source: path.join(pagesDir, 'index.js'),
},
{
path: '/bar/baz',
source: 'bar/baz.js',
},
{
path: '/foo',
source: 'foo.js',
},
{
path: '/foo/',
source: 'foo/index.js',
permalink: '/hello/world',
source: path.join(pagesDir, 'hello', 'world.js'),
},
]);
});
test('versioned website', async () => {
const {pagesDir, env, siteConfig} = await loadSetup('versioned');
const pagesMetadatas = await loadPages({pagesDir, env, siteConfig});
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 () => {
const nonExistingDir = path.join(__dirname, '__fixtures__', 'nonExisting');
const pagesMetadatas = await loadPages(nonExistingDir);
const {env, siteConfig} = await loadSetup('simple');
const pagesDir = path.join(__dirname, '__fixtures__', 'nonExisting');
const pagesMetadatas = await loadPages({pagesDir, env, siteConfig});
expect(pagesMetadatas).toEqual([]);
});
});

View file

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

View file

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

View file

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