Skip to content

Instantly share code, notes, and snippets.

@dsingleton
Last active February 10, 2018 22:53
Show Gist options
  • Save dsingleton/55fe1c13b0706edbf65e5f080c8d2acb to your computer and use it in GitHub Desktop.
Save dsingleton/55fe1c13b0706edbf65e5f080c8d2acb to your computer and use it in GitHub Desktop.
Next.js and CSS Modules

🚨 This setup is highly custom and works based on the specific implementation of the build process in Next.js 4.x. With the release of Next 5.x this setup will not work anymore and will likely be replaceable by a much simpler/vanilla Webpack config.

How Deliveroo are using Next.js and CSS modules.

All our static assets are built by extending the next webpack config, and pushed to a CDN when deploying.

Everything in dist/static gets mounted at /static at the CDN

The build script in package.json looks like this:

next build ./app
next export -o app/dist/export ./app
mv app/dist/export/_next app/dist/
rm -r app/dist/export
  • Our next config extends webpack heavily. We're hoping plugins in Next 5 will replace most of this
  • We include include links to generated CSS files with <PageStylesheet>
  • We're not using client routing yet, so this setup may not work when navigating between pages locally
import React from 'react';
import PropTypes from 'prop-types';
import styles from './styles.scss';
export default function Banner({ title, message }) {
return (
<div className={styles.banner}>
{title && <h3 className={styles.title}>{title}</h3>}
<p className={styles.message}>{message}</p>
</div>
);
}
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const autoprefixer = require('autoprefixer');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const imageNamePattern = '[name].[hash:8].[ext]';
module.exports = {
assetPrefix: process.env.CDN_HOST || '',
poweredByHeader: false,
exportPathMap: () => ({}),
webpack: (config, { buildId, dev }) => {
const id = dev ? 'local' : buildId;
const cdnHost = process.env.CDN_HOST || '';
function getRelativeBuildOutputPath(partPath) {
const absoluteOutputPath = path.resolve(__dirname, `dist/static/${id}/${partPath}`);
return path.relative(config.output.path, absoluteOutputPath);
}
function prefixWithCDN(partPath) {
return `${cdnHost}${partPath}`;
}
const modifiedConfig = {
...config,
module: {
rules: [
...config.module.rules.map((loader) => {
// We only want to modify the babel-loaders so if leave
// all other loaders untouched.
if (loader.loader !== 'babel-loader') {
return loader;
}
return {
...loader,
options: {
...loader.options,
cacheDirectory: false,
plugins: [
...loader.options.plugins || [],
[
'file-loader',
{
name: imageNamePattern,
extensions: ['png', 'jpg', 'gif', 'gif', 'svg'],
publicPath: prefixWithCDN(`/static/${id}/assets`),
// Why is this conditional needed?
outputPath: dev ? `./app/dist/static/${id}/assets` : getRelativeBuildOutputPath('assets/'),
},
],
],
},
};
}),
{
test: /\.scss$/,
loader: 'emit-file-loader',
options: {
name: 'dist/[path][name].[ext]',
},
},
{
test: /\.scss$/,
use: ExtractTextPlugin.extract({
use: [
{
loader: 'css-loader',
options: {
localIdentName: dev
? '[folder]__[local]-[hash:8]'
: '[hash:16]',
modules: true,
alias: {
'./static': path.resolve(__dirname, '../node_modules/@deliveroo/consumer-component-library/static'),
},
},
},
{
loader: 'postcss-loader',
options: {
plugins: [autoprefixer({ browsers: ['last 2 versions'] })],
},
},
{
loader: 'sass-loader',
},
],
}),
},
{
test: /\.(png|jpg|jpeg|svg|gif|ttf)$/,
use: [
{
loader: 'file-loader',
options: {
name: imageNamePattern,
publicPath(url) {
return prefixWithCDN(
url.substring(url.indexOf('dist')).replace('dist', ''),
);
},
outputPath(filename) {
// Why is this conditional needed?
const result = dev ? path.join(__dirname, `./dist/static/${id}/assets/${filename}`) : getRelativeBuildOutputPath(`assets/${filename}`);
return result;
},
},
},
],
},
],
},
plugins: [
...config.plugins,
new ExtractTextPlugin({
filename(getPath) {
return getRelativeBuildOutputPath(getPath('stylesheets/[name].css'));
},
publicPath: '',
allChunks: true,
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(
dev ? 'development' : 'production',
),
'process.env.BUILD_ID': JSON.stringify(id),
'process.env.CDN_HOST': JSON.stringify(cdnHost),
}),
new CleanWebpackPlugin([path.join(__dirname, './dist/')], { verbose: false }),
],
};
return modifiedConfig;
},
};
import PageStylesheet from '../components/PageStylesheet';
class Page extends Component {
static async getInitialProps({ req, res }) {
return {}
}
render() {
return (
<div>
<PageStylesheet pageName="page" />
<h1>Hello</h1>
</div>
}
}
}
import React from 'react';
import PropTypes from 'prop-types';
import Head from 'next/head';
const hasCommons = process.env.NODE_ENV === 'production';
const buildId = process.env.BUILD_ID;
const cdnHost = process.env.CDN_HOST || '';
function getStylesheets(name) {
const tags = [];
// _error.js.css isn't created in prod (styles are in commons), but needed for dev
const isPageInCommons = hasCommons && name === '_error';
if (!isPageInCommons) {
tags.push(
<link
key={name}
rel="stylesheet"
type="text/css"
href={`${cdnHost}/static/${buildId}/stylesheets/bundles/pages/${name}.js.css`}
/>,
);
}
// In production mode, next.js additionally outputs a commons chunk.
// Due to our css setup this means we also get a stylesheet for 'commons'
// which needs to be included.
if (hasCommons) {
tags.push(
<link
key="common"
rel="stylesheet"
type="text/css"
href={`${cdnHost}/static/${buildId}/stylesheets/commons.css`}
/>,
);
}
return tags;
}
const PageStylesheet = ({ pageName }) => (
<Head>{getStylesheets(pageName)}</Head>
);
PageStylesheet.propTypes = {
pageName: PropTypes.string.isRequired,
};
export default PageStylesheet;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment