prerender-loader icon indicating copy to clipboard operation
prerender-loader copied to clipboard

How does this work with apps with routes?

Open FezVrasta opened this issue 7 years ago • 13 comments
trafficstars

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

FezVrasta avatar May 31 '18 15:05 FezVrasta

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'
    })
  ) )
}

developit avatar May 31 '18 20:05 developit

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

FezVrasta avatar Jun 01 '18 08:06 FezVrasta

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 avatar Jun 01 '18 15:06 developit

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

developit avatar Sep 04 '18 21:09 developit

This is cool 😎

johnstew avatar Sep 12 '18 14:09 johnstew

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: ...
})

MikaAK avatar Oct 31 '18 06:10 MikaAK

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..

developit avatar Dec 03 '18 19:12 developit

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`),
  });
}))
  1. I had to modify my index.js file to export a StaticRouter rendering so I could pass the url param as the location prop:
// 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!

andybflynn avatar Dec 18 '18 09:12 andybflynn

@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

mikefowler avatar Dec 18 '18 17:12 mikefowler

@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",
}

borisyordanov avatar Dec 19 '18 10:12 borisyordanov

@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.

andybflynn avatar Dec 19 '18 16:12 andybflynn

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

edwardfxiao avatar Feb 13 '19 06:02 edwardfxiao

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 

edwardfxiao avatar Apr 28 '19 02:04 edwardfxiao