HashiCorp Nextjs Scripts
This tool layers a number of configuration choices, code quality checks, and code generators on top of next.js. Specifically, it provides:
- Baked in, zero-config typescript linting & prettier formatting via binary
- Code generators for base website templates, new pages, and new components via binary
- A pre-configured client for easily fetching from DatoCMS
- A strong set of default plugins, including:
- mdx-processed markdown with front-matter and layouts
- css files with pre-configured postcss-preset-env can be imported directly into components
- graphql file loader
- webpack bundle analyzer
Table Of Contents
Quick reference on how to create a new website template: npx @hashicorp/nextjs-scripts generate website
Basic Usage & Options
The plugin looks like this inside of your next.config.js
file:
const withHashicorp = require('@hashicorp/nextjs-scripts')
module.exports = withHashicorp()()
Let's go through the full options:
withHashicorp({
css: {
plugins: [somePlugin(), otherPlugin()],
presetEnvOptions: { stage: 3 },
},
dato: { token: 'xxx', environment: 'xxx' },
tipBranch: 'main',
transpileModules: ['foo'],
})
All of these are optional, none are required to make withHashicorp
function properly. In fact, we recommend not using any custom options unless you need to.
Default Plugins and Enhancements
Out of the box, this plugin adds a couple useful utilities:
These can both be used in any project implementing nextjs-scripts
as described in their readmes.
The Binary
nextjs-scripts
ships with a binary (next-hashicorp
) that includes a variety of useful tools, which we will go through below. Generally, we recommend using npx or a local install and npm scripts to run the binary, rather than installing globally.
Linting & Formatting
nextjs-scripts
provides centrally managed, pre-configured ESLint and Prettier tasks which can be executed via next-hashicorp lint
and next-hashicorp format
respectively. We recommend installing locally and running them as npm tasks. We prefer to run both of these tasks before any commit can be made -- if you share that preference, you can execute both using the command next-hashicorp precommit
.
Both the lint
and format
commands default to running over every file they are able to process, recursively, starting with the root of the project where you run the command. If you'd like to scope them to a specific set of files, any number of file paths or globs can be provided as an argument. For example:
$ next-hashicorp format pages/**/*.jsx lib/config.json
If you would like to change the configuration or use a different configuration for any of these tasks, we'd recommend forking the project and changing it to match your preferences. The purpose of a controlled, centralized config is to ensure that all projects that implement it are consistent, and allowing per-project config changes eliminates this benefit.
Stylelint Configuration
Nextjs-scripts configures the following Stylelint plugins, listed below:
- stylelint-config-standard with stylelint-config-prettier to skip prettier-managed rules.
- stylelint-media-use-custom-media to enforce usage of known custom media queries.
- stylelint-value-no-unknown-custom-properties to enforce usage of known custom properties.
- stylelint-order to alphabetize style declarations.
- stylelint-use-nesting to enforce proper CSS nesting.
You can modify the Stylelint configuration in your local stylelintrc.js
file.
module.exports = {
...require('@hashicorp/nextjs-scripts/.stylelintrc.js'),
rules: {
'csstools/media-use-custom-media': [
'known',
{
importFrom: [
'./node_modules/@hashicorp/react-global-styles/custom-media.css',
],
},
],
'csstools/value-no-unknown-custom-properties': [
true,
{
importFrom: [
'./node_modules/@hashicorp/react-global-styles/custom-properties/color.css',
'./node_modules/@hashicorp/react-global-styles/custom-properties/font.css',
],
},
],
},
}
Generators
nextjs-scripts
also provides a few generators that can provision templates for common assets. At the moment, this includes:
next-hashicorp generate website
- creates a new, bare-bones website template that idiomatically implements next-hashicorp toolingnext-hashicorp generate component
- creates a new component template in your components
foldernext-hashicorp generate page
- creates a new page template in your pages
folder
After running these commands, you will be asked a couple questions, then your files will be generated.
Markdown Blocks
Many of our websites share common sections in their readmes which describe, for example, our custom markdown configuration, or how to start the server. It is much easier to keep these sections up to date in one central location than to try to maintain parity via copy-pasting across 10+ properties. This is the purpose of the markdown-blocks
command, which allows centrally located blocks of markdown to be rendered into readme files. Here's how it works with the markdown - you add a comment in the following format to specify a block section:
Some text, etc...
<!-- BEGIN: block-name -->
<!-- END: block-name -->
More text
Now make sure block-name
is a file within the /markdown-blocks
folder in this project. If that is all set, you can run next-hashicorp markdown-blocks path/to/readme.md
and it will parse the file and place the most recent version of any given block in its zone. Here's how the final output might look:
Some text, etc...
<!-- BEGIN: block-name -->
<!-- Generated text, do not edit directly -->
Contents of the `markdown-blocks/block-name.md` file will go here!
<!-- END: block-name -->
More text
If the content in the markdown block file needs to update, updating it and running the same command as above will ensure that the block area in the readme is using the latest content, but only when the command has been run. It should generate a clean diff wherever it's updated. The intent here is to ensure that when updates need to be made to common, shared readme sections, they can be made in one place and applied with a short, simple command in any place that uses them.
Blocks may have any valid markdown content, and cannot be nested within each other. A clear error will be thrown if a block is not found, is misspelled, or is nested.
GraphiQL
We provide a handy bin command that opens up Dato's in-browser GraphiQL IDE in your default browser. The URL to this IDE can be a bit annoying to track down because you need to have your API Token handy but since nextjs-scripts
hangs on to this, we can avoid that step.
next-hashicorp graphiql
If you're unfamiliar with what GraphiQL provides you, please have a look at the GraphiQL repo.
Markdown Compilation
Previously, this library bundled markdown processing through next-mdx-enhanced, but it was removed in version 15.0.0
in favor of next-mdx-remote
, which offers superior performance and flexibility. With the new mdx processing solution, markdown options are passed in manually in routes that process markdown, rather than centrally as part of the webpack configuration, as next-mdx-remote
loads mdx content as data rather than native js imports.
This library does still hold on to a set of common markdown configuration options that HashiCorp uses across properties though, which can be accessed as seen below in typescript-y format.
import { Plugin } from 'unified'
import { PluginOptions } from '@hashicorp/remark-plugins'
import markdownDefaults from '@hashicorp/nextjs-scripts/markdown'
const markdownDefaults({
addRehypePlugins?: []Plugin,
addRemarkPlugins?: []Plugin,
pluginOptions?: PluginOptions,
resolveIncludes?: String,
enableMath?: Boolean
})
For more detail on how to set up next-mdx-remote
in a nextjs site, check out the official example. To integrate with the defaults from this library, you'd just pass into renderToString
as mdxOptions
:
import markdownDefaults from '@hashicorp/nextjs-scripts/markdown'
export async function getStaticProps() {
renderToString(content, { mdxOptions: markdownDefaults() })
}
It's worth noting that the defaults add syntax highlighting using prism to all code blocks. In order to use the accompanying styles, you can import @hashicorp/nextjs-scripts/prism/style.css
into your main stylesheet.
Loading From DatoCMS
We use DatoCMS as an interface through which our non-technical staff can have the ability to modify content on our websites. Dato is not used on every part of every page, rather as we are building each site we decide which areas to add it to and what to make editable.
There are two different strategies for data loading, and depending on the scenario, you should use different tools and techniques to get it done.
DatoCMS exposes two endpoints. One provides production ready, published content. The other also returns records that are in a saved, but unpublished state for previewing. Setting HASHI_ENV=preview
in your environment will use the preview endpoint and return unpublished records. The default is to return production only records to avoid unexpectedly exposing preview content.
Loading Initial Data
If you need to load a set of initial data in order to render a component, and that data does not change at all after the initial load, you should use getStaticProps
to do it. nextjs-scripts
provides a pre-configured graphql request client that can be used to fetch data from DatoCMS as such:
import fetch from '@hashicorp/nextjs-scripts/dato/client'
import query from './query.graphql'
function someComponent({ posts }) {
return <p>{JSON.stringify(posts)}</p>
}
export async function getStaticProps() {
const { posts } = await fetch({ query })
return { posts }
}
This will integrate nicely with nextjs, ensuring that the necessary data is loaded before the page renders for client-side routing, and fetching on the server or at build time for dynamic and static build outputs, respectively.
Loading Dynamic Data
If you have more complex data fetching needs such as:
- you want to render the page first then fetch data after for only one portion of the page
- you want to fetch data in response to user input or client-side timers
- you want to re-fetch the initial data in response to user input or timers
- you want to make several different data fetching requests in parallel and render their outputs on the page as soon as they are available
You will need a more powerful tool than a blocking function that loads data only for the initial render. If you have run into this scenario, let's talk about it as a team -- we don't have a solution prepared as we haven't yet encountered this situation, but we have spent some time tinkering with tools like Apollo and URQL in the past which can be potential solutions.
CSS Processing
Nextjs-scripts configures a standard stack of postcss plugins, listed below:
If you'd like to add extra plugins before or after the stack, or change the options passed to postcss-preset-env
, you can control this via the css
options as such:
withHashicorp({
css: {
beforePlugins: [plugin1, plugin2],
afterPlugins: [plugin3],
presetEnvOptions: { nesting: false },
},
})()
Utilities
There are a few utility scripts for commonly used conventions in HashiCorp sites which are detailed below.
Bugsnag Configuration
It's nice and easy to set up Bugsnag with the central config in nextjs-scripts. To pull down and initialize the client, you can import it as such:
import Bugsnag from '@hashicorp/nextjs-scripts/lib/bugsnag'
Just make sure that you have defined BUGSNAG_CLIENT_KEY
and BUGSNAG_SERVER_KEY
as environment variables. It requires two keys because nextjs can render javascript on the client and server, and will interact with the service differently depending on the environment. The first time this import runs, the client will be initialized.
If you want to just pull down the ErrorBoundary
component, this can be imported directly as such:
import { ErrorBoundary } from '@hashicorp/nextjs-scripts/bugsnag'
NProgress
By default, nextjs does not provide any loading indicator for client-side route transitions. They recommend the use of NProgress, a small script that dislays a loading bar at the top of the browser frame.
It can be added to your app as such, within _app.js
import '@hashicorp/nextjs-scripts/lib/nprogress/style.css'
import NProgress from '@hashicorp/nextjs-scripts/lib/nprogress'
import Router from 'next/router'
NProgress({ Router })
If you want to add some custom action to the route change's start
, finish
, or error
states, you can pass in functions that will run accordingly:
import '@hashicorp/nextjs-scripts/lib/nprogress/style.css'
import NProgress from '@hashicorp/nextjs-scripts/lib/nprogress'
import Router from 'next/router'
NProgress({
Router,
start: () => console.log('route change started'),
finish: () => console.log('route change complete'),
error: () => console.log('route change error'),
})
It's worth noting that the finish
handler will always automatically fire an analytics page
event as long as the window.analytics
object is present.
Make sure to remember the css import as well!
Consent Manager
It is required that we use a consent manager for any and all scripts that track personal data, analytics included. We have a custom script that provides this functionality that can be brought in via nextjs-scripts
as such:
import createConsentManager from '@hashicorp/nextjs-scripts/lib/consent-manager'
const { ConsentManager, openConsentManager } = createConsentManager({
segmentWriteKey: 'xxx',
preset: ''
segmentServices: [],
otherServices: [],
categories: [],
})
For more detail on the segmentServices
, otherServices
, and categories
keys, see the consent manager documentation
The return values are the consent manager component, fully configured, which can be initialized empty like <ConsentManager />
, and an openConsentManager
function - whenever this is called it will open up the consent manager interface. We typically have a link in the footer that opens up these preferences.
By default, there will be no services loaded into the consent manager, it is strongly recommended to use one of the presets, oss
or enterprise
. The services included in each are detailed below:
OSS
- Google Analytics
- Optinmonster
Enterprise
- Google Analytics
- Google Tag Manager
- Marketo
- Heap
- LinkedIn Insights
Any other services added via the segmentServices
or otherServices
configuration keys will be added to and not overwrite services loaded in by presets. As a note, if you want to add one of the services that exists in either of the above lists, but is not included in your preset, or if you are building your own set of services, you can import services pre-configured as in the example below:
import createConsentManager from '@hashicorp/nextjs-scripts/lib/consent-manager'
import services from '@hashicorp/nextjs-scripts/lib/consent-manager/services'
const { ConsentManager, openConsentManager } = createConsentManager({
segmentWriteKey: 'xxx',
preset: 'oss',
segmentServices: [services.marketo],
})
As a general note, if you find yourself adding additional services or deviating significantly from the presets, it's a good idea to consult with the team first. The more services we add, the more we bloat the size and load time of our websites, so we try to be as minimal as possible while also serving the needs of the marketing organization.
Anchor Link Analytics
HashiCorp maintains a lot of documentation sites, all of which have many automatically generated permalinks based on headline text, and which can often break as a result of text changes and reorganization. As such, we try to run some extra analytics on permalinks by tracking, when a page url contains an anchor link (like hashicorp.com#foo
) whether the given anchor is actually present on the page. This allows us to more confidently remove custom anchor links that are unused, and to detect when a popular incoming anchor link is broken so it can be fixed.
To enable this tracking, simply import @hashicorp/nextjs-scripts/lib/anchor-link-analytics
in your _app.js
. This script is SSR-compatible and runs inside requestIdleCallback
so that it has a minimal impact on page performance. An example of a bare bones implementation:
import useAnchorLinkAnalytics from '@hashicorp/nextjs-scripts/lib/anchor-link-analytics'
export default function App({ Component, pageProps }) {
useAnchorLinkAnalytics()
return <Component {...pageProps} />
}
Utilities
The lib
folder hosts a variety of utilities that are intended to make development simpler across all our web properties. In this section we'll briefly discuss each one. Check out the new website template in generators/website/templates
for usage examples of each one. For more information on each one, check out their readmes, linked below:
Publishing
Publishing is handled automatically in CI through the use of labels on PRs. To mark your PR as a specific type of change, add the corresponding label:
If the change is internal to the package and does not impact consumer behavior, you can use the internal
label. If you want to skip a release for a PR, to group a set of PRs together under one release for example, use the skip-release
label.