Razzle: How do I troubleshoot server rendering?

Created on 5 Feb 2019  ·  9Comments  ·  Source: jaredpalmer/razzle

Hello,

We've noticed a problem where if you log in to Google's Search console, and fetch as Google, the website is rendered as a blank page.

I've done some research, and some of it lead me to this article:

https://medium.com/@gajus/react-application-seen-as-a-blank-page-via-fetch-as-google-afb11dff8562

It's very likely the search engine, and fetch as google itself don't behave the same way, or else we would have lost rankings by now, nevertheless I would like to resolve this issue, as it would keep any website owner awake.

I have downloaded phantomjs, with the assumption it's the same as fetch as Google, and trying to render it returns a number of errors:

image

This problem may be as simple to resolve as changing the target browsers being compiled, however simply installing babel/polyfil and importing doesn't work.

Would anyone be able to point me in the right direction here?

stale

Most helpful comment

So my suggestion (and maybe we need a troubleshooting guide in the docs about this) is to comment out as much as you can from server.js, client.js , App.js, and potentially aspects of your webpack overrides (if you have any ) to get your Razzle application back to a "hello world" like state. I would also suggest copying Razzle's HTML template back into your server.js file just to be sure that all the scripts are loaded properly out of the box. Once it's running, open up Chrome dev tools and go to the "Performance" tab, check the "Screenshots" checkbox, then press the Refresh icon. See the GIF below.

kapture 2019-02-05 at 8 33 42

The goal is to make sure that there is no blank white frame in the timeline area. After you get a proper render with no flashes, you should add back more and more of your old code. Take baby steps. This is quite easy to mess up if you are not careful.

As for your specific case, I would start with removing all that google tag manager stuff, all of your CSS, as well as whatever code-splitting you are doing. Just get it to render and work backwards. Next, get Redux working. Then add back react-helmet, and then, once you think you got it right, double check by doing a Lighthouse test as well (Audits tab). Commit this state to a new branch so you can go back to it in the future. Next, add back your analytics scripts and check rendering again. Finally, attempt to add back code-splitting.

Note: If you keep your css and do the above analysis with Razzle in development mode, you may get a FOUC because Razzle does not process .css styles on the server during development (just on the client). Thus, to fully test that you are doing proper SSR with style hydration if you are using .css files, you should build Razzle for production and run the production server locally. However, if you are simply checking for the presence of your HTML from the server, you can run this in development mode.

All 9 comments

If you’re page is fetching blank then you have broken your SSR or hydration scheme. Razzle does not suffer from what you’ve described out of the box, in fact, it is used on SEO-focused sites like BBC.com. can you paste your server.js file? Are you fetching your data on the sever?

Hi Jared,

Thanks for the prompt reply, yes we do fetch data from a server.

Here is my server.js

import App from './App';
import React from 'react';
import Helmet from 'react-helmet';
import { StaticRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import express from 'express';
import compression from 'compression';
import { renderToString } from 'react-dom/server';

import configureStore from './store/configureStore';
import { remoteLoader } from './api/remoteLoader';
import serialize from 'serialize-javascript';

import { Capture } from 'react-loadable';
import { getBundles } from 'react-loadable/webpack';
import stats from '../build/react-loadable.json';

import { IS_PRODUCTION } from './components/shared/constants';

const assets = require(process.env.RAZZLE_ASSETS_MANIFEST);

const server = express();
server.use(compression());

if (!IS_PRODUCTION) {
    server.set('cache', false);
}

server
    .disable('x-powered-by')
    .use(express.static(process.env.RAZZLE_PUBLIC_DIR, { Expires: '30d' }))
    .get('/*', (req, res) => {
        //Ensures clients with old css paths are served the current file
        if (req.path.indexOf('/static/css') > -1 && assets.client.css) {
            const currentCssFile = `${process.env.RAZZLE_PUBLIC_DIR}${assets.client.css}`;
            return res.sendFile(currentCssFile);
        }

        remoteLoader(apiResult => {
            const responseCode = typeof apiResult.status === 'undefined' ? 404 : apiResult.status;

            if (responseCode === 301) {
                return res.redirect(responseCode, apiResult.headers.location);
            }

            // Compile an initial state
            const initialState = {
                remote: {
                    cms: {
                        result: apiResult ? apiResult.data : false,
                        loading: false
                    },
                    myDrewberry: {
                        searchResults: null,
                        loading: false,
                        failed: false
                    }
                }
            };
            // Create a new Redux store instance
            const store = configureStore(initialState);

            const context = {};
            const modules = [];
            const markup = renderToString(
                <Capture report={moduleName => modules.push(moduleName)}>
                    <StaticRouter context={context} location={req.url}>
                        <Provider store={store}>
                            <App />
                        </Provider>
                    </StaticRouter>
                </Capture>
            );
            const helmet = Helmet.renderStatic();

            if (context.url) {
                res.redirect(context.url);
            } else {
                const bundles = getBundles(stats, modules);
                const chunks = bundles.filter(bundle => bundle.file.endsWith('.js'));

                res.status(responseCode).send(
                    `<!doctype html>
                    <html ${helmet.htmlAttributes.toString()}>
                    <head>

                        ${assets.client.css ? `<link rel="stylesheet" href="${assets.client.css}">` : ''}
                        ${helmet.title.toString()}
                        ${helmet.meta.toString()}
                        ${helmet.link.toString()}
                        <link rel="icon" type="image/png" href="/favicon16.png" sizes="16x16"/>
                        <link rel="icon" type="image/png" href="/favicon32.png" sizes="32x32"/>
                        <link rel="icon" type="image/png" href="/favicon96.png" sizes="96x96"/>
                        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/> <!--320-->
                        <meta http-equiv="expires" content="0">
                    </head>
                    <body class="drewberry-preload">

                        <!-- Google Tag Manager -->
                            <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
                            new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
                            j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
                            'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
                            })(window,document,'script','dataLayer','GTM-TKPXWB');</script>
                        <!-- End Google Tag Manager -->

                        <div id="root">${markup}</div>
                        <script>
                            window.__PRELOADED_STATE__ = ${serialize(initialState)}
                        </script>
                        ${
                            IS_PRODUCTION
                                ? `<script src="${assets.client.js}"></script>`
                                : `<script src="${assets.client.js}" crossorigin></script>`
                        }
                          ${chunks
                              .map(chunk =>
                                  IS_PRODUCTION
                                      ? `<script src="/${chunk.file}"></script>`
                                      : `<script src="http://${process.env.HOST}:${parseInt(process.env.PORT, 10) + 1}/${
                                            chunk.file
                                        }"></script>`
                              )
                              .join('\n')}
                          <script>window.main();</script>
                    </body>
                </html>`
                );
            }
        }, req.path);
    });

export default server;

I can confirm that out of the box Razzle doesn't suffer from it, just loaded a clean install and using phantomjs none of the warning showed up, which means it's probably something some library we installed.

Do you see anything obvious in the above?

It feels like a wild goose chase at the moment! :-)

So my suggestion (and maybe we need a troubleshooting guide in the docs about this) is to comment out as much as you can from server.js, client.js , App.js, and potentially aspects of your webpack overrides (if you have any ) to get your Razzle application back to a "hello world" like state. I would also suggest copying Razzle's HTML template back into your server.js file just to be sure that all the scripts are loaded properly out of the box. Once it's running, open up Chrome dev tools and go to the "Performance" tab, check the "Screenshots" checkbox, then press the Refresh icon. See the GIF below.

kapture 2019-02-05 at 8 33 42

The goal is to make sure that there is no blank white frame in the timeline area. After you get a proper render with no flashes, you should add back more and more of your old code. Take baby steps. This is quite easy to mess up if you are not careful.

As for your specific case, I would start with removing all that google tag manager stuff, all of your CSS, as well as whatever code-splitting you are doing. Just get it to render and work backwards. Next, get Redux working. Then add back react-helmet, and then, once you think you got it right, double check by doing a Lighthouse test as well (Audits tab). Commit this state to a new branch so you can go back to it in the future. Next, add back your analytics scripts and check rendering again. Finally, attempt to add back code-splitting.

Note: If you keep your css and do the above analysis with Razzle in development mode, you may get a FOUC because Razzle does not process .css styles on the server during development (just on the client). Thus, to fully test that you are doing proper SSR with style hydration if you are using .css files, you should build Razzle for production and run the production server locally. However, if you are simply checking for the presence of your HTML from the server, you can run this in development mode.

Ok, thanks for the awesome work with Razzle, and now for this. I'm going to Audit it nicely and report back.

Of course, the first thing to check on any "empty" (not blank, that's an error ) seo page, is: does your page depend on componentDidMount to load data to be displayed. You can use curl to check quickly. If yes, you need something like After.js to preload.

could you advice how to debug using the Performance tab, if there is a blank page around the 1000ms mark? I know my issue is around an icon font library not getting loaded on server and thus it shows these blank empty squares at initial load...

screen shot 2019-02-11 at 9 57 30 am

Ah, all of my custom fonts are not getting loaded until much later. I'm loading my fonts through via CSS' @font-face {}.

@font-face {
  font-family: 'SFProText';
  font-display: auto;
  src: url('fonts/SFPro/SF-Pro-Display-Regular.otf') format('truetype');
  font-weight: normal;
  font-style: normal;
}

@jaredpalmer I too have the same issue. Fonts and assets are not loaded in my client page.

I have not added any webpack.config file in my application setup, which i cloned from your repo. I just taken the copy from your repo and installed npm modules , then i started using the application by running a npm start command.
anything i missed to configure or must use my new webpack file to solve this issue?

Please help me to resolve this issue.

My App.css file looks like this..
@font-face {
font-family: 'Roboto-medium';
src: url("./static/fonts/Roboto-Medium-webfont.eot");
src: url("./static/fonts/Roboto-Medium-webfont.eot?#iefix") format("embedded-opentype"), url("./static/fonts/Roboto-Medium-webfont.woff") format("woff"), url("./static/fonts/Roboto-Medium-webfont.ttf") format("truetype"); }

@font-face {
font-family: 'Roboto-regular';
src: url("./static/fonts/roboto-regular-webfont.eot");
src: url("./static/fonts/roboto-regular-webfont.eot?#iefix") format("embedded-opentype"), url("./static/fonts/roboto-regular-webfont.woff") format("woff"), url("./static/fonts/roboto-regular-webfont.ttf") format("truetype"); }

thanks inadvane :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jcblw picture jcblw  ·  4Comments

GouthamKD picture GouthamKD  ·  3Comments

panbanda picture panbanda  ·  5Comments

Jayphen picture Jayphen  ·  4Comments

corydeppen picture corydeppen  ·  3Comments