1

I have different routes in an angular web app, like '/about', '/contact' etc.

After running build:ssr, I run ng run myproject:prerender

This completes and creates folders in /dist/myproject/browser/*. A different folder for each route.

In each folder is an index.html file which is the rendered route HTML. All good so far.

However, in each of the index.html rendered files, the first line looks like this:

<html lang="en"><head><script src="runtime-es2015.c9afb3256f2870e161de.js" type="module"></script><script src="runtime-es5.c9afb3256f2870e161de.js" nomodule="" defer=""></script><script src="polyfills-es5.2878b91c9d9c0a1ee890.js" nomodule="" defer=""></script><script src="polyfills-es2015.4cfd8e3e2fd9d10b9e9e.js" type="module"></script><script src="vendor-es2015.429669b901f4df42a057.js" type="module"></script><script src="vendor-es5.429669b901f4df42a057.js" nomodule="" defer=""></script><script src="main-es2015.a676e1a4bb3cc6e8a80c.js" type="module"></script><script src="main-es5.a676e1a4bb3cc6e8a80c.js" nomodule="" defer=""></script>

This looks fine until you realise that the script src is relative.

So when deployed, lets say https://myproject.com/about would load the rendered index.html from the /about/ folder. However, when loading scripts, because the src is relative, the script paths become: https://myproject.com/about/runtim-es2015....js.

This returns a 404 and crashes the app because the scripts don't exist in that folder. Those scripts are located in the root directory.

Does that make sense?

I created a temporary fix by writing and including the following script in my deployment pipeline, which adds a forward slash to the src paths:

const { readdirSync } = require('fs')
const fs = require('fs')

const getDirectories = source =>
    readdirSync(source, { withFileTypes: true })
        .filter(dirent => dirent.isDirectory())
        .map(dirent => dirent.name)

const loop = function (DirectoryString) {

    const Dirs = getDirectories(DirectoryString);

    if (Dirs.length) {

        Dirs.forEach(folderName => {

            const DirectoryStr = './dist/myproject/browser/' + folderName + '/';

            if (fs.existsSync(DirectoryStr)) {

                console.log(DirectoryStr + 'index.html Fixed');

                loop(DirectoryStr);

                fs.readdir(DirectoryStr, (err, files) => {
                    files.forEach(file => {
                        if (file === "index.html") {

                            const fileName = `${DirectoryStr}index.html`;

                            try {
                                fs.readFile(fileName, 'utf8', function (err, data) {
                                    if (err) {
                                        return console.log(err);
                                    } else {
                                        var result = data.replace(/script src\=\"(?!http)([a-zA-Z])/g, 'script src="/$1');
                                        fs.writeFile(fileName, result, 'utf8', function (err) {
                                            if (err) return console.log(err);
                                        });
                                    }
                                });
                            } catch (e) {
                                console.log(e);
                            }
                        }
                    });
                });

            }

        });

    }

}

loop('./dist/myproject/browser/');

This works, for now, however, I would like a way to fix this in Angular. I couldn't find anyone else with this same problem. Any suggestions?

UPDATE

The issue also happens sometimes in localhost development. It happens if the route is more than one path deep, so to speak.

So the ng server does an automatic reload of localhost:4200/order/step2 (triggered from updating the code), the app doesn't load and I get the error

GET http://localhost:4200/order/step2/runtime.js net::ERR_ABORTED 404 (Not Found)

This is quite annoying for development. The project never used to this, only started recently :(

UPDATE 2

Also, I observe when viewing the source of the localhost rendered page, that the script tags are added outside the document.

From the top, this is the rendered source of the document:

<script src="runtime.js" type="module"></script><script src="polyfills.js" type="module"></script><script src="styles.js" type="module"></script><script src="vendor.js" type="module"></script><script src="main.js" type="module"></script><!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8">

Why on earth would Angular do this?

UPDATE 3

I have <base href="/"> in my document head, but I suspect that because the script tags are being added before the base declaration, and outside the document, that the base is not being applied.

UPDATE 4

Here is a github repo that reproduces the issue:

https://github.com/EmanH/angular-bug-SO-61358153

9
  • Try adding "deployUrl": "/", in the options section of the prerender target of angular.json file Commented Apr 22, 2020 at 7:39
  • @David that didn't work. Failed with the error Schema validation failed with the following errors: Data path "" should NOT have additional properties(deployUrl). Commented Apr 23, 2020 at 1:49
  • Do you have a small project in github where you can reproduce the issue? Commented Apr 27, 2020 at 2:26
  • Sorry, I was wrong about the deployUrl setting, you need to add it to the production configuration of the build target of angular.json file. I tried this and it adds / to the script path in index.html when I run ng run project:prerender:production You can do the same for other configurations if it should apply to other configurations. However I'm not sure why your scripts are added to the top of the page... Commented Apr 27, 2020 at 8:55
  • If you look at the prerendered index.html files in your dist folder, do they also have the scripts on top of the file before the base tag? Dumb question, do you have some code that creates/moves dom elements (appendChild/renderer/...) that could be buggy? Commented Apr 30, 2020 at 16:29

1 Answer 1

3
+100

Index.html in your repo does not have a closing </body> tag. Let's look into sources of angular-cli to understand how Angular adds <script> tags.

// Determine script insertion point
let scriptInsertionPoint;
if (bodyElement.__location && bodyElement.__location.endTag) {
  scriptInsertionPoint = bodyElement.__location.endTag.startOffset;
} else {
  // Less accurate fallback
  // parse5 4.x does not provide locations if malformed html is present
  scriptInsertionPoint = params.inputContent.indexOf('</body>');
}

indexSource.insert(scriptInsertionPoint, scriptElements);

It searches for the position of the closing </body> tag! In your case scripts are inserted at -1 position.

OLD ANSWER

Please check this simple repo https://github.com/dnmakarov/angular-universal-guide. It has SSR configured, so make sure that prerendering works fine there. I've checked - it works fine for me.

The main issue is the place where <script> tags are placed inside index.html. I've checked Angular sources and looks like that this logic has not been changed for months. So please make sure that you use the last versions of angular-cli, @nguniversal/*, and @angular/* packages.

If the issue still exists after that please share at least minimal repository where I can reproduce it. I've worked with Angular Universal a lot and see such an issue for the first time. Thanks.

Sign up to request clarification or add additional context in comments.

2 Comments

I have a very simple Github repository that replicates this issue: github.com/EmanH/angular-bug-SO-61358153 Run ng build and examine the created index.html file.
Ah! What a noob mistake. Thanx.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.