Thursday, March 5, 2020

Parcel building with relative asset paths when using react-router

Leave a Comment

Problem:

You want to create a production build that can be uploaded in any folder, so it will work whether you upload it to site.com or site.com/app.

You use react-router in your app, so you also have to make sure that when you refresh a page, let's say site.com/contact-us it will still load the index.html from the correct location. The solution for this is usually to have a .htaccess file to load index.html whenever we try to load a path that doesn't exist:
RewriteEngine on

# Don't rewrite files or directories
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]

# Rewrite everything else to index.html to allow html5 state links
RewriteRule ^ index.html [L]

Solution:

Step 1: Build parcel with relative asset paths, using --public-url ./:
parcel build src/index.html --no-source-maps --public-url ./

Step 2: The above rewrite only fixes loading the index.html file from the correct location, we still have to make sure all the other assets are loaded relative to your .htaccess file path. To do that, we load any missing file to our target folder:
# If we try to load a missing resource file, load it from folder root
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} ^.*\.(jpg|css|js|gif|png|ico|mp3)$ [NC]
RewriteRule .+?([^\/]*\.*)$ $1 [L]

Step 3: Unforunately so far I don't have a solution for setting a relative path for react-router, so you still have to manually set the public url:
export const browserHistory = createBrowserHistory({
    basename: '/app'
});
This makes sure that when you have a router link to /contact-us it will actually link to /app/contact-us.
The good part is this path will only be referenced once in your build code, so you can easily change it by replacing that string even after the build was done.

Final .htaccess:

Those are the rewrites that make sure your index.html and assets are loaded correctly, even when you move your built app into a different folder.
RewriteEngine on
# Don't rewrite files or directories
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]

# If we try to load a missing resource file, load it from folder root
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} ^.*\.(jpg|css|js|gif|png|ico|mp3)$ [NC]
RewriteRule .+?([^\/]*\.*)$ $1 [L]

# Rewrite everything else to index.html to allow html5 state links
RewriteRule ^ index.html [L]

I am still thinking of a solution for using a relative basename in react-router, so that way links will also be correct when placed in any subfolder or subdomain.
Let me know if you know a way to use a relative basename for browserHistory.

Later edit: A hacky solution for relative basename

I did manage to figure out a dynamic solution for having a relative basename. It's based on the idea that once our app loads, we could detect the base folder based on the current location.pathname.
I know all the possible start values for all my Routes, which in my case are /sites or /settings. Once we know this, we can safely say that anything that is before those strings in the URL is the base path of our app.

const possibleStartPaths = ['/sites', '/settings'];
// Select everything in the pathname that appears before those strings.
// For example /subfolder/myapp/sites returns /subfolder/myapp
const baseMatch = window.location.pathname.match(
    new RegExp(`(.+?)(?:${possibleStartPaths.map((x) => x.replace('/', '\\/')).join('|')})`)
);
const basename = baseMatch ? baseMatch [1] + '/' : window.location.pathname;
export const browserHistory = createBrowserHistory({ basename });

0 comentarii:

Post a Comment