Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include codeinjection for site and posts #22

Open
4 of 5 tasks
aileen opened this issue Jan 11, 2019 · 6 comments
Open
4 of 5 tasks

Include codeinjection for site and posts #22

aileen opened this issue Jan 11, 2019 · 6 comments
Labels
enhancement Improvement/enhancement of existing features help wanted Community project

Comments

@aileen
Copy link
Member

aileen commented Jan 11, 2019

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 Global site/config values #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 #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

  • Write a parser util that differentiates between the HTML tags delivered in code injection
  • Find a solution for post codeinjection_foot
  • Implement codeinjection for posts
  • Implement codinjection for the site (needs Global site/config values #20)
  • properly parse script tags and make available as codeinjection_scripts
@aileen aileen added the enhancement Improvement/enhancement of existing features label Jan 11, 2019
@ScreamZ
Copy link

ScreamZ commented May 13, 2019

Any news on this ?

aileen added a commit to TryGhost/gatsby-source-ghost that referenced this issue Jun 6, 2019
refs TryGhost/gatsby-starter-ghost#22

- There is no way in Gatsby of injecting code at the end of the `<body>` tag, which means, everything has to go into the `<head>`
- For this reason, we concat all codeinjections for each, posts, pages, and settings (site)
- In Gatsby, we have to use `react-helmet` to inject script and style tags. But those have to be declared separately.
- Because the upstream dependency to parse and select the `<style>` and `<script>` tags doesn't work properly with the `<script>` tags, I only added `<style>` for now.
- Adding a new node field `codeinjection_styles` to posts, pages, and settings, which includes all concatenated style tags from head and foot code injection.
aileen added a commit that referenced this issue Jun 6, 2019
refs #22

- Update [email protected] which includes the new node fiels `codeinjection_styles`
- Inject styles with react-helmet into
    - Layout for site codeinjection
    - Post template for post codeinjection
    - Page template for page codeinjection
@aileen
Copy link
Member Author

aileen commented Jun 10, 2019

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 😊

@ZionDials
Copy link
Contributor

@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.
Code Injection Ghost CMS Header If you visit my website you can see how it is interpolated.
ZionDials Website Code

@aileen
Copy link
Member Author

aileen commented Sep 2, 2019

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 🤗

@aileen aileen added the help wanted Community project label Oct 2, 2019
@jsbrain
Copy link

jsbrain commented Aug 7, 2020

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

@Nevensky
Copy link

Nevensky commented Jun 7, 2021

Any updates on this @aileen, is there a particular reason why @jsbrain solution wasn't implemented in Ghost?

aaromp pushed a commit to aaromp/aaronward.info that referenced this issue Jun 20, 2022
refs TryGhost#22

- Update [email protected] which includes the new node fiels `codeinjection_styles`
- Inject styles with react-helmet into
    - Layout for site codeinjection
    - Post template for post codeinjection
    - Page template for page codeinjection
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Improvement/enhancement of existing features help wanted Community project
Projects
None yet
Development

No branches or pull requests

5 participants