I inherited a React 16 app, built with create-create-app, built and deployed to GitHub Pages with a workflow that watches the main branch. My mission is to move it from Netlify to its new homepage on Squarespace. I'd prefer to avoid big changes, e.g. ejecting CRA, at least until the app is loading on the new homepage.
One of the pain points is the JS/CSS build filenames change due to the hashing behavior described here. I was able to load the app on Squarespace by inserting the following into a code block:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous" />
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous" />
<link rel="stylesheet" href="https://my-org.github.io/my-repo/static/css/main.a8d72baf.css" />
<div id="root">Insert app here</div>
<script src="https://my-org.github.io/my-repo/static/js/main.9ca04874.js" defer></script>
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
The problem with this approach is the site goes down whenever the filenames change. I can update Squarespace rather quickly but it's error prone and tedious. I looked into disabling the hashing behavior but didn't like the solutions I found, e.g. ejecting CRA. Plus I assume there's a good reason for it.
So the first thing I tried was generating the HTML snippet above by extracting the relevant lines from build/index.html. I added the following script after the build step in the workflow:
import { JSDOM } from 'jsdom';
import { open } from 'node:fs/promises';
const inPath = 'build/index.html';
const dom = await JSDOM.fromFile(inPath);
const outPath = 'build/embed-snippet.html';
const outFile = await open(outPath, 'w');
const outStream = outFile.createWriteStream();
const links = dom.window.document.querySelectorAll('link[rel=stylesheet]');
for (const link of links) {
outStream.write(link.outerHTML + '\n');
}
outStream.write('<div id="root">Insert app here</div>\n');
const scripts = dom.window.document.querySelectorAll('script');
for (const script of scripts) {
outStream.write(script.outerHTML + '\n');
}
Then I replaced the contents of the code block on Squarespace:
<div id="root-host">Insert app here</div>
<script>
const url = 'https://my-org.github.io/my-repo/embed-snippet.html';
const response = await fetch(url, {cache: 'no-cache'});
document.getElementById('root-host').innerHTML = await response.text();
</script>
This doesn't work because scripts inserted with innerHTML don't execute.
Next, I modified the script to generate another script instead of the HTML snippet:
import { JSDOM } from 'jsdom';
import { open } from 'node:fs/promises';
const inPath = 'build/index.html';
const document = (await JSDOM.fromFile(inPath)).window.document;
const outPath = 'build/embed.js';
const outFile = await open(outPath, 'w');
const outStream = outFile.createWriteStream();
const links = [...document.querySelectorAll('link[rel=stylesheet]')].map(el => {
return {
href: el.hasAttribute('href') ? el.getAttribute('href') : undefined,
integrity: el.hasAttribute('integrity') ? el.getAttribute('integrity') : undefined,
crossOrigin: el.hasAttribute('crossOrigin') ? el.getAttribute('crossOrigin') : undefined,
};
});
const scripts = [...document.querySelectorAll('script')].map(el => {
return {
src: el.hasAttribute('src') ? el.getAttribute('src') : undefined,
integrity: el.hasAttribute('integrity') ? el.getAttribute('integrity') : undefined,
crossOrigin: el.hasAttribute('crossOrigin') ? el.getAttribute('crossOrigin') : undefined,
defer: el.hasAttribute('defer') ? true : undefined,
async: el.hasAttribute('async') ? true : undefined,
};
});
outStream.write(`const links = ${JSON.stringify(links)};
for (const link of links) {
const el = document.createElement('link');
el.rel = 'stylesheet';
el.href = link.href;
if (link.integrity) el.integrity = link.integrity;
if (link.crossOrigin) el.crossOrigin = link.crossOrigin;
document.head.appendChild(el);
}
const scripts = ${JSON.stringify(scripts)};
for (const script of scripts) {
const el = document.createElement('script');
el.src = script.src;
if (script.integrity) el.integrity = script.integrity;
if (script.crossOrigin) el.crossOrigin = script.crossOrigin;
if (script.defer) el.defer = true;
if (script.async) el.async = true;
document.body.appendChild(el);
}`);
The resulting embed.js looks like this:
const links = [{
"href": "https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css",
"integrity": "sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T",
"crossOrigin": "anonymous"
}, {
"href": "https://use.fontawesome.com/releases/v5.7.2/css/all.css",
"integrity": "sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr",
"crossOrigin": "anonymous"
},
{
"href": "https://my-org.github.io/my-repo/static/css/main.a8d72baf.css"
}];
for (const link of links) {
const el = document.createElement('link');
el.rel = 'stylesheet';
el.href = link.href;
if (link.integrity) el.integrity = link.integrity;
if (link.crossOrigin) el.crossOrigin = link.crossOrigin;
document.head.appendChild(el);
}
const scripts = [{
"src": "https://my-org.github.io/my-repo/static/js/main.9ca04874.js",
"defer": true
}, {
"src": "https://code.jquery.com/jquery-3.3.1.slim.min.js",
"integrity": "sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo",
"crossOrigin": "anonymous"
}, {
"src": "https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js",
"integrity": "sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1",
"crossOrigin": "anonymous"
}, {
"src": "https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js",
"integrity": "sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM",
"crossOrigin": "anonymous"
}];
for (const script of scripts) {
const el = document.createElement('script');
el.src = script.src;
if (script.integrity) el.integrity = script.integrity;
if (script.crossOrigin) el.crossOrigin = script.crossOrigin;
if (script.defer) el.defer = true;
if (script.async) el.async = true;
document.body.appendChild(el);
}
Here's the new contents of the code block on Squarespace:
<div id="root">Insert app here</div>
<script src="https://my-org.github.io/my-repo/embed.js"></script>
The problem now is the app only loads about half the time. The other half of the time I get an error like this:
Uncaught TypeError: Cannot read properties of undefined (reading 'fn')
at util.js:55:5
at bootstrap.min.js:6:200
at bootstrap.min.js:6:246
Uncaught TypeError: window.$(...).tooltip is not a function
at Header.js:18:29
The first error suggests Bootstrap is loading before jQuery, but the scripts are ordered correctly per the BS4 docs.
The second error suggests the React app is loading before Popper. I tried changing the order of the scripts like so:
const scripts = [{
"src": "https://code.jquery.com/jquery-3.3.1.slim.min.js",
"integrity": "sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo",
"crossOrigin": "anonymous"
}, {
"src": "https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js",
"integrity": "sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1",
"crossOrigin": "anonymous"
}, {
"src": "https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js",
"integrity": "sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM",
"crossOrigin": "anonymous"
}, {
"src": "https://my-org.github.io/my-repo/static/js/main.9ca04874.js",
"defer": true
}];
But the problem persists. I also tried adding defer to all the scripts, but that didn't work either. This is really puzzling because according to MDN:
Scripts loaded with the defer attribute will load in the order they appear on the page.
Header.js calls tooltip() inside a useEffect hook:
import React, { useRef, useState , useEffect} from 'react';
const Header = (props) => {
const myRef = useRef(null);
useEffect(() => {
window.$(myRef.current).tooltip();
}, []);
I read somewhere that the hook runs after the DOM is loaded, similar to $(document).ready, and is the right time/place for stuff like this. Even if that isn't true, that wouldn't explain the first error, would it.
I'm about ready to throw my hands up and accept that I (or whoever inherits this project next) have to manually update Squarespace at the exact right moment after each deployment. But there's gotta be a better way, right?
loadevent listener for every script that is about to be loaded in order to force the correct loading order.