Next.js 3rd Party CSS Namespacing


In one of the projects we've been working on at centralsoft.io, we had a very interesting problem to solve. How can we inject 3rd party css in our site and contain it so it only affects the intended content.

The idea of our project is to fetch and render HTML websites or snippets that have been generated using a third party visual web designer / CMS (in this case, Webflow) to our main website that is built in Next.js.

After we set up the fetching and parsing process, the next step was rendering the extracted contents to our website. The content includes:

  • metadata
  • styles (inline and links)
  • scripts (inline and links)
  • HTML snippets

Loading metadata under the <head> tag is straightforward using the Next.js built in <Head> component.
In a similar way we load <script> and <style> tags.

<Head>
  ...
  {metadata && <Metadata metadata={metadata} />}
  {styles && <Styles links={styles.links} inline={styles.inline) />}
  {scripts && <Scripts scripts={scripts} />}
  ...
</Head>

For the actual HTML content, we use React's dangerouslySetInnerHTML something like this:

<div
  className="webflow-wrapper"
  dangerouslySetInnerHTML={{ __html: content }}
/>

This approach works as expected. The metadata is set, content loads, the scripts run fine and the style is applied.

The issue arises when the loaded css clashes with our own custom css we have on the page. If we look into the css files that are being generated by Webflow we can see something like this:

...

.heading {
  ...
  display: -ms-flexbox;
  display: flex;
  font-size: 28px;
  ...
}

.main-content {
  ...
  padding-top: 100px;
  padding-bottom: 100px;
  ...
}

...

Certainly this is an issue, we need to figure out a way so that the third party css loaded to our site doesn't clash with ours.

The solution boils down to downloading the contents of each style link, wrapping them under a scss namespace, compiling it to css and loading it.
This process is quite time consuming, and doing this on each request to a page, definitely is not a good solution.

Nest.js 9.3 https://nextjs.org/blog/next-9-3 brought Static Site Generation compatibilities using the getStaticProps and getStaticPaths functions.

This allows us to namespace (prefix) all the 3rd party stylesheet during build time. The compiled styles will then be loaded inline to the static html file.

  • getStaticProps needs to be exported from a page component (under /pages directory)
export async function getStaticProps (ctx) {
  // ...
  return {
	  props: {
      // ...
    },
  }
}
  • Downloading each stylesheet link
async function downloadStylesheets(stylesheets: string[]): Promise<string[]> {
  const inline = []
  for (const sheet of stylesheets) {
    const res = await fetch(sheet)
    inline.push(await res.text())
  }
  return inline
}
  • Merging all downloaded content under one stylesheet
function merge(blocks: string[]): string {
  let merged = ""
  blocks.map((block) => {
    merged += block
    return block
  })
  return merged
}
  • Wrapping the stylesheet contents under a specific namespace
function wrap(namespace, content) {
  const wrapped = `
    .${namespace} {
        ${content}
    }
  `
  return wrapped
}
  • Compiling the scss to css using node-sass. The reason why we can use node-sass is that we are running Server Side during build time.
const compiler = require("node-sass")

function compile(scss: string): string {
  const css = compiler
    .renderSync({
      data: scss,
      outputStyle: "compressed",
    })
    .css.toString()
  return css
}
  • Finally:
function DynamicPage({ content, style }) {
  return (
	  <>
      <div
        className="webflow-wrapper"
        dangerouslySetInnerHTML={{ __html: content }}
      />
	    {style && <style type="text/css">{style}</style>}
    </>
	)
}

function namespace(namespace: string, blocks: string[]): string {
  const merged = merge(blocks)
  const wrapped = wrap(namespace, merged)
  const compiled = compile(wrapped)
  return compiled
}

export async function getStaticProps (ctx) {
  // ...
  const content = "" // load content
  const extractedStylesheets = await extractStyleshets(content)

  const style = namespace("webflow-wrapper", extractedStylesheets)
  return {
	  props: {
      // ...
      style,
      content
    },
  }
}

export default DynamicPage

Namespacing (prefixing) 3rd party css is a straightforward process. Using SCSS namespaces to wrap all the 3rd party css and compiling it to css is time-consuming work to be repeated after each request to our website.
Next.js SSG (Static Site Generation) allows us to do this process during build time for all of our Static pages.