@hashicorp/nextjs-scripts
TypeScript icon, indicating that this package has built-in type declarations

19.0.3 • Public • Published

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(/* options */)(/* normal nextjs config */)

Let's go through the full options:

withHashicorp({
  css: {
    // array of postcss plugins
    plugins: [somePlugin(), otherPlugin()],
    // https://github.com/csstools/postcss-preset-env#options
    presetEnvOptions: { stage: 3 },
  },
  dato: { token: 'xxx', environment: 'xxx' }, // if necessary, override the default datocms token/env with your own
  tipBranch: 'main', // configure a branch name for tip.project.io subdomain, to ensure "noindex" http header is set
  transpileModules: ['foo'], // third party package names that should be transpiled by babel
})

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.

// .stylelintrc.js with configured rules of custom media and custom properties.
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 tooling
  • next-hashicorp generate component - creates a new component template in your components folder
  • next-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({
  // Additional rehype or remark plugins
  addRehypePlugins?: []Plugin,
  addRemarkPlugins?: []Plugin,
  // options passed to remark plugins, example shown below
  // see https://github.com/hashicorp/remark-plugins for more info
  pluginOptions?: PluginOptions,
  // passes the value to `resolveFrom` include-markdown plugin
  // https://github.com/hashicorp/remark-plugins/blob/master/plugins/include-markdown/README.md#options
  resolveIncludes?: String,
  // enables math function processing via https://github.com/remarkjs/remark-math
  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 },
  },
})(/* normal nextjs config */)

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', // production only - staging/local key populated automatically in dev
  preset: ''              // optional but strong recommended: pick one: 'enterprise' or 'oss'
  segmentServices: [],    // optional: maps to `segmentServices` key
  otherServices: [],      // optional: maps to `additionalServices` key
  categories: [],         // optional: maps to `categories` key
})

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:

  • major
  • minor
  • patch

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.

Readme

Keywords

none

Package Sidebar

Install

npm i @hashicorp/nextjs-scripts

Weekly Downloads

1,548

Version

19.0.3

License

MPL-2.0

Unpacked Size

218 kB

Total Files

134

Last publish

Collaborators

  • abhishek-hashicorp
  • dstaley
  • cameronperera
  • alexju
  • consul-ui-services
  • wenincode-hashicorp
  • hashicb
  • britt.lindgren
  • paulhcp
  • nandereck
  • tstormk
  • hashibot-hds
  • lackeyjb1
  • youriwims
  • jpogran
  • _natmegs
  • thrashr888
  • melsumner
  • mwickett
  • didoo
  • zchsh
  • hcitsec
  • gregone
  • meirish
  • enmod
  • kaxcode
  • anubhavmishra-hashicorp
  • hashibot-web
  • cstitt-hashi
  • kstraut
  • mocohen
  • dhaulagiri