I have this custom script that from a third party I am trying to embed into React Helmet
Usually the scripts are given only as a src url. If I am given the code below, what is the best practice to embed the embed the script with React Helmet
<script>
document.getElementsByTagName('head')[0].appendChild(function(s){
var d=document,m2g=d.createElement('script'),l=function(){Mobi2Go.load(s.container,s.ready);},jq=window.jQuery&&+window.jQuery.fn.jquery.replace(/^(\d+).*$/,'$1')===1&&+window.jQuery.fn.jquery.replace(/^\d+\.(\d+).*$/,'$1')>=7,qs=window.location.search.substring(1),re='=(.*?)(?:;|$)',c=d.cookie.match('MOBI2GO_SESSIONID'+re),w=window.innerWidth;
m2g.src='https://www.mobi2go.com/store/embed/1990-v8oL.js?'+qs+(jq?'&no_jquery':'')+(c?'&s='+c[1]:'')+'&device_width='+w;
if(m2g.onload!==undefined)m2g.onload=l;else m2g.onreadystatechange=function(){if(m2g.readyState!=='loaded'&&m2g.readyState!=='complete')return;m2g.onreadystatechange=null;l();}
window.Mobi2Go_est = +(new Date);
return m2g;
}({
container: 'Mobi2Go-Storefront', // Replace with ID of the element to inject UI into
ready: function() {} // Callback to fire when app is ready
}));
</script>
There is obviously errors if I include all of that in the React Helmet
Not sure what is the best practice to make it work.
I have also tried placing it in the html.js file in gatsby
import React from 'react';
import PropTypes from 'prop-types';
export default function HTML(props) {
return (
<html {...props.htmlAttributes}>
<head>
<meta charSet="utf-8" />
<meta httpEquiv="x-ua-compatible" content="ie=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
{props.headComponents}
</head>
<body {...props.bodyAttributes}>
{props.preBodyComponents}
<div
key={`body`}
id="___gatsby"
dangerouslySetInnerHTML={{ __html: props.body }}
/>
{props.postBodyComponents}
</body>
<script
dangerouslySetInnerHTML={{
__html: `
document.getElementsByTagName('head')[0].appendChild(function(s){
var d=document,m2g=d.createElement('script'),l=function(){Mobi2Go.load(s.container,s.ready);},jq=window.jQuery&&+window.jQuery.fn.jquery.replace(/^(\d+).*$/,'$1')===1&&+window.jQuery.fn.jquery.replace(/^\d+\.(\d+).*$/,'$1')>=7,qs=window.location.search.substring(1),re='=(.*?)(?:;|$)',c=d.cookie.match('MOBI2GO_SESSIONID'+re),w=window.innerWidth;
m2g.src='https://www.mobi2go.com/store/embed/1990-v8oL.js?'+qs+(jq?'&no_jquery':'')+(c?'&s='+c[1]:'')+'&device_width='+w;
if(m2g.onload!==undefined)m2g.onload=l;else m2g.onreadystatechange=function(){if(m2g.readyState!=='loaded'&&m2g.readyState!=='complete')return;m2g.onreadystatechange=null;l();}
window.Mobi2Go_est = +(new Date);
return m2g;
}({
container: 'Mobi2Go-Storefront', // Replace with ID of the element to inject UI into
ready: function() {} // Callback to fire when app is ready
})););
`,
}}
/>
</html>
);
}
HTML.propTypes = {
htmlAttributes: PropTypes.object,
headComponents: PropTypes.array,
bodyAttributes: PropTypes.object,
preBodyComponents: PropTypes.array,
body: PropTypes.string,
postBodyComponents: PropTypes.array,
};
Use the script tag inside the helmet tag. Most important your script should me inside the curly braces
<Helmet>
<script>
{`alert( 'Hello, world!' );`}
</script>
</Helmet>
It should work using react-helmet, there are examples of it in the README.
You might be missing type="text/javascript" in your script tag:
<script type="text/javascript">
// your script goes here
</script>
Al alternative option if this doesn't work for you is to customize Gatsby's html.js.
Related
I am struggling to collect custom Simple Analytics metadata in my Next.js app. Looking at their docs, I can either set metadata on the window object (link) or add it via a callback function (link).
My Next.js app looks as follows:
_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document'
export default class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head>
[...snip...]
<script dangerouslySetInnerHTML={{
__html: `window.sa_event=window.sa_event||function(){a=[].slice.call(arguments);sa_event.q?sa_event.q.push(a):sa_event.q=[a]};`
}}/>
</Head>
<body>
<Main />
<NextScript />
// NOTE 3: Where can I define `func` so I have access to the router/query string params?
<script async defer data-metadata-collector="func" src="https://scripts.simpleanalyticscdn.com/latest.js"></script>
<noscript><img src="https://queue.simpleanalyticscdn.com/noscript.gif" alt=""/></noscript>
</body>
</Html>
)
}
}
page.js
import Head from 'next/head'
import { useRouter } from 'next/router'
import Layout from '../components/layout'
export default function Page() {
const router = useRouter()
const i = router.query.i
return (
<>
<Head>
[...snip...]
</Head>
// NOTE 1: This does not work
<script>
sa_metadata = { i: i }
</script>
// NOTE 2: I cannot access `i` here
<script dangerouslySetInnerHTML={{
__html: `window.sa_metadata={ i: i };`
}}/>
[...snip...]
</>
)
}
As you can see, I tried two ways of setting metadata on window (NOTES 1 and 2) and I got stuck on the callback function (NOTE 3). Would appreciate any help in moving this forward.
I'm trying to add a clucth.co widget to a Gatsby site, but it does not render. I've tried using react Helmet for the <script> part, but it still does not work.
Hopefully I'm missing something simple here, but looking at other solutions I can't find anything that works.
For reference: https://clutch.co/content/add-review-widget-your-website
<script type="text/javascript" src="https://widget.clutch.co/static/js/widget.js"></script>
<div className="clutch-widget" data-url="https://widget.clutch.co" data-widget-type="7" data-height="65" data-clutchcompany-id="XXXXXXX"></div>
You have multiple ways of inserting a third-party script in Gatsby. The problem you'll face in all of them is that you need to await that your div:
<div className="clutch-widget" data-url="https://widget.clutch.co" data-widget-type="7" data-height="65" data-clutchcompany-id="XXXXXXX"></div>
Needs to be rendered your script won't be able to load.
Using Script component (2022 update)
Since the release of the Script Gatsby component (powered by Partytown) it's much easier adding third-party scripts. Just:
import React from "react"
import { Script } from "gatsby"
function YourPage() {
return <Script src="https://my-example-script" />
}
export default YourPage
Using Helmet:
You said you already tried but it should. You may need to try the drop-in support that adds the gatsby-plugin-react-helmet. Then:
<Layout>
<SEO title="Live" />
<Helmet>
<script src="https://tlk.io/embed.js" type="text/javascript"/>
</Helmet>
</Layout>
Check the compatibility issues when used with hooks.
Using onRenderBody API from gatsby-ssr.js:
Gatsby exposes a setHeadComponents function in the onRenderBodyAPI that you can take advantage from:
import React from "react"
export const onRenderBody = ({ setHeadComponents }, pluginOptions) => {
setHeadComponents([
<script key="tracking"
src="https://widget.clutch.co/static/js/widget.js
type="text/javascript"
async
/>,
])
}
This snippet above will insert the <script> in the <head> tag of the compiled HTML.
Here you have another approach using dangerouslySetInnerHTML:
setHeadComponents([
<script dangerouslySetInnerHTML={{whateveryouneedtoset}}>
])
Extracted from Unable to Inject 3rd Party Scripts in Gatsby
Modifying directly the html.js:
You can customize even more the output of the resultant HTML by modifying the html.js, the boilerplate that uses Gatsby to build your entire site.
Run:
cp .cache/default-html.js src/html.js
Or alternatively, copy the default-html.js from .cache folder into /src and rename it to html.js. When compiling, if the html.js is present, Gatsby will take it to build your site based on that skeleton.
You'll have something like:
import React from "react"
import PropTypes from "prop-types"
export default function HTML(props) {
return (
<html {...props.htmlAttributes}>
<head>
<meta charSet="utf-8" />
<meta httpEquiv="x-ua-compatible" content="ie=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
{props.headComponents}
</head>
<body {...props.bodyAttributes}>
{props.preBodyComponents}
<div
key={`body`}
id="___gatsby"
dangerouslySetInnerHTML={{ __html: props.body }}
/>
{props.postBodyComponents}
</body>
</html>
)
}
HTML.propTypes = {
htmlAttributes: PropTypes.object,
headComponents: PropTypes.array,
bodyAttributes: PropTypes.object,
preBodyComponents: PropTypes.array,
body: PropTypes.string,
postBodyComponents: PropTypes.array,
}
There you can add your <script> directly:
<head>
<meta charSet="utf-8" />
<meta httpEquiv="x-ua-compatible" content="ie=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<script type="text/javascript" src="https://widget.clutch.co/static/js/widget.js"></script>
{props.headComponents}
</head>
Using gatsby-plugin-load-script:
Just install and use the plugin:
{
resolve: 'gatsby-plugin-load-script',
options: {
src: 'https://widget.clutch.co/static/js/widget.js',
},
},
Hacking the gatsby-browser.js API:
If none of the above fits you, you can still use one of gatsby-browser.js APIs (onClientEntry) to manually add script given a source URL:
const addScript = url => {
const script = document.createElement("script")
script.src = url
script.async = true
document.body.appendChild(script)
}
export const onClientEntry = () => {
window.onload = () => {
addScript("https://widget.clutch.co/static/js/widget.js")
}
}
In order to not have the clutch widget disappear on route changes, I ended up running the Init and Destroy methods from window.CLUTCHCO myself in useEffect.
React.useEffect(() => {
// add widget to end of body and run it
const script = document.createElement("script")
script.type = "text/javascript"
script.src = "https://widget.clutch.co/static/js/widget.js"
script.async = true
document.body.appendChild(script)
// run script
script.onload = () => {
// #ts-expect-error Apparently we have to manually do this!! 🗑️
// eslint-disable-next-line #typescript-eslint/no-unsafe-member-access, #typescript-eslint/no-unsafe-call
window.CLUTCHCO.Init()
}
return () => {
// #ts-expect-error Apparently we have to manually do this!! 🗑️
// eslint-disable-next-line #typescript-eslint/no-unsafe-member-access, #typescript-eslint/no-unsafe-call
window.CLUTCHCO.Destroy()
document.body.removeChild(script)
}
}, [])
Created a react app and then converted it into a single spa react app using
https://www.youtube.com/watch?v=W8oaySHuj3Y
When a hit is made to http://localhost:8080/org-app.js I get a response of the javascript files.
Also when http://single-spa-playground.org/playground/instant-test?name=#org/app&url=8080 the app loads.
However now trying to import the same app in an html page does not replaces the tag.However, it loads the component know this because of the api calls being made and redux store being loaded.
Have not done singleSpa.registerApplication even if I do it is it necessary a root component needs to be made to register the application.
org-app.js
import React from "react";
import ReactDOM from "react-dom";
import singleSpaReact from "single-spa-react";
import Root from "./root.component";
import il8n from "./i18n";
const domElementGetter = () => {
let el = document.getElementById("example-app");
if (!el) {
el = document.createElement("div");
el.id = "example-app";
document.body.appendChild(el);
}
return el;
};
const lifecycles = singleSpaReact({
React,
ReactDOM,
il8n,
rootComponent: Root,
errorBoundary(err, info, props) {
// Customize the root error boundary for your microfrontend here.
return null;
},
domElementGetter,
});
export const { bootstrap, mount, unmount } = lifecycles;
TestPage.html Directly opened
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<script src="https://cdn.jsdelivr.net/npm/regenerator-runtime#0.13.5/runtime.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/import-map-overrides#2.3.0/dist/import-map-overrides.js"></script>
<script src="https://cdn.jsdelivr.net/npm/systemjs#6.8.3/dist/system.js"></script>
<script src="https://cdn.jsdelivr.net/npm/systemjs#6.8.3/dist/extras/amd.js"></script>
<meta name="importmap-type" content="systemjs-importmap" />
<script type="systemjs-importmap">
{
"imports": {
"single-spa": "https://cdn.jsdelivr.net/npm/single-spa#5.9.0/lib/system/single-spa.min.js",
"react": "https://cdn.jsdelivr.net/npm/react#16.13.1/umd/react.production.min.js",
"react-dom": "https://cdn.jsdelivr.net/npm/react-dom#16.13.1/umd/react-dom.production.min.js",
"rxjs": "https://cdn.jsdelivr.net/npm/#esm-bundle/rxjs/system/es2015/rxjs.min.js",
"rxjs/operators": "https://cdn.jsdelivr.net/npm/#esm-bundle/rxjs/system/es2015/rxjs-operators.min.js"
}
}
</script>
<script type="systemjs-importmap">
{
"imports": {
"#example/app": "http://localhost:8080/org-app.js"
}
}
</script>
</head>
<body>
<script>
System.import("#example/app");
</script>
<div id="example-app"></div>
<h1></h1>
<p>My first paragraph.</p>
<import-map-overrides-full
show-when-local-storage="devtools"
dev-libs
></import-map-overrides-full>
</body>
</html>
The problem here is that you're trying to bypass single-spa entirely. The root config should be where the applications get registered which creates the association between routes and applications and dictates when they will be mounted/unmounted. Simply calling System.import("#example/app"); is not enough because the applications do not manage their own lifecycles. Instead you could do something like this:
System.import("single-spa").then(({ registerApplication, start }) => {
registerApplication({
name: "#example/app",
app: () => System.import("#example/app"),
activeWhen: ["/"],
});
start({
urlRerouteOnly: true,
});
});
I cannot see a benefit do doing it this way over what create-single-spa provides.
Lastly, it seems that you're trying to do this to control where the applications are being mounted to. There are two ways to do this with what single-spa already provides:
Using single-spa-layout you can simply create all the intermediate DOM nodes
Use the domElementGetter option of the corresponding framework helpers to designate where the application should mount to.
Setup
I've initialized a new project using vite on an Arch based operating system.
When I try to create the simple counter from the vue docs, the elemet doesn't render.
Code
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="counter">
Counter: {{ counter }}
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
main.js
import { createApp } from 'vue'
var CounterApp = {
data() {
return {
counter: 0
}
},
mounted() {
setInterval(() => {
this.counter++
}, 1000)
}
}
createApp(CounterApp).mount('#counter')
When I inspect the element it is commented out:
Question
Why is that? And how to resolve the error?
Doing that replaces the normal mounting process, and treats the root element like a template string for the App component. Since template strings require the runtime compiler, you would need to use a full build. There should be a console warning about that.
To avoid increasing the size of your app (by ~30%) with the full build, it's recommended to leave the mounting point untouched, and give the App component its own proper template:
index.html
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
App.vue
<template>
<div id="counter">
Counter: {{ counter }}
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
counter: 0
}
},
mounted() {
setInterval(() => {
this.counter++
}, 1000)
}
}
</script>
By default the runtime compiler is not included in the Vue build.
To include it, add the following resolve.alias configuration:
vite.config.js
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
vue: 'vue/dist/vue.esm-bundler.js',
},
},
})
Docs https://vitejs.dev/config/#resolve-alias
I am trying to use this booking widget from timekit.io https://developers.timekit.io/docs/booking-widget-v2#getting-started It works in development in Gatsby but the build fails with error WebpackError: Cannot find module 'jQuery' I have tried the reccomendations here to no avail https://www.gatsbyjs.org/docs/debugging-html-builds/
It's a shame this widget relies on jquery. I am seriously considering just using the api manually and building the interaction, but for the client this was the best cost solution.
Thanks ahead of time.
Here is my code thus far
//gatsby-node.js
exports.onCreateWebpackConfig = ({ actions }) => {
const { setWebpackConfig } = actions
setWebpackConfig({
externals: {
jquery: "jQuery", // important: 'Q' capitalized
},
})
}
//gatsby-ssr.js
const React = require("react")
export const onRenderBody = ({ setHeadComponents }, pluginOptions) => {
setHeadComponents([
<script
src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
crossOrigin="anonymous"
></script>,
])
}
//html.js
import React from "react"
import PropTypes from "prop-types"
export default function HTML(props) {
return (
<html {...props.htmlAttributes}>
<head>
<meta charSet="utf-8" />
<meta httpEquiv="x-ua-compatible" content="ie=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<script
src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossOrigin="anonymous"
/>
{props.headComponents}
</head>
<body {...props.bodyAttributes}>
{props.preBodyComponents}
<div
key={`body`}
id="___gatsby"
dangerouslySetInnerHTML={{ __html: props.body }}
/>
{props.postBodyComponents}
</body>
</html>
)
}
HTML.propTypes = {
htmlAttributes: PropTypes.object,
headComponents: PropTypes.array,
bodyAttributes: PropTypes.object,
preBodyComponents: PropTypes.array,
body: PropTypes.string,
postBodyComponents: PropTypes.array,
}
//index.js
import React, { useEffect } from "react"
import $ from "jquery"
import TimekitBooking from "timekit-booking"
const IndexPage = () => {
useEffect(() => {
const widget = new TimekitBooking()
console.log(widget)
const config = {
app_key: "fdsf",
project_id: "fdsa",
}
widget.init(config)
})
return (
<>
<div id="bookingjs"></div>
</>
)
}
export default IndexPage