Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extracted static stylesheet file into header #2534

Closed
radeno opened this issue Jul 11, 2017 · 13 comments
Closed

Extracted static stylesheet file into header #2534

radeno opened this issue Jul 11, 2017 · 13 comments

Comments

@radeno
Copy link
Contributor

radeno commented Jul 11, 2017

Many issues are trying to solve extracting CSS file as separate link for production env. Eg: #1479 #1396 #1615

There is another solution what is works, and is quite elegant. (maybe isn't 😄 )
This solution works for Next v3 beta and Docker. My source directory is src and build directory is one level up .next. We are using separated style files in /style directory with index.css where are all css files imported.

Required Webpack plugins and loaders

  • extract-text-webpack-plugin
  • css-loader
  • raw-loader
  • postcss-loader

next.config.js

const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  distDir: '../.next',
  webpack: (config, { dev }) => {
    config.module.rules.push({
      test: /\.css$/,
      loader: 'emit-file-loader',
      options: {
        name: 'dist/[path][name].[ext]'
      }
    });

    if (!dev) {
      config.module.rules.push({
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          use: [
            {
              loader: 'css-loader',
              options: {
                importLoaders: 2,
                modules: false,
                url: true,
                sourceMap: false,
                minimize: true,
                localIdentName: false
                  ? '[name]-[local]-[hash:base64:5]'
                  : '[hash:base64:5]'
              }
            },
            { loader: 'postcss-loader' }
          ]
        })
      });

      config.plugins.push(new ExtractTextPlugin('app.css'));
    } else {
      config.module.rules.push({
        test: /\.css$/,
        use: [{ loader: 'raw-loader' }, { loader: 'postcss-loader' }]
      });
    }

    return config;
  }
};

File is emitted in build directory and then is processed with css-loader and postcss-loader. After processing is extracted next to app.js file.
Do not use babel-loader for global as in this example: https://github.com/zeit/next.js/tree/v3-beta/examples/with-global-stylesheet It is not neccessary and doesn't work well with postcss-calc.

Docker

In dockerfile, copy generated app.css to static directory
RUN mkdir src/static/styles && cp .next/app.css src/static/styles/app.css

Header link for production, style for development

pages/_document.js we also used React-Helmet, so this example is

import Document, { Head, Main, NextScript } from 'next/document';
import stylesheet from '../styles/index.css';

export default class extends Document {
  render() {
    return (
      <html>
        <Head>
          {process.env.NODE_ENV == 'production'
            ? <link
                rel="stylesheet"
                type="text/css" 
                href={`/static/styles/app.css?${this.props.__NEXT_DATA__
                  .buildStats['app.js'].hash}`}
              />
            : <style
                global
                dangerouslySetInnerHTML={{
                  __html: stylesheet
                }}
              />}
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </html>
    );
  }
}

Link to app.css is appended with querystring with same hash as is used in app.js path for disabling cache after redeploy.

What we can do better?

Next Server define route for production app.js file (https://github.com/zeit/next.js/blob/v3-beta/server/index.js#L151). This example should be more elegant if we can define route for app.css

Then this could be automated or at least we should have better links for app.css file. Ex:

<link
  rel="stylesheet"
  type="text/css"
  href={`/_next/${this.props.__NEXT_DATA__.buildStats['app.js'].hash}/app.css`}
/>
@radeno radeno changed the title Extracted static stylesheet file for header Extracted static stylesheet file into header Jul 11, 2017
@mocheng
Copy link

mocheng commented Jul 25, 2017

Nice work!

I have one question about non-production env. In your code, stylesheet is imported from ../styles/index.css and set to inline style by dangerouslySetInnerHTML. But, css files are generally spread in each components. If there is a single ``index.cssfile, it is not necessary to utilizeExtractTextPlugin`. Please correct me if my understanding is wrong.

import stylesheet from '../styles/index.css';

<style
   global
   dangerouslySetInnerHTML={{
      __html: stylesheet
    }}
/>

@radeno
Copy link
Contributor Author

radeno commented Jul 25, 2017

We don't use styles in JS components. Every CSS file is separated to own file in directory styles
With lowercase we have general and global styles definitions or external dependencies:

buttons.css
inputs.css
lightgallery.css
....

other copies naming by independent components or composites:

Contact.css
Search.css
Tabs.css
....

I think domain functionalities and design should be separated. Another advantage is we can use CSS variables, no need to use SASS.
Then main index file looks like:

@import "_settings";
@import "global.css";
@import "grid.css";
@import "buttons.css";

@trongthanh
Copy link

I'm also in need of this setup and this is the best solution so far. Below is my next.config.js in which:

  • Styles written in SASS / SCSS
  • Auto-prefixer with postcss (inlining the plugins settings)
  • In dev, I need source map, so I turn on the sourceMap: 'inline' at postcss-loader
  • I haven't test the production outcome, but it's just the idea.
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const autoprefixer = require('autoprefixer');

module.exports = {
    webpack: (config, { dev }) => {
        config.module.rules.push({
            test: /\.s?css$/,
            loader: 'emit-file-loader',
            options: {
                name: 'dist/[path][name].[ext]',
            },
        });

        if (!dev) {
            config.module.rules.push({
                test: /\.scss$/,
                use: ExtractTextPlugin.extract({
                    use: [
                        {
                            loader: 'css-loader',
                            options: {
                                importLoaders: 2,
                                modules: false,
                                url: true,
                                sourceMap: false,
                                minimize: true,
                                localIdentName: false
                                    ? '[name]-[local]-[hash:base64:5]'
                                    : '[hash:base64:5]',
                            },
                        },
                        {
                            loader: 'postcss-loader',
                            options: {
                                sourceMap: true,
                                plugins: () => [
                                    autoprefixer(),
                                ],
                            },
                        },
                        {
                            loader: 'sass-loader',
                            options: {
                                sourceMap: true,
                                includePaths: [
                                    path.resolve(__dirname, 'scss'),
                                    path.resolve(__dirname, 'pages'),
                                ],
                            },
                        },
                    ],
                }),
            });

            config.plugins.push(new ExtractTextPlugin('app.css'));
        } else {
            config.module.rules.push({
                test: /\.scss$/,
                use: [
                    { loader: 'raw-loader' },
                    {
                        loader: 'postcss-loader',
                        options: {
                            sourceMap: 'inline',
                            plugins: () => [
                                autoprefixer(),
                            ],
                        },
                    },
                    {
                        loader: 'sass-loader',
                        options: { sourceMap: true },
                    },
                ],
            });
        }

        return config;
    },
};

@longlivedigital
Copy link

Hi @radeno. This all looks great and is very close to something i am working on. However, in your non-production env, it looks like you don't have hot-reloading. is that correct?

If so, I have an approach that will be able to have hot loading in dev and have a compiled css file in prod (i got that bit from your code above). I am thinking of doing a PR for this in the examples as it seems that there are many people that are asking for this kind of solution.

@radeno
Copy link
Contributor Author

radeno commented Aug 23, 2017

@longlivedigital what do you mean by hot-reloading because next.js has hot-reloader in core.
After changing CSS file whole page is refreshed automatically. Did you mean this or reloading without refreshing whole page?

@pruhstal
Copy link

@longlivedigital any progress here?

@komowicakomo
Copy link

hai! , this is working thanks! , is there a way to import the app.css without copying it first to static directory ?

@radeno
Copy link
Contributor Author

radeno commented Oct 30, 2017

I dont see any way, because default serving routes can't be redefined:
https://github.com/zeit/next.js/blob/9320d9f006164f2e997982ce76e80122049ccb3c/server/index.js#L131

@sheerun
Copy link
Contributor

sheerun commented Dec 13, 2017

I've implemented similar idea in with-extracted-stylesheet example: #3451

It has content-hash filename for extracted CSS file that @radeno suggests, and hot-reloading.

@radeno
Copy link
Contributor Author

radeno commented Dec 13, 2017

@sheerun very very nice, great work!
There could be support native by next.js because for better performance CSS link should be before any Javascript link. But for now is bundled javascript always first because javascript is rendering always first https://github.com/zeit/next.js/blob/canary/server/document.js#L95

Hope next.js will implement native CSS bundling and packing.

@davidqhr
Copy link

davidqhr commented Dec 18, 2017

I can't get NEXT_DATA on server side.. any help?

this.props.__NEXT_DATA__ is null

this.props.__NEXT_DATA__.buildStats['app.js'].hash

@sheerun
Copy link
Contributor

sheerun commented Dec 18, 2017

It is available only on custom document (in pages/_document.js), please see the README

@davidqhr
Copy link

@sheerun Thanks a lot!

@lock lock bot locked as resolved and limited conversation to collaborators Jan 31, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants