gatsby-starter-ghost
gatsby-starter-ghost copied to clipboard
Include codeinjection for site and posts
Problem description
codeinjection_head
and codeinjection_foot
for the site as well as for posts is currently not used in the starter. The reason for that is
a) it's a bit tricky with scripts in React (hence our dirty little script here
b) code injection can contain almost anything, but most likely <script>
or <style>
tags, which we need to differentiate for reasons that I'll describe further down
- For site codeinjection we could use a custom
html.js
but would need to have the Ghost settings available at that point already (needs https://github.com/TryGhost/gatsby-starter-ghost/issues/20) - For posts we would need to use React Helmet and can’t edit our
html.js
, as we need the codeinjection per post and not global and we’d prefer to add it to the<head>
section, like we do in Ghost frontend → https://www.gatsbyjs.org/docs/custom-html/#react-helmet - Using React Helmet or
html.js
requires that we know already if it’s a<script>
,<style>
, or any other tag we can think of, as it needs to be specified → https://github.com/nfl/react-helmet#reference-guide - Injecting it the way we inject the body (
dangerouslySetInnerHTML
) of a post doesn’t work either, as it needs to be wrapped in a parent element, which then prevents any of the injected tags from being executed. Furthermore, we would have it in our<body>
instead of the<head>
Proposal
Differentiate the codeinjection
content
One possible solution is to create a parser util that differentiates between <script>
and <style>
tags and returns e. g. an Object with those values like this:
"script": ["<script>...</script>", "<script>...</script>"],
"style": ["<style>...</style>", "<style>...</style>"]
☝️ This is just one idea and open for discussions/suggestions!
Site codeinjection (settings.codeinjection_head
& settings.codeinjection_foot
) in html.js
The site codeinjection can be injected into the html.js
file. 👉 https://www.gatsbyjs.org/docs/custom-html/ but there is - of course - a tricky part here too: the html.js
generation happens before the schema generation and doesn't allow GraphQL queries. This relies therefore on the outcome of https://github.com/TryGhost/gatsby-starter-ghost/issues/20 is implemented, which is supposed to make those Ghost settings available on pre build time.
Post codeinjection (post.codeinjection_head
& post.codeinjection_foot
) in post.js
and page.js
The codeinjection for the <head>
can be solved using React Helmet like described here.
The codeinjection_foot
is not clear where to add it, as the recommended way to inject scripts and styles is using React Helmet. If there's no better way possible to insert the codeinjection_foot
before the closing </body>
tag, it's probably best to use the same implementation as for the head.
Todos
- [x] Write a parser util that differentiates between the HTML tags delivered in code injection
- [x] Find a solution for post
codeinjection_foot
- [x] Implement codeinjection for posts
- [x] Implement codinjection for the site (needs https://github.com/TryGhost/gatsby-starter-ghost/issues/20)
- [ ] properly parse script tags and make available as
codeinjection_scripts
Any news on this ?
Code injection is now working for <style>
tags (both for site and post code injection). I tried to do the same for <script>
, but the html parser was having issues with the content and cut it off. I tested this with a normal Google analytics scripts, which is the most common use case for site code injection.
The code for that parses the content is here.
If anyone has an idea, how to solve this, please let me know, or - even better - submit a PR 😊
@AileenCGN Personally, I have implemented this in my own Fork of this repository. What I have found, as a workaround, is to place the functions themselves inside of the Ghost CMS without <script>
tags. Here is how I have it implemented in the Layout Component.
If you visit my website you can see how it is interpolated.
Thank you @ZionDials for this! This might be a good solution for some users already. The problem is, that we have to cover the case that both, <script>
and <style>
tags are used in codeinjection and your solution would not work in this case. But it would work for users who know exactly that they'll only use scripts in their codeinjection, so thank you very much for sharing this 🤗
I'm wondering how this issue became stale as it seems to be a major feature and therefore I expected this to be needed by many ... but anyhow, I just came to be in need of this too so I implemented it in a way that I think could serve as a possible solution, even while it's still a little bit hacky some might say but actually works like a charm.
First, we build ourselves some kind of function that can extract our wanted script tags from the data delivered by gatsby-source-ghost
. We do this by utilizing a little "hack" that helps us to convert the plain HTML string available under codeinjection_{head|foot} to "real" HTML elements so we can make use of the native HTML functions, queries, etc. Why? Because it is easy, reliable, and dependency-free. Also, it allows us to manipulate the elements in any way we want, for example adding special ids, attributes, classes, and so on:
const extractCodeInjectionElements = (
type: "script" | "style",
scriptString: string
) => {
let scriptElements = []
if (documentGlobal) {
// We create a script wrapper to render the scriptString to real HTML
const scriptWrapper = documentGlobal.createElement("div")
scriptWrapper.innerHTML = scriptString
// Now we can use native HTML queries to get our script|style elements (in this case a
// collection)
const scriptsCollection = scriptWrapper.getElementsByTagName(type)
// Lets map the collection to a "normal" array of script elements
for (let i = 0; i < scriptsCollection.length; i++) {
scriptElements.push(scriptsCollection.item(i))
}
}
// And return them ...
return scriptElements
}
Note that I created that function to work with 2 types of tags, script
and style
. This is because somehow for me, the codeinjection_styles
property is just always undefined, no matter how many style tags I put into the head or foot section of the code injection in my ghost instance. Don't know why this is, maybe @aileen can enlighten me? Anyhow instead of trying to figure out why it doesn't work for me I just wrote the function to work for both:
// Transform the codeinjection string to an array of "real" HTML script elements
const headScriptElements = extractCodeInjectionElements(
"script",
data.ghostPage?.codeinjection_head || ""
)
const footScriptElements = extractCodeInjectionElements(
"script",
data.ghostPage?.codeinjection_foot || ""
)
// As `codeinjection_styles` prop is always undefined, we just extract the style elements
// from head and foot on our own.
const styleElements = extractCodeInjectionElements(
"style",
"".concat(
data.ghostPage?.codeinjection_head || "",
data.ghostPage?.codeinjection_foot || ""
)
)
So that's basically it. Now we just map over the returned elements in our JSX, create the respecting script or style tags and "fill" them with the raw code by accessing the element.innerText
property. This method works both in development, production, and with or without Helmet. The full implementation looks like this:
import { graphql } from "gatsby"
import React from "react"
import { Helmet } from "react-helmet"
import Layout from "../components/Layout"
import { documentGlobal } from "../utils/helpers"
const Page: React.FC<{ data: GatsbyTypes.GhostPageQuery }> = ({ data }) => {
const extractCodeInjectionElements = (
type: "script" | "style",
scriptString: string
) => {
let scriptElements = []
if (documentGlobal) {
// We create a script wrapper to render the scriptString to real HTML
const scriptWrapper = documentGlobal.createElement("div")
scriptWrapper.innerHTML = scriptString
// Now we can use native HTML queries to get our script elements (in this case a
// collection)
const scriptsCollection = scriptWrapper.getElementsByTagName(type)
// Lets map the collection to a "normal" array of script elements
for (let i = 0; i < scriptsCollection.length; i++) {
scriptElements.push(scriptsCollection.item(i))
}
}
// And return them ...
return scriptElements
}
// Transform the codeinjection string to an array of "real" HTML script elements
const headScriptElements = extractCodeInjectionElements(
"script",
data.ghostPage?.codeinjection_head || ""
)
const footScriptElements = extractCodeInjectionElements(
"script",
data.ghostPage?.codeinjection_foot || ""
)
// Same for the style elements
const styleElements = extractCodeInjectionElements(
"style",
"".concat(
data.ghostPage?.codeinjection_head || "",
data.ghostPage?.codeinjection_foot || ""
)
)
return (
<Layout>
<Helmet>
{/* Now we just iterate over the elements and create a script tag for each
of them inside Helmet to exactly replicate our script tags from Ghost
(ghost_head in this case). */}
{headScriptElements.map((script, index) => (
<script
key={`ci_h_${index}`}
type="text/javascript"
src={script?.getAttribute("src") || undefined}
async={script?.hasAttribute("async")}
>{`${script?.innerText}`}</script>
))}
{/* Or, if we want it clean, we just join multiple elements into one. */}
<style type="text/css">{`${styleElements
.map((e) => e?.innerText)
.join("")}`}</style>
</Helmet>
{/* Our normal html from Ghost. */}
<div className="tw-px-6 sm:tw-px-8 lg:tw-px-10 tw-py-8 tw-content-page">
<div
dangerouslySetInnerHTML={{ __html: data.ghostPage?.html || "" }}
></div>
</div>
{/* Finally, add the scripts from the ghost_foot section */}
{footScriptElements.map((script, index) => (
<script
key={`ci_f_${index}`}
type="text/javascript"
src={script?.getAttribute("src") || undefined}
async={script?.hasAttribute("async")}
>{`${script?.innerText}`}</script>
))}
</Layout>
)
}
export default Page
// This page query loads all posts sorted descending by published date
// The `limit` and `skip` values are used for pagination
export const pageQuery = graphql`
query GhostPage($slug: String!) {
ghostPage(slug: { eq: $slug }) {
...GhostPageFields
}
}
`
So there you go, that's my implementation. If you guys like it you could basically implement an improved version of the extractCodeInjectionElements
, maybe inside the @ghost/helpers package or so to provide a unified version. Let me know what you think! 😃
EDIT: forgot to add the src and async attributes to the final head
and foot
script elements
Any updates on this @aileen, is there a particular reason why @jsbrain solution wasn't implemented in Ghost?