mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-10 15:47:23 +02:00
feat: custom pages (#994)
This commit is contained in:
parent
7d4d9fe961
commit
8691a2525c
25 changed files with 263 additions and 128 deletions
|
@ -83,42 +83,40 @@ module.exports = async function start(siteDir, cliOptions = {}) {
|
|||
const compiler = webpack(config);
|
||||
|
||||
// webpack-serve
|
||||
setTimeout(async () => {
|
||||
await serve(
|
||||
{},
|
||||
{
|
||||
compiler,
|
||||
open: true,
|
||||
devMiddleware: {
|
||||
logLevel: 'silent',
|
||||
},
|
||||
hotClient: {
|
||||
port: hotPort,
|
||||
logLevel: 'error',
|
||||
},
|
||||
logLevel: 'error',
|
||||
port,
|
||||
host,
|
||||
add: app => {
|
||||
// serve static files
|
||||
const staticDir = path.resolve(siteDir, 'static');
|
||||
if (fs.existsSync(staticDir)) {
|
||||
app.use(mount(baseUrl, serveStatic(staticDir)));
|
||||
}
|
||||
|
||||
// enable HTTP range requests
|
||||
app.use(range);
|
||||
|
||||
// rewrite request to `/` since dev is only a SPA
|
||||
app.use(
|
||||
convert(
|
||||
history({
|
||||
rewrites: [{from: /\.html$/, to: '/'}],
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
await serve(
|
||||
{},
|
||||
{
|
||||
compiler,
|
||||
open: true,
|
||||
devMiddleware: {
|
||||
logLevel: 'silent',
|
||||
},
|
||||
);
|
||||
}, 1000);
|
||||
hotClient: {
|
||||
port: hotPort,
|
||||
logLevel: 'error',
|
||||
},
|
||||
logLevel: 'error',
|
||||
port,
|
||||
host,
|
||||
add: app => {
|
||||
// serve static files
|
||||
const staticDir = path.resolve(siteDir, 'static');
|
||||
if (fs.existsSync(staticDir)) {
|
||||
app.use(mount(baseUrl, serveStatic(staticDir)));
|
||||
}
|
||||
|
||||
// enable HTTP range requests
|
||||
app.use(range);
|
||||
|
||||
// rewrite request to `/` since dev is only a SPA
|
||||
app.use(
|
||||
convert(
|
||||
history({
|
||||
rewrites: [{from: /\.html$/, to: '/'}],
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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)};`,
|
||||
|
|
|
@ -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)),
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
),
|
||||
);
|
||||
|
|
21
v2/lib/theme/Pages/index.js
Normal file
21
v2/lib/theme/Pages/index.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
export default () => <div>Baz</div>;
|
|
@ -1,3 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
export default () => <div>Foo</div>;
|
|
@ -1,3 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
export default () => <div>Foo in subfolder</div>;
|
|
@ -1,3 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
export default () => <div>Index</div>;
|
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue