tizmagik / react-head

⛑ SSR-ready Document Head tag management for React 16+
MIT License
322 stars 34 forks source link

Add support for <Script> and <Static> tags #115

Open cameronbraid opened 3 years ago

cameronbraid commented 3 years ago

I have the need to add <script type="application/ld+json"> tags into my head, so I implemented a <Script> tag with a doc comment saying that this should not be used to inject code.

I also needed a way, in my gatsby based project, to add arbitrary tags to the head from a headless cms. Neither react-helmet or react-head support this use case.

The solution I came up with was based on ideas from https://github.com/remarkablemark/html-react-parser however using html-react-parser added 7.6k to the bundle. However the idea was sound, so I added the <Static> tag to this library based on their ideas. It is a cut down version of html-react-parser's domToReact that is very minimal. It also doesn't provide runtime parsing of the html string - this is to be done before hand. Hence the tag name Static. It only adds ~ 400 bytes to the gzippped bundle.

How do you use it ?

First of all you need to pre-parse the head html string from your headless cms.

example

<link rel="canonical" href="https://www.drivenow.com.au">

needs to become

[{type:'tag',name:'link',attribs:{rel:'canonical',href:'https://www.drivenow.com.au'}}]

for example (using the parseHtmlHeadToStatic function below)

let headJson = parseHtmlHeadToStatic(`<link rel="canonical" href="https://www.drivenow.com.au">`)

Then you use it as follows :

<Static json={headJson}/>

One example of code to parse the html into the correct json structure is as follows. This code can be added to react-head if someone knows how to do that in a tree-shaking safe way. Otherwise it can be added as a separate npm project.


import { StaticNode } from "react-head"
import htmlDomParser from "html-dom-parser"

export function parseHtmlHeadToStatic( head: string ) : StaticNode[] {
  if (!head) return null
  let nodes = htmlDomParser(head)
  nodes = filterTopLevelTextNodes(nodes)
  removeCyclicAndExtraProps(nodes)
  nodes = JSON.parse(JSON.stringify(nodes)) // convert to JSON
  return nodes as StaticNode[]
}
export function filterTopLevelTextNodes(nodes) {
  return nodes.filter(node=>{
    return node.type != 'text'
  })
}

export function removeCyclicAndExtraProps(nodes) {
  nodes.forEach(node=>{
    delete node.parent
    delete node.next
    delete node.prev
    delete node.endIndex
    delete node.startIndex
    if (node.children) {
      if (node.children.length == 0) {
        delete node.children
      }
      else {
        removeCyclicAndExtraProps(node.children)
      }
    }
  })
}