Skip to content

Commit

Permalink
Merge pull request #18 from zarathustra323/branding
Browse files Browse the repository at this point in the history
Robots.txt + sitemap.xml; stitch graph schema; publisher context
  • Loading branch information
zarathustra323 authored Aug 13, 2018
2 parents 09bb4c6 + e1a7899 commit 6a474e1
Show file tree
Hide file tree
Showing 20 changed files with 782 additions and 70 deletions.
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ services:
environment:
NODE_ENV: development
PORT: ${APP_PORT-3005}
ROOT_URI: ${ROOT_URI-http://docker.for.mac.host.internal:8100}
GRAPHQL_URI: ${GRAPHQL_URI-http://docker.for.mac.host.internal:8100}
volumes:
node_modules: {}
next:
Expand Down
2 changes: 1 addition & 1 deletion nodemon.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"verbose": true,
"ignore": ["node_modules", ".next"],
"watch": ["src/server/**/*", "server.js"],
"watch": ["src/server/**/*.js", "server.js"],
"ext": "js json"
}
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@
"apollo-link": "^1.2.2",
"apollo-link-error": "^1.1.0",
"apollo-link-http": "^1.5.4",
"apollo-server-express": "^2.0.0",
"dotenv": "^5.0.1",
"envalid": "^4.1.4",
"express": "^4.16.3",
"graphql": "^0.13.2",
"graphql-tag": "^2.9.2",
"graphql-tools": "^3.1.1",
"handlebars": "^4.0.11",
"helmet": "^3.13.0",
"http-proxy-middleware": "^0.18.0",
"isomorphic-unfetch": "^2.1.1",
"moment": "^2.19.1",
"next": "6.1.1-canary.4",
Expand All @@ -40,7 +43,8 @@
"react-apollo": "^2.1.9",
"react-dom": "^16.4.2",
"react-moment": "^0.7.9",
"reactstrap": "^6.3.1"
"reactstrap": "^6.3.1",
"validator": "^10.5.0"
},
"devDependencies": {
"eslint": "^4.19.1",
Expand Down
4 changes: 2 additions & 2 deletions src/apollo/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import fetch from 'isomorphic-unfetch';

export default (req) => {
const headers = req ? req.headers : {};
const host = (req) ? req.ROOT_URI : '';
const uri = (req) ? `${req.protocol}://${req.get('host')}` : '';
if (headers.host) {
headers['x-forwarded-host'] = headers.host;
delete headers.host;
Expand All @@ -21,7 +21,7 @@ export default (req) => {
if (networkError) console.error(`[Network error]: ${networkError}`);
}),
new HttpLink({
uri: `${host}/graph`,
uri: `${uri}/graph`,
headers,
fetch,
}),
Expand Down
1 change: 1 addition & 0 deletions src/gql/fragments/story/view.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ fragment StoryViewFragment on Story {
id
title
teaser
url
publishedAt
advertiser {
id
Expand Down
6 changes: 5 additions & 1 deletion src/gql/queries/pages/story.graphql
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
#import '../../fragments/story/view.graphql'

query StoryPage($input: PublishedStoryInput!) {
query StoryPage($input: PublishedStoryInput!, $publisherId: String) {
publishedStory(input: $input) {
...StoryViewFragment
publisher(contextId: $publisherId) {
id
name
}
}
}
22 changes: 17 additions & 5 deletions src/pages/story.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Query } from 'react-apollo';
import Head from 'next/head';

import Title from '../components/Title';
import TrackPageView from '../components/TrackPageView';
import StoryView from '../components/StoryView';
Expand All @@ -10,11 +11,11 @@ import ErrorAlert from '../components/ErrorAlert';

import pageQuery from '../gql/queries/pages/story.graphql';

const Story = ({ id, preview }) => {
const Story = ({ id, preview, publisherId }) => {
const input = { id, preview };
return (
<Fragment>
<Query query={pageQuery} variables={{ input }}>
<Query query={pageQuery} variables={{ input, publisherId }}>
{({ loading, error, data }) => {
if (loading) {
return <LoadingBar />;
Expand All @@ -23,13 +24,21 @@ const Story = ({ id, preview }) => {
return <ErrorAlert message={error.message} />;
}
const { publishedStory } = data;
const { title, teaser } = publishedStory;
const {
title,
teaser,
url,
publisher,
} = publishedStory;
return (
<Fragment>
<TrackPageView params={{ story_id: id, page_title: title }} />
<Title value={title} />
<Head>
<meta name="description" content={teaser} />
{/* @todo Eventually use the publisher context. */}
<meta name="native-x:publisher" content={publisher.name} />
<link rel="canonical" href={url} />
</Head>
<StoryView {...publishedStory} />
</Fragment>
Expand All @@ -42,19 +51,22 @@ const Story = ({ id, preview }) => {

Story.getInitialProps = async ({ req, query }) => {
let preview = false;

if (req) {
preview = Boolean(req.query);
}
const { id } = query;
return { id, preview };
const { id, publisherId } = query;
return { id, preview, publisherId };
};

Story.defaultProps = {
preview: false,
publisherId: null,
};

Story.propTypes = {
id: PropTypes.string.isRequired,
publisherId: PropTypes.string,
preview: PropTypes.bool,
};

Expand Down
18 changes: 18 additions & 0 deletions src/server/env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const { isURL } = require('validator');

const {
port,
cleanEnv,
makeValidator,
} = require('envalid');

const url = makeValidator((v) => {
const opts = { protocols: ['http', 'https'], require_tld: false, require_protocol: true };
if (isURL(v, opts)) return v;
throw new Error('Expected a valid URL with http or https');
});

module.exports = cleanEnv(process.env, {
PORT: port({ desc: 'The port that express will run on.', default: 3005 }),
GRAPHQL_URI: url({ desc: 'The GraphQL URI for proxying/stitching API requests. Should follow the https://[account_key].[domain].[tld] structure in production.' }),
});
7 changes: 7 additions & 0 deletions src/server/graph/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const { graphql } = require('graphql');
const createSchema = require('./schema');

module.exports = async (options = {}, resultKey) => {
const schema = await createSchema();
return graphql({ ...options, schema }).then(res => (resultKey ? res.data[resultKey] : res.data));
};
32 changes: 32 additions & 0 deletions src/server/graph/schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const { HttpLink } = require('apollo-link-http');
const fetch = require('isomorphic-unfetch');
const {
makeRemoteExecutableSchema,
introspectSchema,
} = require('graphql-tools');
const env = require('../env');

const { GRAPHQL_URI } = env;

const link = new HttpLink({
uri: `${GRAPHQL_URI}/graph`,
fetch,
});

let promise;
module.exports = async () => {
const build = async () => {
const schema = await introspectSchema(link);
return makeRemoteExecutableSchema({
schema,
link,
});
};
if (!promise) {
// Prevents the introspection from happening more than once.
// What happens, though, when the remote schema updates?
// This would cache the schema and would require a reload of the app.
promise = build();
}
return promise;
};
12 changes: 12 additions & 0 deletions src/server/handlebars.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const handlebars = require('handlebars');
const { readFileSync } = require('fs');

// Register custom helpers, etc here...

const renderTemplate = (path, data) => {
const source = readFileSync(`${__dirname}/templates/${path}`).toString();
const template = handlebars.compile(source);
return template(data);
};

module.exports = { handlebars, renderTemplate };
9 changes: 2 additions & 7 deletions src/server/index.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
const env = require('./env');
const express = require('express');
const helmet = require('helmet');
const routes = require('./routes');

const { PORT, ROOT_URI } = process.env;

const applyProxy = (req, res, next) => {
req.ROOT_URI = ROOT_URI;
next();
};
const { PORT } = env;

const server = express();
server.use(applyProxy)
server.use(helmet());

module.exports = (client) => {
Expand Down
17 changes: 17 additions & 0 deletions src/server/routes/graph.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const helmet = require('helmet');
const { Router } = require('express');
const { ApolloServer } = require('apollo-server-express');
const createSchema = require('../graph/schema');

const router = Router();

const create = async () => {
const schema = await createSchema();
router.use(helmet.noCache());
const server = new ApolloServer({ schema });
server.applyMiddleware({ app: router, path: '/' });
};

create();

module.exports = router;
9 changes: 2 additions & 7 deletions src/server/routes/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
const proxy = require('http-proxy-middleware');
const index = require('./root');
const graph = require('./graph');
const story = require('./story');

const { ROOT_URI } = process.env;

module.exports = (server, client) => {
server.use('/', index(client));
server.use('/story', story(client));
server.use('/graph', proxy({
target: `${ROOT_URI}/graph`,
changeOrigin: true,
}));
server.use('/graph', graph);
};
43 changes: 43 additions & 0 deletions src/server/routes/root.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
const path = require('path');
const { Router } = require('express');
const { renderTemplate } = require('../handlebars');
const asyncRoute = require('../utils/async-route');
const graphql = require('../graph/client');

const router = Router();

Expand All @@ -8,5 +11,45 @@ module.exports = (client) => {
const file = path.resolve(__dirname, '../../static/favicon.ico');
client.serveStatic(req, res, file);
});

router.get('/robots.txt', asyncRoute(async (req, res) => {
const source = `
query Account {
account {
id
storyUri
}
}
`;
const account = await graphql({ source }, 'account');
const { storyUri } = account;
const txt = renderTemplate('robots.txt.hbs', { uri: storyUri });
res.set('Content-Type', 'text/plain; charset=UTF-8');
res.send(txt);
}));

router.get('/sitemap.xml', asyncRoute(async (req, res) => {
const source = `
query Sitemap {
storySitemap {
id
loc
lastmod
changefreq
priority
image {
id
loc
caption
}
}
}
`;
const items = await graphql({ source }, 'storySitemap');
const xml = renderTemplate('sitemap.xml.hbs', { items });
res.set('Content-Type', 'text/xml; charset=UTF-8');
res.send(xml);
}));

return router;
};
3 changes: 2 additions & 1 deletion src/server/routes/story.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ module.exports = (client) => {
client.render(req, res, '_error');
} else {
const actualPage = '/story';
const props = { id };
const publisherId = req.query.pubid || null;
const props = { id, publisherId };
client.render(req, res, actualPage, props);
}
});
Expand Down
7 changes: 7 additions & 0 deletions src/server/templates/robots.txt.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# www.robotstxt.org/
# www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449

User-agent: *
Disallow: /graph

Sitemap: {{uri}}/sitemap.xml
20 changes: 20 additions & 0 deletions src/server/templates/sitemap.xml.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
{{#each items as |item|}}
<url>
<loc>{{item.loc}}</loc>
{{#if item.lastmod}}<lastmod>{{item.lastmod}}</lastmod>{{/if}}
<changefreq>{{item.changefreq}}</changefreq>
<priority>{{item.priority}}</priority>

{{#if item.image.loc}}
<image:image>
<image:loc>{{item.image.loc}}
{{#if item.image.caption}}<image:caption>{{item.image.caption}}</image:caption>{{/if}}
</image:loc>
</image:image>
{{/if}}

</url>
{{/each}}
</urlset>
1 change: 1 addition & 0 deletions src/server/utils/async-route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
Loading

0 comments on commit 6a474e1

Please sign in to comment.