Created
February 26, 2025 17:36
-
-
Save jcrooke/07aa70cf6b7ce9bace20f1637da4ccd0 to your computer and use it in GitHub Desktop.
Fix Next.js SSG strict CSP error regarding inline script usage
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { resolve } = require('path'); | |
const { createHash } = require('crypto'); | |
const { readFileSync, writeFileSync } = require('fs'); | |
const { globSync } = require('glob'); | |
/** | |
* Script to extract inline scripts from HTML files and externalize them | |
* This helps with CSP (Content Security Policy) compliance by removing inline scripts | |
*/ | |
/** | |
* @typedef {Object} ScriptExtractionResult | |
* @property {string[]} scripts - Array of extracted script contents | |
* @property {string} updatedContent - HTML content with scripts removed | |
*/ | |
/** | |
* Configuration object for script processing | |
*/ | |
const CONFIG = { | |
MAGIC_STRING: '__placeholder_for_inline_scripts__', | |
SCRIPT_REGEX: /<script>(.+?)<\/script>/gs, | |
ASSETS_DIR: 'assets' | |
}; | |
// Set production environment for proper config loading | |
process.env.NODE_ENV = 'production'; | |
const { basePath, distDir } = require('../next.config.js'); | |
/** | |
* Creates a script tag with the proper path | |
* @param {string} hash - Hash of the script content | |
* @returns {string} HTML script tag | |
*/ | |
const createScriptTag = (hash) => { | |
const normalizedBasePath = basePath.endsWith('/') ? basePath : `${basePath}/`; | |
return `<script src="${normalizedBasePath}${CONFIG.ASSETS_DIR}/chunk.${hash}.js" crossorigin=""></script>`; | |
}; | |
/** | |
* Extracts inline scripts from HTML content | |
* @param {string} htmlContent - Raw HTML content | |
* @returns {ScriptExtractionResult} Extracted scripts and updated HTML | |
*/ | |
const extractInlineScripts = (htmlContent) => { | |
const scripts = []; | |
const updatedContent = htmlContent.replace( | |
CONFIG.SCRIPT_REGEX, | |
(_, scriptContent) => { | |
const addMagicString = scripts.length === 0; | |
const normalizedScript = scriptContent.endsWith(';') | |
? scriptContent | |
: `${scriptContent};`; | |
scripts.push(normalizedScript); | |
return addMagicString ? CONFIG.MAGIC_STRING : ''; | |
} | |
); | |
return { scripts, updatedContent }; | |
}; | |
/** | |
* Processes a single HTML file | |
* @param {string} filePath - Path to the HTML file | |
* @param {string} baseDir - Base directory path | |
*/ | |
const processHtmlFile = (filePath, baseDir) => { | |
try { | |
const htmlContent = readFileSync(filePath).toString(); | |
const { scripts, updatedContent } = extractInlineScripts(htmlContent); | |
if (!scripts.length) { | |
console.log(`βοΈ Skipping ${filePath} - No inline scripts found`); | |
return; | |
} | |
console.log(`π¨ Processing ${filePath}`); | |
console.log(` βββ Found ${scripts.length} inline script(s)`); | |
const combinedScripts = scripts.join(''); | |
const hash = createHash('md5').update(combinedScripts).digest('hex'); | |
const outputPath = `${baseDir}/${CONFIG.ASSETS_DIR}/chunk.${hash}.js`; | |
// Ensure we write the external script first | |
writeFileSync(outputPath, combinedScripts); | |
console.log(` βββ Created external script: chunk.${hash}.js`); | |
// Then update the HTML file | |
const finalContent = updatedContent.replace( | |
CONFIG.MAGIC_STRING, | |
createScriptTag(hash) | |
); | |
writeFileSync(filePath, finalContent); | |
console.log(` βββ Updated HTML with external script reference\n`); | |
} catch (error) { | |
console.error(`β Error processing ${filePath}:`, error.message); | |
} | |
}; | |
/** | |
* Main function to process all HTML files | |
*/ | |
const main = () => { | |
try { | |
const baseDir = resolve(distDir.replace(/^\//, '')); | |
const htmlFiles = globSync(`${baseDir}/**/*.html`); | |
console.log('π Starting inline scripts extraction process...'); | |
console.log(`π Found ${htmlFiles.length} HTML files to process\n`); | |
htmlFiles.forEach((file) => processHtmlFile(file, baseDir)); | |
console.log('β Inline scripts extraction completed!'); | |
} catch (error) { | |
console.error('β Fatal error:', error.message); | |
process.exit(1); | |
} | |
}; | |
// Execute the script | |
main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Fix Next.js SSG strict CSP error regarding inline script usage
Issue
You're here because you're encountering a Content Security Policy (CSP) violation due to your webpage trying to execute an inline script that isn't explicitly allowed by your CSP rules.
Out of the box, even in 2025, Next.js Requires
unsafe-inline
in SSG mode, this is because Next.js uses inline<script>
elements for hydration and initial client-side execution.Using
unsafe-inline
in a CSP can be a security risk because it allows inline scripts and styles to execute, making the site more vulnerable to Cross-Site Scripting (XSS) attacks. Even in a statically generated Next.js site, there are potential risks.π¬ The Node.js script in this gist will extract inline scripts from HTML files and externalize them - ensuring CSP compliance by removing inline scripts.
Usage
Add
remove-inline-scripts.js
above to ascripts
directory in your project.Ensure required packages are installed e.g.
npm i glob
Ensure your
next.config.js
contains at least the following attributes;package.json
scripts to include a "postbuild";npm run postbuild
after a build, or ensure your Github workflow runs this command after the build.