Skip to content

Instantly share code, notes, and snippets.

@jcrooke
Created February 26, 2025 17:36
Show Gist options
  • Save jcrooke/07aa70cf6b7ce9bace20f1637da4ccd0 to your computer and use it in GitHub Desktop.
Save jcrooke/07aa70cf6b7ce9bace20f1637da4ccd0 to your computer and use it in GitHub Desktop.
Fix Next.js SSG strict CSP error regarding inline script usage
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();
@jcrooke
Copy link
Author

jcrooke commented Feb 26, 2025

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

  1. Add remove-inline-scripts.js above to a scripts directory in your project.

  2. Ensure required packages are installed e.g. npm i glob

  3. Ensure your next.config.js contains at least the following attributes;

/** @type {import('next').NextConfig} */
const nextConfig = {
  distDir: 'out',
  basePath: '',
  output: 'export', // static export
  ...
};

module.exports = nextConfig;
  1. Update your package.json scripts to include a "postbuild";
"scripts": {
    "build": "next build",
    "postbuild": "node ./scripts/remove-inline-scripts.js",
    ...
  1. Run: npm run postbuild after a build, or ensure your Github workflow runs this command after the build.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment