Skip to content

Commit 8291044

Browse files
authored
feat(gatsby): add assetPrefix to support deploying assets separate from html (#12128)
* chore: add validation schema, start tweaking webpack config * chore: keep iterating * chore: yadda * chore: keep working * keep doing stuff * chore: get mostly done (let's see!) * chore: remove unused package * chore: ensure url is normalized correctly * chore: try try again * chore: fix for base path * test: tests are important; fix them * chore: remove a silly change * chore: fix linter Note: this should've been fine * fix(gatsby-plugin-offline): hard fail if assetPrefix is used Note: very possible this may be reverted * refactor: add a publicPath helper * test: add some get public path tests * chore: use correct name * docs: add asset prefix doc, and tweak path prefix * chore: allow relative url for assetPrefix * test: add a few more unit tests * test: clean up test * chore: fix e2e-test Note: this is a bug, will fix the underlying bug too. pathPrefix should have no effect unless using --prefix-paths * fix: fall back to empty string, not slash * Update docs/docs/asset-prefix.md * fix: handle relative paths * feat: add withAssetPrefix helper for gatsby-link This should rarely be used--but should be exposed * fix: use withAssetPrefix (if available) for gatsby-plugin-manifest * Allow using gatsby-plugin-offline with assetPrefix * Add docs for using offline-plugin with asset prefix * clarify docs * feat(*): use withAssetPrefix helper from gatsby-link BREAKING CHANGE: this is a breaking change (as currently authored) for a few plugins (specified in this commit). I'll work on a fallback--but I think it might make sense to just fail here. We can specify a peerDependency in the package.json of each of these packages, too. * test: get tests passing * test: add a test for assetPrefix with nesting * Update docs/docs/path-prefix.md Co-Authored-By: DSchau <[email protected]> * chore: fix up merge conflicts/get tests passing * chore: tweak version * fix(gatsby-plugin-sitemap): work with asset prefix * fix(gatsby): disallow both relative assetPrefix and pathPrefix * chore: fallback to withPathPrefix, bump peerDep * chore: remove caveat re: trailing slash * fix: gatsby-plugin-sitemap regression * chore: revert peer dep * chore: use basePath if it's defined * chore: remove eslint global comment * chore: ensure prefixPaths is set to enable pathPrefix * chore: fix read-only error (can't reassign imports ya dingus) * chore: actually fallback * Update docs/docs/asset-prefix.md Co-Authored-By: DSchau <[email protected]> * Update docs/docs/path-prefix.md Co-Authored-By: DSchau <[email protected]> * Update docs/docs/asset-prefix.md Co-Authored-By: DSchau <[email protected]> * chore: simply/merely remove the easy term ;) * Update docs/docs/asset-prefix.md Co-Authored-By: DSchau <[email protected]> * test: write e2e test for asset prefix Note: this very well may fail * chore: fix package json and make isURL test stricter * chore: fix yarn and stuff hopefully * chore: minor clean up * fix(gatsby): fix initial navigation not registering in history * chore: remove unneccessary dep * fix: use __BASE_PATH__ in development runtime too; add a test * chore: fix @pieh nit before he finds it
1 parent 6eee488 commit 8291044

File tree

49 files changed

+694
-124
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+694
-124
lines changed

.eslintrc.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
"globals": {
2727
"before": true,
2828
"spyOn": true,
29-
"__PATH_PREFIX__": true
29+
"__PATH_PREFIX__": true,
30+
"__BASE_PATH__": true,
31+
"__ASSET_PREFIX__": true
3032
},
3133
"rules": {
3234
"arrow-body-style": [

docs/blog/2019-02-08-government-open-data-site-with-gatsby/index.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,7 @@ We set a `BASEURL` environment variable in `gatsby-config.js` that resolves the
223223
const BASEURL = process.env.BASEURL || ""
224224

225225
module.exports = {
226-
// Note: it must *not* have a trailing slash.
227-
// This is currently the realtive path in our Jekyll deployment. This path points to our Gatsby pages.
226+
// This is currently the relative path in our Jekyll deployment. This path points to our Gatsby pages.
228227
// This prefix is prepended to load all our related images, code, and pages.
229228
pathPrefix: `${BASEURL}/gatsby-public`,
230229
}

docs/docs/asset-prefix.md

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
---
2+
title: Adding an Asset Prefix
3+
---
4+
5+
Gatsby produces static content that can be hosted _anywhere_ at scale in a cost-effective manner. This static content is comprised of HTML files, JavaScript, CSS, images, and more that power your great Gatsby application.
6+
7+
In some circumstances you may want to deploy _assets_ (non-HTML resources such as JavaScript, CSS, etc.) to a separate domain. Typically this is when you're required to use a dedicated CDN for assets or need to follow company-specific hosting policies.
8+
9+
This `assetPrefix` functionality is available starting in [email protected], so that you can seamlessly use Gatsby with assets hosted from a separate domain. To use this functionality, ensure that your version of `gatsby` specified in `package.json` is at least `2.4.0`.
10+
11+
## Usage
12+
13+
### Adding to `gatsby-config.js`
14+
15+
```js:title=gatsby-config.js
16+
module.exports = {
17+
assetPrefix: `https://cdn.example.com`,
18+
}
19+
```
20+
21+
One more step - when we build out this application, we need to add a flag so that Gatsby picks up this option.
22+
23+
### The `--prefix-paths` flag
24+
25+
When building with an `assetPrefix`, we require a `--prefix-paths` flag. If this flag is not specified, the build will ignore this option, and build out content as if it was hosted on the same domain. To ensure we build out successfully, use the following command:
26+
27+
```shell
28+
gatsby build --prefix-paths
29+
```
30+
31+
That's it! We now have an application that is ready to have its assets deployed from a CDN and its core files (e.g. HTML files) can be hosted on a separate domain.
32+
33+
## Building / Deploying
34+
35+
Once your application is built out, all assets will be automatically prefixed by this asset prefix. For example, if we have a JavaScript file `app-common-1234.js`, the script tag will look something like:
36+
37+
```html
38+
<script src="https://cdn.example.com/app-common-1234.js"></script>
39+
```
40+
41+
However - if we were to deploy our application as-is, those assets would not be available! We can do this in a few ways, but the general approach will be to deploy the contents of the `public` folder to _both_ your core domain, and the CDN/asset prefix location.
42+
43+
### Using `onPostBuild`
44+
45+
We expose an [`onPostBuild`](/docs/node-apis/#onPostBuild) API hook. This can be used to deploy your content to the CDN, like so:
46+
47+
```js:title=gatsby-node.js
48+
const assetsDirectory = `public`
49+
50+
exports.onPostBuild = async function onPostBuild() {
51+
// do something with public
52+
// e.g. upload to S3
53+
}
54+
```
55+
56+
### Using `package.json` scripts
57+
58+
Additionally, we can use an npm script, which will let us use some command line interfaces/executables to perform some action, in this case, deploying our assets directory!
59+
60+
In this example, I'll use the `aws-cli` and `s3` to sync the `public` folder (containing all our assets) to the `s3` bucket.
61+
62+
```json:title=package.json
63+
{
64+
"scripts": {
65+
"build": "gatsby build --prefix-paths",
66+
"postbuild": "aws s3 sync public s3://mybucket"
67+
}
68+
}
69+
```
70+
71+
Now whenever the `build` script is invoked, e.g. `npm run build`, the `postbuild` script will be invoked _after_ the build completes, therefore making our assets available on a _separate_ domain after we have finished building out our application with prefixed assets.
72+
73+
## Additional Considerations
74+
75+
### Usage with `pathPrefix`
76+
77+
The [`pathPrefix`](/docs/path-prefix/) feature can be thought of as semi-related to this feature. That feature allows _all_ your website content to be prefixed with some constant prefix, for example you may want your blog to be hosted from `/blog` rather than the project root.
78+
79+
This feature works seamlessly with `pathPrefix`. Build out your application with the `--prefix-paths` flag and you'll be well on your way to hosting an application with its assets hosted on a CDN, and its core functionality available behind a path prefix.
80+
81+
### Usage with `gatsby-plugin-offline`
82+
83+
When using a custom asset prefix with `gatsby-plugin-offline`, your assets can still be cached offline. However, to ensure the plugin works correctly, there are a few things you need to do.
84+
85+
1. Your asset server needs to have the following headers set:
86+
87+
```
88+
Access-Control-Allow-Origin: <site origin>
89+
Access-Control-Allow-Credentials: true
90+
```
91+
92+
Note that the origin needs to be specific, rather than using `*` to allow all origins. This is because Gatsby makes requests to fetch resources with `withCredentials` set to `true`, which disallows using `*` to match all origins. This is also why the second header is required. For local testing, use `http://localhost:9000` as the origin.
93+
94+
2. Certain essential resources need to be available on your content server (i.e. the one used to serve pages). This includes `sw.js`, as well as resources to precache: the Webpack bundle, the app bundle, the manifest (and any icons referenced), and the resources for the offline plugin app shell.
95+
96+
You can find most of these by looking for the `self.__precacheManifest` variable in your generated `sw.js`. Remember to also include `sw.js` itself, and any icons referenced in your `manifest.webmanifest` if you have one. To check your service worker is functioning as expected, look in Application → Service Workers in your browser dev tools, and check for any failed resources in the Console/Network tabs.

docs/docs/gatsby-config.md

-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ It's common for sites to be hosted somewhere other than the root of their domain
6363

6464
```javascript:title=gatsby-config.js
6565
module.exports = {
66-
// Note: it must *not* have a trailing slash.
6766
pathPrefix: `/blog`,
6867
}
6968
```

docs/docs/path-prefix.md

+61-25
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,82 @@
22
title: Adding a Path Prefix
33
---
44

5-
Many sites are hosted at something other than the root of their domain.
5+
Many applications are hosted at something other than the root (`/`) of their domain.
66

7-
E.g. a Gatsby blog could live at `example.com/blog/` or a site could be hosted
8-
on GitHub Pages at `example.github.io/my-gatsby-site/`
7+
For example, a Gatsby blog could live at `example.com/blog/` or a site could be hosted on GitHub Pages at `example.github.io/my-gatsby-site/`
98

109
Each of these sites need a prefix added to all paths on the site. So a link to
1110
`/my-sweet-blog-post/` should be rewritten to `/blog/my-sweet-blog-post`.
1211

13-
In addition links to various resources (JavaScript, images, CSS) need the same
14-
prefix added (this is accomplished by setting the `publicPath` in webpack).
12+
In addition links to various resources (JavaScript, CSS, images, and other static content) need the same prefix, so that the site continues to function and display correctly, even if served from this path prefix.
1513

16-
Luckily, for most sites, this work can be offloaded to Gatsby. Using
17-
[Gatsby's Link component](/docs/gatsby-link/) for internal links ensures those links
18-
will be prefixed correctly. Gatsby ensures that paths created internally and by
19-
webpack are also correctly prefixed.
14+
Let's get this functionality implemented. We'll add an option to our `gatsby-config.js`, and add a flag to our build command.
2015

21-
## Development
16+
### Add to `gatsby-config.js`
2217

23-
During development, write paths as if there was no path prefix e.g. for a blog
24-
hosted at `example.com/blog`, don't add `/blog` to your links. The prefix will
25-
be added when you build for deployment.
26-
27-
## Production build
28-
29-
There are two steps for building a site with path prefixes.
30-
31-
First define the prefix in your site's `gatsby-config.js`.
32-
33-
```javascript:title=gatsby-config.js
18+
```js:title=gatsby-config.js
3419
module.exports = {
35-
// Note: it must *not* have a trailing slash.
3620
pathPrefix: `/blog`,
3721
}
3822
```
3923

40-
Then pass `--prefix-paths` cmd option to Gatsby.
24+
### Build
25+
26+
Once the `pathPrefix` is specified in `gastby-config.js`, we are well on our way to a prefixed app. The final step is to build out your application with a flag `--prefix-paths`, like so:
4127

4228
```shell
4329
gatsby build --prefix-paths
4430
```
4531

46-
NOTE: When running the command without the `--prefix-paths` flag, Gatsby ignores
47-
your `pathPrefix`.
32+
If this flag is not passed, Gatsby will ignore your `pathPrefix` and build out your site as if it were hosted from the root domain.
33+
34+
### In-app linking
35+
36+
As a developer using this feature, it should be seamless. We provide APIs and libraries to make using this functionality a breeze. Specifically, the [`Link`](/docs/gatsby-link/) component has built-in functionality to handle path prefixing.
37+
38+
For example, if we want to link to our `/page-2` link (but the actual link will be prefixed, e.g. `/blog/page-2`) we don't want to hard code this path prefix in all of our links. We have your back! By using the `Link` component, we will automatically prefix your paths for you. If you later migrate off of `pathPrefix` your links will _still_ work seamlessly.
39+
40+
Let's look at a quick example.
41+
42+
```jsx:title=src/pages/index.js
43+
import React from "react"
44+
import { Link } from "gatsby"
45+
import Layout from "../components/layout"
46+
47+
function Index() {
48+
return (
49+
<Layout>
50+
{/* highlight-next-line */}
51+
<Link to="page-2">Page 2</Link>
52+
</Layout>
53+
)
54+
}
55+
```
56+
57+
Without doing _anything_ and merely using the `Link` component, this link will be prefixed with our specified `pathPrefix` in `gatsby-config.js`. Woo hoo!
58+
59+
If we want to do programatic/dynamic navigation, totally possible too! We expose a `navigate` helper, and this too automatically handles path prefixing.
60+
61+
```jsx:title=src/pages/index.js
62+
import React from "react"
63+
import { navigate } from "gatsby"
64+
import Layout from "../components/layout"
65+
66+
export default function Index() {
67+
return (
68+
<Layout>
69+
{/* Note: this is an intentionally contrived example, but you get the idea! */}
70+
{/* highlight-next-line */}
71+
<button onClick={() => navigate("/page-2")}>
72+
Go to page 2, dynamically
73+
</button>
74+
</Layout>
75+
)
76+
}
77+
```
78+
79+
### Additional Considerations
80+
81+
The [`assetPrefix`](/docs/asset-prefix/) feature can be thought of as semi-related to this feature. That feature allows your assets (non-HTML files, e.g. images, JavaScript, etc.) to be hosted on a separate domain, for example a CDN.
82+
83+
This feature works seamlessly with `assetPrefix`. Build out your application with the `--prefix-paths` flag and you'll be well on your way to hosting an application with its assets hosted on a CDN, and its core functionality available behind a path prefix.

e2e-tests/development-runtime/cypress/integration/navigation/linking.js

+10
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ describe(`navigation`, () => {
2525
cy.location(`pathname`).should(`equal`, `/`)
2626
})
2727

28+
it(`can navigate back using history`, () => {
29+
cy.getTestElement(`page-two`)
30+
.click()
31+
.waitForRouteChange()
32+
33+
cy.go(`back`).waitForRouteChange()
34+
35+
cy.location(`pathname`).should(`equal`, `/`)
36+
})
37+
2838
describe(`non-existant route`, () => {
2939
beforeEach(() => {
3040
cy.getTestElement(`broken-link`)

e2e-tests/gatsby-image/gatsby-config.js

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
const path = require(`path`)
22

33
module.exports = {
4-
pathPrefix: `/blog`,
54
siteMetadata: {
65
title: `Gatsby Image e2e`,
76
},

e2e-tests/path-prefix/.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
node_modules
44
yarn-error.log
55

6-
# Build directory
6+
# Build assets
77
/public
88
.DS_Store
9+
/assets
910

1011
# Cypress output
1112
cypress/videos/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
const { assetPrefix } = require(`../../gatsby-config`)
2+
3+
const assetPrefixExpression = new RegExp(`^${assetPrefix}`)
4+
5+
const assetPrefixMatcher = (chain, attr = `href`) =>
6+
chain.should(`have.attr`, attr).and(`matches`, assetPrefixExpression)
7+
8+
describe(`assetPrefix`, () => {
9+
beforeEach(() => {
10+
cy.visit(`/`).waitForRouteChange()
11+
})
12+
13+
describe(`runtime`, () => {
14+
it(`prefixes preloads`, () => {
15+
assetPrefixMatcher(cy.get(`head link[rel="preload"]`))
16+
})
17+
18+
it(`prefixes styles`, () => {
19+
assetPrefixMatcher(cy.get(`head style[data-href]`), `data-href`)
20+
})
21+
22+
it(`prefixes scripts`, () => {
23+
assetPrefixMatcher(cy.get(`body script[src]`), `src`)
24+
})
25+
})
26+
27+
describe(`gatsby-plugin-manifest`, () => {
28+
it(`prefixes manifest`, () => {
29+
assetPrefixMatcher(cy.get(`head link[rel="manifest"]`))
30+
})
31+
32+
it(`prefixes shortcut icon`, () => {
33+
assetPrefixMatcher(cy.get(`head link[rel="shortcut icon"]`))
34+
})
35+
36+
it(`prefixes manifest icons`, () => {
37+
assetPrefixMatcher(cy.get(`head link[rel="apple-touch-icon"]`))
38+
})
39+
})
40+
41+
describe(`gatsby-plugin-sitemap`, () => {
42+
it(`prefixes sitemap`, () => {
43+
assetPrefixMatcher(cy.get(`head link[rel="sitemap"]`))
44+
})
45+
})
46+
47+
describe(`gatsby-plugin-feed`, () => {
48+
it(`prefixes RSS feed`, () => {
49+
assetPrefixMatcher(cy.get(`head link[type="application/rss+xml"]`))
50+
})
51+
})
52+
})

e2e-tests/path-prefix/cypress/integration/path-prefix.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ describe(`Production pathPrefix`, () => {
3939
cy.getTestElement(`page-2-link`)
4040
.click()
4141
.waitForRouteChange()
42-
43-
cy.go(`back`).waitForRouteChange()
42+
.go(`back`)
43+
.waitForRouteChange()
4444

4545
cy.location(`pathname`).should(`eq`, withTrailingSlash(pathPrefix))
4646
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// ***********************************************************
2+
// This example plugins/index.js can be used to load plugins
3+
//
4+
// You can change the location of this file or turn off loading
5+
// the plugins file with the 'pluginsFile' configuration option.
6+
//
7+
// You can read more here:
8+
// https://on.cypress.io/plugins-guide
9+
// ***********************************************************
10+
11+
// This function is called when a project is opened or re-opened (e.g. due to
12+
// the project's config changing)
13+
14+
module.exports = (on, config) => {
15+
// `on` is used to hook into various events Cypress emits
16+
// `config` is the resolved Cypress config
17+
}

0 commit comments

Comments
 (0)