Next.js 3rd Party CSS Namespacing
Introduction
Permalink to "Introduction"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.
Problem
Permalink to "Problem"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.
Solution
Permalink to "Solution"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.
Next.js to the rescue
Permalink to "Next.js to the rescue"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.
Detailed Steps
Permalink to "Detailed Steps"getStaticProps
needs to be exported from apage
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
tocss
usingnode-sass
. The reason why we can usenode-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
Conclusion
Permalink to "Conclusion"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.