DEV Community

Cover image for Help serving assets over HTTP/2 for a Gatsby Netlify hosted site
Nick Taylor
Nick Taylor

Posted on • Updated on • Originally published at iamdeveloper.com

Help serving assets over HTTP/2 for a Gatsby Netlify hosted site

Hi all. I threw out this Tweet into the Twitterverse, but thought it would be wise to ask for help here as well.

I have a Gatsby site deployed to Netlify, and some of my assets are being served over HTTP/1.1. I know that Netlify supports HTTP/2 by default for sites enabled with HTTP2.

I know that I need to add entries into my _headers file, e.g.

/
  Link: </js/example-script.js>; rel=preload; as=script
  Link: </css/example-style.css>; rel=preload; as=style
Enter fullscreen mode Exit fullscreen mode

but it would be a pain to update this after every deploy. Is anyone aware of a gatsby plugin that might do this, or how do you go about handling this with your Gatsby site when hosted on Netlify?

I can probably generate the _headers file as part of my build process, but my gut tells me someone has already done this 😉

The source code is here if anyone is interested.

GitHub logo nickytonline / www.nickyt.co

Source code for my web site nickyt.co

Netlify Status

Welcome

This is the source code for the web site of Nick Taylor built using the Eleventy static site generator. It uses Andy Bell's Hylia template. I've made tweaks to it and will probably make more over time, but just wanted to give Andy a shout out for his awesome template.

Terminal commands

Install the dependencies first

npm install
Enter fullscreen mode Exit fullscreen mode

Serve the site locally in watch mode

npm start
Enter fullscreen mode Exit fullscreen mode

Build a production version of the site

npm run production
Enter fullscreen mode Exit fullscreen mode

Test the production site locally

cd dist
npx serve
Enter fullscreen mode Exit fullscreen mode

Photo by Lukas Juhas on Unsplash

Top comments (12)

Collapse
 
sebastienlorber profile image
Sebastien Lorber

Hey, signed up just to tell that http2 was not working on my Netlify site.

Turns out it was due to a firewall (Avast -> Web Shield -> Turn off "inspect https requests")

If sites like http2.pro/ tells you http2 works on your site, but both Chrome and FF show https1.1 protocol, I suggest you inspect your site from another computer/network and turn off your computer protections...

This is a really annoying thing if ubiquitous user software like Avast start to prevent website optimizations we work hard to setup...

Collapse
 
nickytonline profile image
Nick Taylor

Thanks for the heads up Sebastien!

Collapse
 
nickytonline profile image
Nick Taylor • Edited

So I'm actually on the wrong track. What I mention in the post is to enable HTTP2 Server Push which is an optional thing. The pages should be serving over HTTP2. @easyaspython put me on to the right track, but still not solved.

Collapse
 
darcyrayner profile image
Darcy Rayner

It should use HTTP 2 by default. It looks like all the requests that are HTTP 1.1 from the iamdeveloper domain are being loaded by a service worker. I reckon that has something to do with it.

Collapse
 
nickytonline profile image
Nick Taylor

Yeah that was what @easyaspython had guessed. So I guess a false reading from lighthouse?

Collapse
 
nickytonline profile image
Nick Taylor

It's apparently a bug with Chrome and Lighthouse.

ServiceWorker serving cache in HTTP/1.1 protocol #11123

rayriffy avatar
rayriffy commented on Jan 17, 2019

Description

I deployed my prodution and staging site on Netlify and I got reports from Lighthouse audits that some my resources are being served in HTTP/1.1 protocol which it shouldn't do that.

I found out later that all of the resurces that being served in HTTP/1.1 are from ServiceWorker. I think there's some issue with gatsby-plugin-offine

Steps to reproduce

  1. Open DevTools with Network tab
  2. Go to blog-staging.rayriffy.com. From this point you can see all resources are downloaded in HTTP/2 protocol
  3. Refresh page. And you should see cached data from ServiceWorker are served in HTTP/1.1

Expected result

ServiceWorker should serve cache in HTTP/2 protocol

Actual result

It served in HTTP/1.1 protocol

Environment

  System:
    OS: Windows 10
    CPU: x64 Intel(R) Core(TM) i7-5500U CPU @ 2.40GHz
  Binaries:
    Yarn: 1.13.0 - C:\Program Files (x86)\Yarn\bin\yarn.CMD
    npm: 6.5.0 - C:\Program Files\nodejs\npm.CMD
  Browsers:
    Edge: 44.17763.1.0
  npmPackages:
    gatsby: ^2.0.91 => 2.0.91
    gatsby-image: ^2.0.26 => 2.0.26
    gatsby-paginate: ^1.0.16 => 1.0.16
    gatsby-plugin-feed: ^2.0.8 => 2.0.11
    gatsby-plugin-google-analytics: ^2.0.9 => 2.0.9
    gatsby-plugin-google-fonts: ^0.0.4 => 0.0.4
    gatsby-plugin-manifest: ^2.0.13 => 2.0.13
    gatsby-plugin-netlify: ^2.0.5 => 2.0.6
    gatsby-plugin-netlify-cache: ^1.0.0 => 1.0.0
    gatsby-plugin-offline: ^2.0.21 => 2.0.21
    gatsby-plugin-react-helmet: ^3.0.5 => 3.0.5
    gatsby-plugin-robots-txt: ^1.3.0 => 1.3.0
    gatsby-plugin-sharp: ^2.0.17 => 2.0.17
    gatsby-plugin-sitemap: ^2.0.4 => 2.0.4
    gatsby-plugin-typography: ^2.2.5 => 2.2.5
    gatsby-remark-copy-linked-files: ^2.0.8 => 2.0.8
    gatsby-remark-embed-gist: ^1.1.5 => 1.1.5
    gatsby-remark-embed-spotify: ^2.0.2 => 2.0.2
    gatsby-remark-images: ^3.0.1 => 3.0.1
    gatsby-remark-prismjs: ^3.2.0 => 3.2.0
    gatsby-remark-responsive-iframe: ^2.0.8 => 2.0.8
    gatsby-remark-smartypants: ^2.0.5 => 2.0.7
    gatsby-source-filesystem: ^2.0.16 => 2.0.16
    gatsby-transformer-json: ^2.1.7 => 2.1.7
    gatsby-transformer-remark: ^2.2.0 => 2.2.0
    gatsby-transformer-sharp: ^2.1.10 => 2.1.10

error The system cannot find the path specified.

gatsby-config.js

Source
var hostname

if (process.env.GATSBY_ENV === 'production') {
  hostname = 'https://blog.rayriffy.com'
} else if (process.env.GATSBY_ENV === 'staging') {
  hostname = 'https://blog-staging.rayriffy.com'
} else if (process.env.GATSBY_ENV === 'development') {
  hostname = 'https://localhost:8000'
}

module.exports = {
  siteMetadata: {
    title: 'Riffy Blog',
    author: 'Phumrapee Limpianchop',
    description: 'The Nerdy Blogger',
    siteUrl: `${hostname}`,
  },
  pathPrefix: '/',
  plugins: [
    {
      resolve: `gatsby-plugin-google-fonts`,
      options: {
        fonts: [`kanit`],
      },
    },
    `gatsby-plugin-netlify-cache`,
    `gatsby-transformer-json`,
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        path: `./src/assets/database`,
      },
    },
    {
      resolve: 'gatsby-plugin-robots-txt',
      options: {
        resolveEnv: () => process.env.GATSBY_ENV,
        env: {
          production: {
            policy: [
              {
                userAgent: '*',
                disallow: ['/pages', '/category', '/author'],
              },
            ],
          },
          staging: {
            policy: [
              {
                userAgent: '*',
                disallow: ['/'],
              },
            ],
          },
          development: {
            policy: [
              {
                userAgent: '*',
                disallow: ['/'],
              },
            ],
          },
        },
      },
    },
    {
      resolve: `gatsby-plugin-netlify`,
      options: {
        headers: {
          '/feed.json': ['Access-Control-Allow-Origin: *'],
        },
      },
    },
    {
      resolve: `gatsby-plugin-sitemap`,
      options: {
        output: `/sitemap.xml`,
        exclude: [
          '/pages/*',
          '/category',
          '/category/*',
          '/author',
          '/author/*',
        ],
      },
    },
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        path: `${__dirname}/src/pages`,
        name: 'pages',
        ignore: [`**/.*`],
      },
    },
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        path: `${__dirname}/src/assets`,
        name: 'assets',
        ignore: [`**/.*`],
      },
    },
    {
      resolve: `gatsby-transformer-remark`,
      options: {
        plugins: [
          'gatsby-remark-embed-spotify',
          'riffy-gjs-embeded-video',
          {
            resolve: 'gatsby-remark-embed-gist',
            options: {
              username: 'rayriffy',
              includeDefaultCss: true,
            },
          },
          {
            resolve: `gatsby-remark-images`,
            options: {
              maxWidth: 1000,
              linkImagesToOriginal: false,
              sizeByPixelDensity: true,
              withWebp: true,
              quality: 80,
            },
          },
          'gatsby-remark-responsive-iframe',
          'gatsby-remark-prismjs',
          'gatsby-remark-copy-linked-files',
          'gatsby-remark-smartypants',
        ],
      },
    },
    `gatsby-transformer-sharp`,
    `gatsby-plugin-sharp`,
    {
      resolve: `gatsby-plugin-google-analytics`,
      options: {
        trackingId: `${
          process.env.GATSBY_ENV === 'production'
            ? 'UA-85367836-2'
            : process.env.GATSBY_ENV === 'staging'
            ? 'UA-85367836-3'
            : ''
        }`,
      },
    },
    `gatsby-plugin-feed`,
    {
      resolve: `gatsby-plugin-manifest`,
      options: {
        name: `Riffy Blog`,
        short_name: `Riffy Blog`,
        start_url: `/`,
        background_color: `#f5f5f5`,
        theme_color: `#1e88e5`,
        display: `minimal-ui`,
        icon: `src/assets/logo.png`,
      },
    },
    {
      resolve: `gatsby-plugin-offline`,
      options: {
        dontCacheBustUrlsMatching: /(\.js$|\.css$|\/static\/)/,
        runtimeCaching: [
          {
            urlPattern: /(\.js$|\.css$|\/static\/)/,
            handler: `cacheFirst`,
          },
          {
            urlPattern: /^https?:\/\/(www\.blog.rayriffy\.com|localhost:8000|localhost:9000|blog-staging\.rayriffy\.com|blog\.rayriffy\.com).*\.(png|jpg|jpeg|webp|svg|gif|tiff|js|woff|woff2|json|css)$/,
            handler: `staleWhileRevalidate`,
          },
          {
            urlPattern: /^https?:\/\/fonts\.googleapis\.com\/css/,
            handler: `staleWhileRevalidate`,
          },
        ],
      },
    },
    `gatsby-plugin-react-helmet`,
    {
      resolve: 'gatsby-plugin-typography',
      options: {
        pathToConfigModule: 'src/utils/typography',
        omitGoogleFont: true,
      },
    },
  ],
}

package.json

Source
{
  "name": "rayriffy-blog",
  "description": "The Nerdy Blogger",
  "version": "1.0.10",
  "author": "Phumrapee Limpianchop <contact@rayriffy.com>",
  "bugs": {
    "url": "https://github.com/rayriffy/rayriffy-blog/issues"
  },
  "dependencies": {
    "cross-env": "^5.2.0",
    "gatsby": "^2.0.91",
    "gatsby-image": "^2.0.26",
    "gatsby-paginate": "^1.0.16",
    "gatsby-plugin-feed": "^2.0.8",
    "gatsby-plugin-google-analytics": "^2.0.9",
    "gatsby-plugin-google-fonts": "^0.0.4",
    "gatsby-plugin-manifest": "^2.0.13",
    "gatsby-plugin-netlify": "^2.0.5",
    "gatsby-plugin-netlify-cache": "^1.0.0",
    "gatsby-plugin-offline": "^2.0.21",
    "gatsby-plugin-react-helmet": "^3.0.5",
    "gatsby-plugin-robots-txt": "^1.3.0",
    "gatsby-plugin-sharp": "^2.0.17",
    "gatsby-plugin-sitemap": "^2.0.4",
    "gatsby-plugin-typography": "^2.2.5",
    "gatsby-remark-copy-linked-files": "^2.0.8",
    "gatsby-remark-embed-gist": "^1.1.5",
    "gatsby-remark-embed-spotify": "^2.0.2",
    "gatsby-remark-images": "^3.0.1",
    "gatsby-remark-prismjs": "^3.2.0",
    "gatsby-remark-responsive-iframe": "^2.0.8",
    "gatsby-remark-smartypants": "^2.0.5",
    "gatsby-source-filesystem": "^2.0.16",
    "gatsby-transformer-json": "^2.1.7",
    "gatsby-transformer-remark": "^2.2.0",
    "gatsby-transformer-sharp": "^2.1.10",
    "lodash": "^4.17.11",
    "prismjs": "^1.15.0",
    "prop-types": "^15.6.2",
    "react": "^16.7.0",
    "react-adsense": "^0.0.6",
    "react-dom": "^16.7.0",
    "react-helmet": "^5.2.0",
    "react-icons": "^3.3.0",
    "react-typography": "^0.16.18",
    "riffy-gjs-embeded-video": "^1.3.1",
    "typeface-merriweather": "0.0.54",
    "typeface-montserrat": "0.0.43",
    "typography": "^0.16.17"
  },
  "devDependencies": {
    "babel-eslint": "^10.0.1",
    "eslint": "^5.12.0",
    "eslint-config-prettier": "^3.5.0",
    "eslint-config-standard": "^12.0.0",
    "eslint-plugin-import": "^2.14.0",
    "eslint-plugin-node": "^8.0.1",
    "eslint-plugin-prettier": "^3.0.1",
    "eslint-plugin-promise": "^4.0.1",
    "eslint-plugin-react": "^7.12.3",
    "eslint-plugin-standard": "^4.0.0",
    "prettier": "^1.14.2"
  },
  "homepage": "https://blog.rayriffy.com",
  "keywords": [
    "gatsby",
    "gatsbyjs",
    "react",
    "reactjs",
    "es6",
    "blog"
  ],
  "license": "MIT",
  "main": "n/a",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/rayriffy/rayriffy-blog.git"
  },
  "scripts": {
    "dev": "cross-env GATSBY_ENV=development gatsby develop --https",
    "dev-staging": "cross-env GATSBY_ENV=staging gatsby develop --https",
    "dev-prod": "cross-env GATSBY_ENV=production gatsby develop --https",
    "lint": "./node_modules/.bin/eslint --ext .js,.jsx --ignore-pattern public .",
    "test": "echo \"Error: no test specified\" && exit 1",
    "format": "prettier --trailing-comma es6 --no-semi --single-quote --write 'src/**/*.js' 'src/**/*.md'",
    "develop": "cross-env GATSBY_ENV=development gatsby develop --https",
    "start": "npm run develop",
    "build": "cross-env GATSBY_ENV=production gatsby build",
    "build-staging": "cross-env GATSBY_ENV=staging gatsby build",
    "deploy": "gatsby build --prefix-paths",
    "fix-semi": "eslint --quiet --ignore-pattern node_modules --ignore-pattern public --parser babel-eslint --no-eslintrc --rule '{\"semi\": [2, \"never\"], \"no-extra-semi\": [2]}' --fix gatsby-node.js"
  }
}

gatsby-node.js

Source
const _ = require('lodash')
const Promise = require('bluebird')
const fs = require('fs')
const path = require('path')
const {createFilePath} = require('gatsby-source-filesystem')

exports.createPages = ({graphql, actions}) => {
  const {createPage} = actions

  var siteUrl

  return new Promise((resolve, reject) => {
    resolve(
      graphql(
        `
          {
            site {
              siteMetadata {
                siteUrl
              }
            }
            allMarkdownRemark(
              sort: {fields: [frontmatter___date], order: DESC}
            ) {
              edges {
                node {
                  fields {
                    slug
                  }
                  frontmatter {
                    title
                    subtitle
                    status
                    author
                  }
                }
              }
            }
            allCategoriesJson {
              edges {
                node {
                  key
                  name
                  desc
                }
              }
            }
            allAuthorsJson {
              edges {
                node {
                  user
                }
              }
            }
            lifestyle: allMarkdownRemark(
              filter: {frontmatter: {category: {regex: "/lifestyle/"}}}
            ) {
              edges {
                node {
                  frontmatter {
                    status
                  }
                }
              }
            }
            misc: allMarkdownRemark(
              filter: {frontmatter: {category: {regex: "/misc/"}}}
            ) {
              edges {
                node {
                  frontmatter {
                    status
                  }
                }
              }
            }
            music: allMarkdownRemark(
              filter: {frontmatter: {category: {regex: "/music/"}}}
            ) {
              edges {
                node {
                  frontmatter {
                    status
                  }
                }
              }
            }
            programming: allMarkdownRemark(
              filter: {frontmatter: {category: {regex: "/programming/"}}}
            ) {
              edges {
                node {
                  frontmatter {
                    status
                  }
                }
              }
            }
            review: allMarkdownRemark(
              filter: {frontmatter: {category: {regex: "/review/"}}}
            ) {
              edges {
                node {
                  frontmatter {
                    status
                  }
                }
              }
            }
            tutorial: allMarkdownRemark(
              filter: {frontmatter: {category: {regex: "/tutorial/"}}}
            ) {
              edges {
                node {
                  frontmatter {
                    status
                  }
                }
              }
            }
            rayriffy: allMarkdownRemark(
              filter: {frontmatter: {author: {regex: "/rayriffy/"}}}
            ) {
              edges {
                node {
                  frontmatter {
                    status
                  }
                }
              }
            }
            SiriuSStarS: allMarkdownRemark(
              filter: {frontmatter: {author: {regex: "/SiriuSStarS/"}}}
            ) {
              edges {
                node {
                  frontmatter {
                    status
                  }
                }
              }
            }
          }
        `,
      )
        .then(result => {
          siteUrl = result.data.site.siteMetadata.siteUrl
          var filteredresult
          if (
            process.env.GATSBY_ENV === 'production' ||
            process.env.GATSBY_ENV === 'staging'
          ) {
            filteredresult = {
              data: {
                allMarkdownRemark: {edges: null},
                allCategoriesJson: {edges: null},
                allAuthorsJson: {edges: null},
                lifestyle: {edges: null},
                misc: {edges: null},
                music: {edges: null},
                programming: {edges: null},
                review: {edges: null},
                tutorial: {edges: null},
                rayriffy: {edges: null},
                SiriuSStarS: {edges: null},
              },
            }
            filteredresult.data.allMarkdownRemark.edges = result.data.allMarkdownRemark.edges.filter(
              a => a.node.frontmatter.status === 'published',
            )
            filteredresult.data.lifestyle.edges = result.data.lifestyle.edges.filter(
              a => a.node.frontmatter.status === 'published',
            )
            filteredresult.data.misc.edges = result.data.misc.edges.filter(
              a => a.node.frontmatter.status === 'published',
            )
            filteredresult.data.music.edges = result.data.music.edges.filter(
              a => a.node.frontmatter.status === 'published',
            )
            filteredresult.data.programming.edges = result.data.programming.edges.filter(
              a => a.node.frontmatter.status === 'published',
            )
            filteredresult.data.review.edges = result.data.review.edges.filter(
              a => a.node.frontmatter.status === 'published',
            )
            filteredresult.data.tutorial.edges = result.data.tutorial.edges.filter(
              a => a.node.frontmatter.status === 'published',
            )
            filteredresult.data.rayriffy.edges = result.data.rayriffy.edges.filter(
              a => a.node.frontmatter.status === 'published',
            )
            filteredresult.data.SiriuSStarS.edges = result.data.SiriuSStarS.edges.filter(
              a => a.node.frontmatter.status === 'published',
            )
            filteredresult.data.allCategoriesJson.edges =
              result.data.allCategoriesJson.edges
            filteredresult.data.allAuthorsJson.edges =
              result.data.allAuthorsJson.edges
          } else if (process.env.GATSBY_ENV === 'development') {
            filteredresult = result
          }
          return filteredresult
        })
        .then(result => {
          if (result.errors) {
            console.error(result.errors)
            reject(result.errors)
          }

          const posts = result.data.allMarkdownRemark.edges
          const catrgories = result.data.allCategoriesJson.edges
          const authors = result.data.allAuthorsJson.edges

          var filter
          const postsPerPage = 5
          if (
            process.env.GATSBY_ENV === 'production' ||
            process.env.GATSBY_ENV === 'staging'
          ) {
            filter = 'draft'
          } else if (process.env.GATSBY_ENV === 'development') {
            filter = ''
          }

          // Create blog lists pages.
          const numPages = Math.ceil(posts.length / postsPerPage)

          _.times(numPages, i => {
            createPage({
              path: i === 0 ? `/` : `/pages/${i + 1}`,
              component: path.resolve('./src/templates/blog-list.js'),
              context: {
                limit: postsPerPage,
                skip: i * postsPerPage,
                status: filter,
                numPages,
                currentPage: i + 1,
              },
            })
          })

          // Create blog posts pages.
          var count = 0
          var jsonFeed = []
          _.each(posts, (post, index) => {
            const previous =
              index === posts.length - 1 ? null : posts[index + 1].node
            const next = index === 0 ? null : posts[index - 1].node

            if (count < 5) {
              jsonFeed.push({
                name: post.node.frontmatter.title,
                desc: post.node.frontmatter.subtitle,
                slug: siteUrl + post.node.fields.slug,
              })
            }

            createPage({
              path: post.node.fields.slug,
              component: path.resolve('./src/templates/blog-post.js'),
              context: {
                author: post.node.frontmatter.author,
                slug: post.node.fields.slug,
                previous,
                next,
              },
            })
            count++
          })

          fs.writeFile('public/feed.json', JSON.stringify(jsonFeed), function(
            err,
          ) {
            if (err) {
              console.error(err)
              reject(err)
            }
          })

          // Create category pages
          var categoryPathPrefix = 'category/'
          _.each(catrgories, category => {
            var totalCount = result.data[category.node.key].edges.length
            var numCategoryPages = Math.ceil(totalCount / postsPerPage)
            var pathPrefix = categoryPathPrefix + category.node.key
            _.times(numCategoryPages, i => {
              createPage({
                path: i === 0 ? pathPrefix : pathPrefix + `/pages/${i + 1}`,
                component: path.resolve('./src/templates/category.js'),
                context: {
                  category: category.node.key,
                  currentPage: i + 1,
                  limit: postsPerPage,
                  numPages: numCategoryPages,
                  pathPrefix,
                  regex: '/' + category.node.key + '/',
                  skip: i * postsPerPage,
                  status: filter,
                },
              })
            })
          })

          // Create author pages
          var authorPathPrefix = 'author/'
          _.each(authors, author => {
            var totalCount = result.data[author.node.user].edges.length
            var numAuthorPages = Math.ceil(totalCount / postsPerPage)
            var pathPrefix = authorPathPrefix + author.node.user
            _.times(numAuthorPages, i => {
              createPage({
                path: i === 0 ? pathPrefix : pathPrefix + `/pages/${i + 1}`,
                component: path.resolve('./src/templates/author.js'),
                context: {
                  author: author.node.user,
                  currentPage: i + 1,
                  limit: postsPerPage,
                  numPages: numAuthorPages,
                  pathPrefix,
                  regex: '/' + author.node.user + '/',
                  skip: i * postsPerPage,
                  status: filter,
                },
              })
            })
          })
        }),
    )
  })
}

exports.onCreateNode = ({node, actions, getNode}) => {
  const {createNodeField} = actions

  if (node.internal.type === `MarkdownRemark`) {
    const value = createFilePath({node, getNode})
    createNodeField({
      name: `slug`,
      node,
      value,
    })
  }
}

PS. I will drop repository here github.com/rayriffy/rayriffy-blog

Thread Thread
 
darcyrayner profile image
Darcy Rayner

It happens in Firefox as well. I think that bug is probably in that gatsby plugin. I've seen service workers use HTTP 2 for loading content before.

Collapse
 
ajmalafif profile image
Ajmal Afif

Hi Nick,

I just checked your site I believe you still haven't solve this, I am testing out some stuffs the _headers and netlify.toml. Will report back if that works!

Collapse
 
cbetta profile image
Cristiano Betta

Did you ever figure this out?

Collapse
 
cbetta profile image
Cristiano Betta

Ignore me, I just spotted that lighthouse is due to be updated to solve this after mid-April

Collapse
 
swyx profile image
swyx
Collapse
 
nickytonline profile image
Nick Taylor

I use Netlify CMS with Gatsby. I believe that includes the gatsby-plugin-netlify package? Either way, I guess I just need to configure it as per the docs?