GoogleChromeLabs / prerender-loader

📰 Painless universal pre-rendering for Webpack.
https://npm.im/prerender-loader
Apache License 2.0
1.9k stars 50 forks source link

How does this work with apps with routes? #6

Open FezVrasta opened 6 years ago

FezVrasta commented 6 years ago

How can you use this project to pre-render an app that provides different pages at different routes?

developit commented 6 years ago

Since the plugin only does prerendering of a source, the way to go here would be to create an instance of HtmlWebpackPlugin for each route.

You can see how preact-cli does it here: https://github.com/developit/preact-cli/blob/master/src/lib/webpack/render-html-plugin.js

In a nutshell, it's roughly:

const URLS = ['/', '/a', '/b'];
module.exports = {
  // in a webpack config
  plugins: [

  ].concat( URLS.map(url =>
    new HtmlWebpackPlugin({
      filename: url + '/index.html',
      template: '!!prerender-loader?'+encodeURIComponent(JSON.stringify(
        string: true,
        params: { url }
      ))+'!index.html'
    })
  ) )
}
FezVrasta commented 6 years ago

Thanks! So the result would be several HTML files that should be then served somehow by my own http server?

developit commented 6 years ago

yup! each with their own independent static HTML (and initial state, titles, etc that you might have injected during prerendering).

I just amended the example with a name configuration value to HtmlWebpackPlugin to clarify how the files get written to disk.

developit commented 6 years ago

Update: you can now also configure JSDOM to report custom URLs for location.href, etc via the documentUrl loader option.

johnstew commented 6 years ago

This is cool 😎

MikaAK commented 6 years ago

I've tried with multiple HtmlWebpackPlugins and I get a fair amount of errors. Is there a way to tell it to emit one ssr-bundle.js file?

        ERROR in chunk contact [entry]
        ssr-bundle.js
        Conflict: Multiple chunks emit assets to the same filename ssr-bundle.js (chunks 1 and 3)

        ERROR in chunk dangers-of-genservers [entry]
        ssr-bundle.js
        Conflict: Multiple chunks emit assets to the same filename ssr-bundle.js (chunks 1 and 4)

        ERROR in chunk home [entry]
        ssr-bundle.js
        Conflict: Multiple chunks emit assets to the same filename ssr-bundle.js (chunks 1 and 5)

        ERROR in chunk polyfill [entry]
        ssr-bundle.js
        Conflict: Multiple chunks emit assets to the same filename ssr-bundle.js (chunks 1 and 6)

        ERROR in chunk process [entry]
        ssr-bundle.js
        Conflict: Multiple chunks emit assets to the same filename ssr-bundle.js (chunks 1 and 8)

        ERROR in chunk quote [entry]
        ssr-bundle.js
        Conflict: Multiple chunks emit assets to the same filename ssr-bundle.js (chunks 1 and 9)
const getUrlPath = (url) => url.match('[^\/]+$')[0]
const prerenderParams = (url) => encodeURIComponent(JSON.stringify({string: true, params: {url}, documentUrl: getUrlPath(url)}))

new HtmlWebpackPlugin({
  template: `!!prerender-loader?${prerenderParams(url)}!pug-loader!$./index.html`,
  inject: true
  excludeChunks: ...
})
developit commented 5 years ago

Hmm - that wouldn't allow for setting parameters since each has a child build. Perhaps the ssr-bundle could be omitted from the parent compiler's assets..

andybflynn commented 5 years ago

For anyone arriving here after I did: I had to make a few additions and changes to the code above to make route rendering work for me (I'm using react-router-dom)

  1. I didn't have to URI encode the JSON params when inlining the loader:
    
    // webpack.config.js

const urls = ['/', '/about/'];

webpack.plugins = webpack.plugins.concat(urls.map((url) => {
return new HtmlWebpackPlugin({ template: !!prerender-loader?${JSON.stringify({string: true, params: {url}})}!${path.join(__dirname, '/src/index.html')}, filename: path.join(__dirname, /dist${url}index.html), }); }))


2. I had to modify my index.js file to export a StaticRouter rendering so I could pass the url param as the location prop:

```javascript
// index.js

import * as ReactDOM from 'react-dom';
import { BrowserRouter, StaticRouter, Route } from 'react-router-dom';
import HomePage from './pages/home';
import AboutPage from './pages/about';

// This part is run in the browser, using Browser Router

ReactDOM.hydrate(
  <BrowserRouter>
    <React.Fragment>      
      <Route path='/' exact component={HomePage} />
      <Route path='/about/' exact component={AboutPage} />
    </React.Fragment>
  </BrowserRouter>
  , document.getElementById('app')
);

// I had to add the below to get html-webpack-plugin to output the correct markup for each route
// Params from the loader are sent to this function
// Note that this function returns `undefined`

export default (params) => {  
  ReactDOM.render(
    <StaticRouter location={params.url} context={{}}>
      <React.Fragment>      
        <Route path='/' exact component={HomePage} />
        <Route path='/about/' exact component={AboutPage} />
      </React.Fragment>
    </StaticRouter>
    , document.getElementById('app')
  )
};

Hope this helps!

mikefowler commented 5 years ago

@MikaAK were you able to get this working in a Webpack config containing multiple entries? I've run into the same Multiple chunks emit assets to the same filename ssr-bundle.js issues

borisyordanov commented 5 years ago

@andybflynn Can you give us a walk through what we need to do to emulate your setup for my project? Why do you have a slash at the end of /about/ in your urls?

I used the same code for the webpack.config.js:

const urls = ['/', /*and other routers here*/ ];

webpack.plugins = webpack.plugins.concat(urls.map((url) => {
    return new HtmlWebpackPlugin({
        filename: path.join(__dirname, `/dist${url}index.html`),
        template: `!!prerender-loader?${JSON.stringify({string: true, params: {url}})}!${path.join(__dirname, '/src/index.html')}`,
    });
}))

And this is what my index file looks like (i'm using typescript)

import * as React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, StaticRouter } from 'react-router-dom';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

ReactDOM.hydrate(
    <BrowserRouter>
        <App />
    </BrowserRouter>,
    document.getElementById('root') as HTMLElement
);

export default (params: any) => {
    ReactDOM.render(
        <StaticRouter location={params.url} context={{}}>
            <App />
        </StaticRouter>,
        document.getElementById('root')
    );
};

registerServiceWorker();

Starting or building the app (and then serving it) doesn't seem to make any difference in how the app works in this config.

Package versions:

"dependencies": {
        "react": "^16.6.3",
        "react-dom": "^16.6.3",
        "react-router-dom": "^4.3.1"
},
"devDependencies": {
        "prerender-loader": "^1.2.0",
}
andybflynn commented 5 years ago

@borisyordanov The only reason I had the forward slash at the end of /about/ was so that I didn't have to put the slash in the filename, i.e.

filename: path.join(__dirname, `/dist${url}index.html`),

instead of

filename: path.join(__dirname, `/dist${url}/index.html`),

As long as your <App /> component contains the <Route> components that you want to render then your setup appears to be the same as mine. Make sure the <Route> paths match your urls that you are sending in the params.

I didn't have to do anything else to get it working. Adding the default export was the breakthrough moment for me. I'm using the same package versions as you.

edwardfxiao commented 5 years ago

https://github.com/GoogleChromeLabs/prerender-loader/issues/29

edwardfxiao commented 5 years ago

For people who may have the same issue. I have created a successful working example with react and multi-entry(production only though) https://github.com/edwardfhsiao/prerender-loader-test-repo

$npm i
$npm run compile