Skip to content

Commit 188eb5c

Browse files
Refactor HTML for apps
1 parent e50f50b commit 188eb5c

File tree

11 files changed

+272
-86
lines changed

11 files changed

+272
-86
lines changed

.eslintrc

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
},
1515
"parser": "babel-eslint",
1616
"rules": {
17-
"object-curly-spacing": 0,
18-
"space-before-function-paren": [2, "never"],
19-
"react/prefer-stateless-function": 0,
17+
"arrow-body-style": "off",
18+
"object-curly-spacing": "off",
19+
"space-before-function-paren": ["error", "never"],
20+
"react/prefer-stateless-function": "off",
2021
"react/jsx-indent-props": "off",
2122
"react/jsx-closing-bracket-location": "off"
2223
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@
139139
"piping": "0.3.1",
140140
"react": "15.0.2",
141141
"react-cookie": "0.4.5",
142+
"react-helmet": "3.1.0",
142143
"react-redux": "4.4.5",
143144
"react-router": "2.4.0",
144145
"redux": "3.5.2",

src/core/containers/ServerHtml.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import React, { Component, PropTypes } from 'react';
2+
import ReactDOM from 'react-dom/server';
3+
import serialize from 'serialize-javascript';
4+
import Helmet from 'react-helmet';
5+
6+
7+
export default class ServerHtml extends Component {
8+
9+
static propTypes = {
10+
appName: PropTypes.string.isRequired,
11+
assets: PropTypes.object.isRequired,
12+
component: PropTypes.object.isRequired,
13+
env: PropTypes.string.isRequired,
14+
sriData: PropTypes.object,
15+
store: PropTypes.object.isRequired,
16+
};
17+
18+
getStatic({filePath, type} = {}) {
19+
const { env, sriData, appName } = this.props;
20+
const leafName = filePath.split('/').pop();
21+
const isDeployed = ['stage', 'dev', 'production'].indexOf(env) > -1;
22+
let sriProps = {};
23+
// Only output files for the current app.
24+
if (leafName.startsWith(appName)) {
25+
if (isDeployed) {
26+
sriProps = {
27+
integrity: sriData[leafName],
28+
crossOrigin: 'anonymous',
29+
};
30+
if (!sriProps.integrity) {
31+
throw new Error(`SRI Data is missing for ${leafName}`);
32+
}
33+
}
34+
switch (type) {
35+
case 'css':
36+
return (<link href={filePath} {...sriProps}
37+
rel="stylesheet" type="text/css" />);
38+
case 'js':
39+
return <script src={filePath} {...sriProps}></script>;
40+
default:
41+
throw new Error('Unknown static type');
42+
}
43+
} else {
44+
return null;
45+
}
46+
}
47+
48+
getStyle() {
49+
const { assets } = this.props;
50+
return Object.keys(assets.styles).map((style) =>
51+
this.getStatic({filePath: assets.styles[style], type: 'css'}));
52+
}
53+
54+
getScript() {
55+
const { assets } = this.props;
56+
return Object.keys(assets.javascript).map((js) =>
57+
this.getStatic({filePath: assets.javascript[js], type: 'js'}));
58+
}
59+
60+
render() {
61+
const { component, store } = this.props;
62+
const content = component ? ReactDOM.renderToString(component) : '';
63+
const head = Helmet.rewind();
64+
const htmlAttrs = head.htmlAttributes.toComponent();
65+
66+
return (
67+
<html {...htmlAttrs}>
68+
<head>
69+
<meta charSet="utf-8" />
70+
<meta name="viewport" content="width=device-width, initial-scale=1" />
71+
<link rel="shortcut icon" href="/favicon.ico" />
72+
{head.title.toComponent()}
73+
{head.meta.toComponent()}
74+
{this.getStyle()}
75+
</head>
76+
<body>
77+
<div id="react-view" dangerouslySetInnerHTML={{__html: content}} />
78+
<script dangerouslySetInnerHTML={{__html: serialize(store.getState())}}
79+
type="application/json" id="redux-store-state" />
80+
{this.getScript()}
81+
</body>
82+
</html>
83+
);
84+
}
85+
}

src/core/server/base.js

Lines changed: 41 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@ import fs from 'fs';
22
import Express from 'express';
33
import helmet from 'helmet';
44
import path from 'path';
5-
import serialize from 'serialize-javascript';
65
import cookie from 'react-cookie';
76
import React from 'react';
7+
import ReactDOM from 'react-dom/server';
8+
import ReactHelmet from 'react-helmet';
89

9-
import { stripIndent } from 'common-tags';
10-
import { renderToString } from 'react-dom/server';
1110
import { Provider } from 'react-redux';
1211
import { match } from 'react-router';
1312
import { ReduxAsyncConnect, loadOnServer } from 'redux-async-connect';
1413

1514
import WebpackIsomorphicTools from 'webpack-isomorphic-tools';
1615
import WebpackIsomorphicToolsConfig from 'webpack-isomorphic-tools-config';
16+
import ServerHtml from 'core/containers/ServerHtml';
1717

1818
import config from 'config';
1919
import { setJWT } from 'core/actions';
@@ -29,7 +29,7 @@ global.CLIENT = false;
2929
global.SERVER = true;
3030
global.DEVELOPMENT = env === 'development';
3131

32-
export default function(routes, createStore) {
32+
function baseServer(routes, createStore, { appInstanceName = appName } = {}) {
3333
const app = new Express();
3434
app.disable('x-powered-by');
3535

@@ -58,7 +58,7 @@ export default function(routes, createStore) {
5858
app.post('/__cspreport__', (req, res) => res.status(200).end('ok'));
5959

6060
// Redirect from / for the search app it's a 302 to prevent caching.
61-
if (appName === 'search') {
61+
if (appInstanceName === 'search') {
6262
app.get('/', (req, res) => res.redirect(302, '/search'));
6363
}
6464

@@ -82,67 +82,37 @@ export default function(routes, createStore) {
8282
store.dispatch(setJWT(token));
8383
}
8484

85-
return loadOnServer({...renderProps, store}).then(() => {
86-
const InitialComponent = (
87-
<Provider store={store} key="provider">
88-
<ReduxAsyncConnect {...renderProps} />
89-
</Provider>
90-
);
91-
92-
const componentHTML = renderToString(InitialComponent);
93-
94-
const assets = webpackIsomorphicTools.assets();
95-
96-
// Get SRI for deployed services only.
97-
const sri = isDeployed ? JSON.parse(
98-
fs.readFileSync(path.join(config.get('basePath'), 'dist/sri.json'))
99-
) : {};
100-
101-
const styles = Object.keys(assets.styles).map((style) => {
102-
const cssHash = sri[path.basename(assets.styles[style])];
103-
if (isDeployed && !cssHash) {
104-
throw new Error('Missing SRI Data');
105-
}
106-
const cssSRI = sri && cssHash ? ` integrity="${cssHash}" crossorigin="anonymous"` : '';
107-
return `<link href="${assets.styles[style]}"${cssSRI}
108-
rel="stylesheet" type="text/css" />`;
109-
}).join('\n');
110-
111-
const script = Object.keys(assets.javascript).map((js) => {
112-
const jsHash = sri[path.basename(assets.javascript[js])];
113-
if (isDeployed && !jsHash) {
114-
throw new Error('Missing SRI Data');
115-
}
116-
const jsSRI = sri && jsHash ? ` integrity="${jsHash}" crossorigin="anonymous"` : '';
117-
return `<script src="${assets.javascript[js]}"${jsSRI}></script>`;
118-
}).join('\n');
119-
120-
const HTML = stripIndent`
121-
<!DOCTYPE html>
122-
<html>
123-
<head>
124-
<meta charset="utf-8">
125-
<title>Isomorphic Redux Demo</title>
126-
<meta name="viewport" content="width=device-width, initial-scale=1" />
127-
${styles}
128-
</head>
129-
<body>
130-
<div id="react-view">${componentHTML}</div>
131-
<script type="application/json" id="redux-store-state">
132-
${serialize(store.getState())}
133-
</script>
134-
${script}
135-
</body>
136-
</html>`;
137-
138-
res.header('Content-Type', 'text/html');
139-
return res.end(HTML);
140-
})
141-
.catch((error) => {
142-
// eslint-disable-next-line no-console
143-
console.error(error.stack);
144-
res.status(500).end(errorString);
145-
});
85+
return loadOnServer({...renderProps, store})
86+
.then(() => {
87+
const InitialComponent = (
88+
<Provider store={store} key="provider">
89+
<ReduxAsyncConnect {...renderProps} />
90+
</Provider>
91+
);
92+
93+
// Get SRI for deployed services only.
94+
const sriData = (isDeployed) ? JSON.parse(
95+
fs.readFileSync(path.join(config.get('basePath'), 'dist/sri.json'))
96+
) : {};
97+
98+
const pageProps = {
99+
appName: appInstanceName,
100+
assets: webpackIsomorphicTools.assets(),
101+
component: InitialComponent,
102+
head: ReactHelmet.rewind(),
103+
env,
104+
sriData,
105+
store,
106+
};
107+
108+
const HTML = ReactDOM.renderToString(<ServerHtml {...pageProps} />);
109+
res.send(`<!DOCTYPE html>${HTML}`);
110+
})
111+
.catch((error) => {
112+
// eslint-disable-next-line no-console
113+
console.error(error.stack);
114+
res.status(500).end(errorString);
115+
});
146116
});
147117
});
148118

@@ -174,7 +144,9 @@ export function runServer({listen = true, app = appName} = {}) {
174144
// Webpack Isomorphic tools is ready
175145
// now fire up the actual server.
176146
return new Promise((resolve, reject) => {
177-
const server = require(`${app}/server`).default;
147+
const routes = require(`${app}/routes`).default;
148+
const createStore = require(`${app}/store`).default;
149+
const server = baseServer(routes, createStore, {appInstanceName: app});
178150
if (listen === true) {
179151
server.listen(port, host, (err) => {
180152
if (err) {
@@ -196,3 +168,5 @@ export function runServer({listen = true, app = appName} = {}) {
196168
console.error(err);
197169
});
198170
}
171+
172+
export default baseServer;

src/disco/containers/App.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import React, { PropTypes } from 'react';
2+
import Helmet from 'react-helmet';
23

34
import 'disco/css/App.scss';
5+
import { gettext as _ } from 'core/utils';
46

57

68
export default class App extends React.Component {
@@ -12,6 +14,9 @@ export default class App extends React.Component {
1214
const { children } = this.props;
1315
return (
1416
<div className="disco-pane">
17+
<Helmet
18+
defaultTitle={_('Discover Add-ons')}
19+
/>
1520
{children}
1621
</div>
1722
);

src/disco/server.js

Lines changed: 0 additions & 7 deletions
This file was deleted.

src/search/containers/App.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import React, { PropTypes } from 'react';
2+
import Helmet from 'react-helmet';
23

34
import 'search/css/App.scss';
5+
import { gettext as _ } from 'core/utils';
46

57

68
export default class App extends React.Component {
@@ -12,6 +14,9 @@ export default class App extends React.Component {
1214
const { children } = this.props;
1315
return (
1416
<div className="search-page">
17+
<Helmet
18+
defaultTitle={_('Add-ons Search')}
19+
/>
1520
{children}
1621
</div>
1722
);

src/search/server.js

Lines changed: 0 additions & 7 deletions
This file was deleted.

0 commit comments

Comments
 (0)