The tiny JSON parser that fixes your CSP headaches
You're building a React app with server-side rendering, and the client needs a little data before it can do anything useful: user preferences, API endpoints, feature flags, that sort of thing.
The usual move is to put it on window:
html<script>window.APP_CONFIG = {userId: 12345,theme: "dark",features: ["new-dashboard", "beta-search"]};</script>
It works, until the security headers arrive.
The CSP problem nobody talks about
Ship that code with a strict Content Security Policy and the browser refuses to run it:
Refused to execute inline script because it violates the followingContent Security Policy directive: "script-src 'self'"
The app boots without the data it expected. That is not a great failure mode.
You could add 'unsafe-inline' to your CSP, but that gives up a lot of the protection you added the policy for. You could generate nonces for every request, but now caching gets harder and the setup gets more complicated.
Meet json-from-script
json-from-script keeps that data in JSON script tags instead of executable inline JavaScript. The browser does not run those tags as code, so a strict CSP can stay strict.
How to use it with plain HTML
Instead of executable JavaScript that triggers CSP violations, use <script type="application/json"> tags. They do not execute code:
html<!DOCTYPE html><html><head><title>My App</title></head><body><div id="app"></div><!-- This is CSP-friendly because it doesn't execute --><script type="application/json" class="data" data-attr="config">{"userId":12345,"theme":"dark","features":["new-dashboard","beta-search"]}</script><script type="application/json" class="data" data-attr="user">{"name":"Alice","email":"[email protected]","verified":true}</script><script src="/bundle.js"></script></body></html>
The data-attr attribute names each JSON block. The library uses those names as keys in the final object, so data-attr="config" becomes the config property.
Reading the data in React
In React, read the data once when the component mounts:
jsximport React, { useState, useEffect } from 'react';import jsonFromScript from 'json-from-script';const App = () => {const [appData, setAppData] = useState(null);useEffect(() => {// This parses all JSON script tags and returns a single objectconst data = jsonFromScript();setAppData(data);}, []);if (!appData) {return <div>Loading...</div>;}return (<div className={`app theme-${appData.config.theme}`}><header><h1>Welcome, {appData.user.name}!</h1>{appData.user.verified && <span className="verified">✓ Verified</span>}</header><main>{appData.config.features.includes('new-dashboard') ? (<NewDashboard userId={appData.config.userId} />) : (<LegacyDashboard userId={appData.config.userId} />)}</main></div>);};
The jsonFromScript() call scans the DOM for all <script class="data"> tags, reads their JSON content, and creates a single object where each property name comes from the data-attr value:
js{config: { userId: 12345, theme: "dark", features: [...] }, // from data-attr="config"}
Why this works
Those <script type="application/json"> tags do not execute, so strict CSP policies leave them alone. You keep the data in the document without turning it into inline JavaScript.
The caching story is calmer too. App logic can live in a normal bundled script, while server-rendered data stays with the HTML that needed it. You are not mixing configuration with executable code just to get it across the hydration boundary.
When the JSON is wrong, you get the same kind of error you would get from JSON.parse(). That makes bad server output easy to catch during development:
jsxuseEffect(() => {try {const data = jsonFromScript();setAppData(data);} catch (error) {console.error('Invalid JSON in script tag:', error);// Handle the error appropriately}}, []);
Customizing the behavior
The default behavior looks for script.data elements and uses data-attr for property names. If your markup already has different conventions, pass a selector and attribute name:
jsx// Look for different script tags with different attributesconst data = jsonFromScript('script.app-config', 'data-key');
This would parse HTML like:
html<script type="application/json" class="app-config" data-key="settings">{"darkMode": true, "language": "en"}</script>
And return:
js{settings: { darkMode: true, language: "en" } // "settings" comes from data-key="settings"}
Next.js-style data loading
Here is how you might use this pattern in a blog application. First, the server renders data as JSON script tags:
html<!-- Server renders this --><script type="application/json" class="data" data-attr="pageProps">{"posts":[{"id":1,"title":"Hello World","excerpt":"My first post"}],"currentUser":{"id":42,"name":"Alice"}}</script><script type="application/json" class="data" data-attr="appConfig">{"apiUrl":"https://api.example.com","version":"2.1.0","features":["comments","sharing"]}</script>
Then the React component parses all the data at once:
jsximport React, { useMemo } from 'react';import jsonFromScript from 'json-from-script';const BlogPage = () => {const { pageProps, appConfig } = useMemo(() => {return jsonFromScript();}, []);return (<div><header><h1>My Blog</h1><p>v{appConfig.version} | Welcome, {pageProps.currentUser.name}!</p></header><main>{pageProps.posts.map(post => (<article key={post.id}><h2>{post.title}</h2><p>{post.excerpt}</p>{appConfig.features.includes('sharing') && (<button>Share this post</button>)}</article>))}</main></div>);};
When this approach shines
This pattern is useful anywhere the page already has the data and the client only needs to pick it up.
For a widget embed, the host page can provide configuration next to the mount point:
html<div id="my-widget"></div><script type="application/json" class="data" data-attr="widget">{"color":"blue","size":"large","showBorder":true}</script>
For progressive enhancement, the server-rendered page can stay in charge of the initial data:
html<!-- Existing server-rendered content --><div class="legacy-content">...</div><!-- New React component with data --><div id="react-component"></div><script type="application/json" class="data" data-attr="componentData">{"items":[...],"settings":{...}}</script>
It also works well for pages with multiple small frontends that need to share the same server-provided configuration.
Getting started
Check out json-from-script on GitHub for installation instructions and documentation.
Then replace executable inline scripts with JSON script tags where the server hands data to the client.
It is a focused tool for one small job.
Closing thoughts
The json-from-script approach is simple on purpose. It uses existing browser behavior and keeps strict CSP policies intact.
Next time inline scripts run into CSP, JSON script tags are worth reaching for.