Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add createRedirects action and Netlify support #2185

Merged
merged 7 commits into from
Sep 20, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions packages/gatsby-plugin-netlify-headers/.gitignore

This file was deleted.

3 changes: 3 additions & 0 deletions packages/gatsby-plugin-netlify/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/*.js
!index.js
yarn.lock
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
# gatsby-plugin-netlify-headers
# gatsby-plugin-netlify

Generates a `_headers` file at the root of the public folder, to configure [HTTP headers on netlify](https://www.netlify.com/docs/headers-and-basic-auth/). Notably, you can immediately enable HTTP/2 server push of critical Gatsby assets through the `Link` headers.

By default, the plugin will add HTTP/2 assets to server push the critical Gatsby scripts (ones that have the `preload` attribute already). It will also add some basic security headers. You can easily add or replace headers through the plugin config.

## Install

`npm install --save gatsby-plugin-netlify-headers`
`npm install --save gatsby-plugin-netlify`

## How to use

```javascript
// In your gatsby-config.js
plugins: [
` gatsby-plugin-netlify-headers`, // make sure to put last in the array
` gatsby-plugin-netlify`, // make sure to put last in the array
]
```

Expand All @@ -25,7 +25,7 @@ If you just need the critical assets, you don't need to add any additional confi
plugins: [
// make sure to put last in the array
{
resolve: ` gatsby-plugin-netlify-headers`,
resolve: ` gatsby-plugin-netlify`,
options: {
headers: {}, // option to add more headers. `Link` headers are transformed by the below criteria
allPageHeaders: [], // option to add headers for all pages. `Link` headers are transformed by the below criteria
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "gatsby-plugin-netlify-headers",
"name": "gatsby-plugin-netlify",
"version": "1.0.1",
"description": "A Gatsby plugin which generates a _headers file for netlify",
"main": "index.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ function transformLink(manifest, publicFolder, pathPrefix) {
} else {
throw new Error(
`Could not find the file specified in the Link header \`${header}\`.` +
`The gatsby-plugin-netlify-headers is looking for a matching file (with or without a ` +
`The gatsby-plugin-netlify is looking for a matching file (with or without a ` +
`webpack hash). Check the public folder and your gatsby-config.js to ensure you are ` +
`pointing to a public file.`
)
Expand Down Expand Up @@ -130,7 +130,7 @@ function stringifyHeaders(headers) {
const validateUserOptions = pluginOptions => headers => {
if (!validHeaders(headers)) {
throw new Error(
`The "headers" option to gatsby-plugin-netlify-headers is in the wrong shape. ` +
`The "headers" option to gatsby-plugin-netlify is in the wrong shape. ` +
`You should pass in a object with string keys (representing the paths) and an array ` +
`of strings as the value (representing the headers). ` +
`Check your gatsby-config.js.`
Expand All @@ -144,15 +144,15 @@ const validateUserOptions = pluginOptions => headers => {
].forEach(mergeOption => {
if (!_.isBoolean(pluginOptions[mergeOption])) {
throw new Error(
`The "${mergeOption}" option to gatsby-plugin-netlify-headers must be a boolean. ` +
`The "${mergeOption}" option to gatsby-plugin-netlify must be a boolean. ` +
`Check your gatsby-config.js.`
)
}
})

if (!_.isFunction(pluginOptions.transformHeaders)) {
throw new Error(
`The "transformHeaders" option to gatsby-plugin-netlify-headers must be a function ` +
`The "transformHeaders" option to gatsby-plugin-netlify must be a function ` +
`that returns a array of header strings.` +
`Check your gatsby-config.js.`
)
Expand Down Expand Up @@ -228,7 +228,7 @@ const applyTransfromHeaders = ({ transformHeaders }) => headers =>
_.mapValues(headers, transformHeaders)

const transformToString = headers =>
`## Created with gatsby-plugin-netlify-headers\n\n${stringifyHeaders(
`## Created with gatsby-plugin-netlify\n\n${stringifyHeaders(
headers
)}`

Expand Down
16 changes: 16 additions & 0 deletions packages/gatsby-plugin-netlify/src/create-redirects.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import fs from "fs"
import pify from "pify"

const writeFile = pify(fs.writeFile)

export default function writeRedirectsFile(pluginData, redirects) {
const { publicFolder } = pluginData

// https://www.netlify.com/docs/redirects/
const data = redirects.map(redirect => {
const status = redirect.isPermanent ? 301 : 302
return `${redirect.fromPath} ${redirect.toPath} ${status}`
})

return writeFile(publicFolder(`_redirects`), data.join(`\n`))
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could add a plugin option to disable this as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any harm in writing an empty _redirects file? Or are you concerned that we might override an existing one? (Guess it would be worth checking to see if we should append vs write.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right—someone might want to make one by hand through the static folder rather than go through generation in this plugin.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll create a follow-up PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in PR #2191

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import WebpackAssetsManifest from "webpack-assets-manifest"

import makePluginData from "./plugin-data"
import buildHeadersProgram from "./build-headers-program"
import createRedirects from "./create-redirects"
import { DEFAULT_OPTIONS, BUILD_HTML_STAGE, BUILD_CSS_STAGE } from "./constants"

let assetsManifest = {}
Expand Down Expand Up @@ -32,5 +33,8 @@ exports.onPostBuild = async ({ store, pathPrefix }, userPluginOptions) => {
const pluginData = makePluginData(store, assetsManifest, pathPrefix)
const pluginOptions = { ...DEFAULT_OPTIONS, ...userPluginOptions }

return buildHeadersProgram(pluginData, pluginOptions)
const { redirects } = store.getState()

await buildHeadersProgram(pluginData, pluginOptions)
await createRedirects(pluginData, redirects)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:neckbeard: Maybe this could be a promise.all, so they can run in parallel instead of series?

}
36 changes: 32 additions & 4 deletions packages/gatsby/cache-dir/root.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,22 @@ import history from "./dev-history"
import { apiRunner } from "./api-runner-browser"
import syncRequires from "./sync-requires"
import pages from "./pages.json"
import redirects from "./redirects.json"
import ComponentRenderer from "./component-renderer"
import loader from "./loader"
loader.addPagesArray(pages)
loader.addDevRequires(syncRequires)
window.___loader = loader

// Convert to a map for faster lookup in maybeRedirect()
const redirectMap = redirects.reduce((map, redirect) => {
map[redirect.fromPath] = redirect
return map
}, {})

// Check for initial page-load redirect
maybeRedirect(location.pathname);

// Call onRouteUpdate on the initial page load.
apiRunner(`onRouteUpdate`, {
location: history.location,
Expand All @@ -22,11 +32,30 @@ function attachToHistory(history) {
window.___history = history

history.listen((location, action) => {
apiRunner(`onRouteUpdate`, { location, action })
if (!maybeRedirect(location.pathname)) {
apiRunner(`onRouteUpdate`, { location, action })
}
})
}
}

function maybeRedirect(pathname) {
const redirect = redirectMap[pathname]

if (redirect != null) {
const pageResources = loader.getResourcesForPathname(pathname)

if (pageResources != null) {
console.error(`The route "${pathname}" matches both a page and a redirect; this is probably not intentional.`)
}

history.replace(redirect.toPath)
return true;
} else {
return false;
}
}

function shouldUpdateScroll(prevRouterProps, { location: { pathname } }) {
const results = apiRunner(`shouldUpdateScroll`, {
prevRouterProps,
Expand Down Expand Up @@ -98,9 +127,8 @@ const Root = () =>
render: routeProps => {
const props = layoutProps ? layoutProps : routeProps
attachToHistory(props.history)
const pageResources = loader.getResourcesForPathname(
props.location.pathname
)
const {pathname} = props.location
const pageResources = loader.getResourcesForPathname(pathname)
if (pageResources) {
return createElement(ComponentRenderer, {
page: true,
Expand Down
9 changes: 9 additions & 0 deletions packages/gatsby/src/bootstrap/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ const {
runQueries,
} = require(`../internal-plugins/query-runner/page-query-runner`)
const { writePages } = require(`../internal-plugins/query-runner/pages-writer`)
const {
writeRedirects,
} = require(`../internal-plugins/query-runner/redirects-writer`)

// Override console.log to add the source file + line number.
// Useful for debugging if you lose a console.log somewhere.
Expand Down Expand Up @@ -318,6 +321,12 @@ module.exports = async (program: any) => {
await writePages()
activity.end()

// Write out redirects.
activity = report.activityTimer(`write out redirect data`)
activity.start()
await writeRedirects()
activity.end()

// Update Schema for SitePage.
activity = report.activityTimer(`update schema`)
activity.start()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import _ from "lodash"
import fs from "fs-extra"
import { store, emitter } from "../../redux/"
import { joinPath } from "../../utils/path"

const writeRedirects = async () => {
bootstrapFinished = true

let { program, redirects } = store.getState()

await fs.writeFile(
joinPath(program.directory, `.cache/redirects.json`),
JSON.stringify(redirects, null, 2)
)
}

exports.writeRedirects = writeRedirects

let bootstrapFinished = false
let oldRedirects
const debouncedWriteRedirects = _.debounce(() => {
// Don't write redirects again until bootstrap has finished.
if (
bootstrapFinished &&
!_.isEqual(oldRedirects, store.getState().redirects)
) {
writeRedirects()
oldRedirects = store.getState().Redirects
}
}, 250)

emitter.on(`CREATE_REDIRECT`, () => {
debouncedWriteRedirects()
})
22 changes: 22 additions & 0 deletions packages/gatsby/src/redux/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -606,5 +606,27 @@ actions.setPluginStatus = (status, plugin) => {
}
}

/**
* Create a redirect from one page to another.
* Redirect data can be used to configure environments like Netlify.
*
* @param {Object} redirect Redirect data
* @param {string} redirect.fromPath Any valid URL. Must start with a forward slash
* @param {string} redirect.isPermanent This is a permanent redirect; defaults to temporary
* @param {Object} redirect.toPath URL of a created page (see `createPage`)
* @example
* createRedirect({ fromPath: '/old-url', toPath: '/new-url', isPermanent: true })
*/
actions.createRedirect = ({ fromPath, isPermanent = false, toPath }) => {
return {
type: `CREATE_REDIRECT`,
payload: {
fromPath,
isPermanent,
toPath,
},
}
}

exports.actions = actions
exports.boundActionCreators = bindActionCreators(actions, store.dispatch)
1 change: 1 addition & 0 deletions packages/gatsby/src/redux/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ module.exports = {
componentDataDependencies: require(`./component-data-dependencies`),
components: require(`./components`),
jobs: require(`./jobs`),
redirects: require(`./redirects`),
}
8 changes: 8 additions & 0 deletions packages/gatsby/src/redux/reducers/redirects.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = (state = [], action) => {
switch (action.type) {
case `CREATE_REDIRECT`:
return [...state, action.payload]
default:
return state
}
}
2 changes: 1 addition & 1 deletion www/gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,6 @@ module.exports = {
],
},
},
`gatsby-plugin-netlify-headers`,
`gatsby-plugin-netlify`,
],
}
2 changes: 1 addition & 1 deletion www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"gatsby-plugin-google-analytics": "^1.0.7",
"gatsby-plugin-lodash": "^1.0.0",
"gatsby-plugin-manifest": "^1.0.7",
"gatsby-plugin-netlify-headers": "^1.0.0",
"gatsby-plugin-netlify": "^1.0.0",
"gatsby-plugin-nprogress": "^1.0.7",
"gatsby-plugin-offline": "^1.0.9",
"gatsby-plugin-react-helmet": "^1.0.6",
Expand Down