In Switzerland, taxes are hyper-local. You deal with three levels: federal, cantonal, and municipal. The implications are very real—your tax burden can change dramatically by moving a few kilometers. For example, Hausen am Albis and Baar are less than 5 km apart, but the tax rates differ by a multiple depending on your income.

Since I’m actively looking to rent or buy property in Switzerland, this always annoyed me. Every time I found a property I liked, I had to double-check whether the local taxes would ruin the deal. I wanted a simple solution: a browser extension that would tell me instantly if I’d pay more or less taxes in a given location—based on a real estimation, not just vague averages.

SwissTaxCalculator to the Rescue

Fortunately, a talented developer created a Nuxt.js-based Swiss tax calculator. It’s accurate and maintained (thanks again to him who pushed improvements while I was working with the code).

So many numbers, I love it!

I thought, “Perfect—I’ll just generate a static version and embed it in my extension.”

Turns out, it’s not that simple.

Challenges with Nuxt and Chrome Extensions

What looked like a straightforward task quickly turned into a minefield. Here’s what I ran into:

1. Navigation Is Broken by Chrome Extension Serving Rules

Chrome Extensions serve static files from a chrome-extension:// URL base, and Nuxt generates routes like /index.html or nested pages that assume server-side routing. This doesn’t work out of the box. I had to fix the path manually on page load:

if (location.pathname.endsWith('/index.html')) {
  const newPath = location.pathname.replace(/index\.html$/, '');
  window.history.replaceState({}, '', newPath + location.search + location.hash);
}

This allows internal navigation to behave correctly and avoids the dreaded white page when clicking around.

2. Full Local, No Server

I didn’t want to rely on a remote API. Not for privacy reasons—although that’s a bonus—but because I didn’t want to deal with other people’s tax data, not even logs. Also, I didn’t want to manage uptime, hosting, or billing. This needed to be fully local.

That meant every asset, every JSON, and every dynamic bit of behavior needed to live inside the /public folder that Nuxt outputs when using npx nuxi generate.

Some methods in the original tax calculator were calling fetch() on the server, which doesn’t work inside an extension. And worse: if your code runs as a content script, you’re technically executing in the context of the host page. In that case, fetch() tries to reach paths relative to the site—not your extension.

So I had to write a more advanced polyfill that:

  • Uses fs/promises on the server
  • Uses chrome.runtime.getURL() inside extension context
  • Falls back to normal fetch() in the browser
export const readFile = async (filePath: string): Promise<string> => {
  if (typeof window === 'undefined') {
    const { readFile } = await import('fs/promises');
    return readFile(filePath, 'utf-8');
  }

  if (chrome.runtime){
    const inExtensionFilePath = chrome.runtime.getURL("/options/" + filePath.replace('./', ''));
    const response = await fetch(inExtensionFilePath);
    if (!response.ok) {
      throw new Error(`Failed to fetch file at ${filePath}`);
    }
    return response.text();
  }

  else {
    const response = await fetch(filePath);
    if (!response.ok) {
      throw new Error(`Failed to fetch file at ${filePath}`);
    }
    return response.text();
  }
};

And of course, this only works if all the necessary files are copied into the extension bundle (in my case, under /options/) and declared correctly in the manifest with proper permissions and access rules.

3. Inline Scripts Are Forbidden

Chrome’s Content Security Policy (CSP) forbids inline <script> tags in HTML, and Nuxt injects its configuration state as a script tag like:

<script>window.__NUXT__ = { ... }</script>

That breaks CSP. You get a blank page or a cryptic error in the console. I needed to externalize every inline script and rewrite index.html.

Cheerio to the Rescue

Cheerio is a lightweight, server-side jQuery implementation. I used it to parse the generated HTML, extract the inline scripts, hash and save them as external .js files, and patch index.html accordingly. I also made sure to inject a change-history.js script as a small runtime fix.

const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const cheerio = require('cheerio');

const indexHtmlPath = path.resolve(__dirname, 'extension', 'options', 'index.html');
const nuxtAssetsDir = path.resolve(__dirname, 'extension', 'options');
const changeHistoryPath = path.resolve(__dirname, 'utils', 'change-history.js');

if (!fs.existsSync(nuxtAssetsDir)) {
  fs.mkdirSync(nuxtAssetsDir, { recursive: true });
}

const htmlContent = fs.readFileSync(indexHtmlPath, 'utf-8');
const $ = cheerio.load(htmlContent);

$('script:not([src])').each((i, el) => {
  const scriptContent = $(el).html().trim();
  if (scriptContent) {
    const hash = crypto.createHash('sha256')
      .update(scriptContent)
      .digest('hex')
      .slice(0, 16);
    const inlineFileName = `inline.${hash}.js`;
    const inlineFilePath = path.join(nuxtAssetsDir, inlineFileName);

    if (!fs.existsSync(inlineFilePath)) {
      fs.writeFileSync(inlineFilePath, scriptContent, 'utf-8');
      console.log(`Created external inline script: ${inlineFilePath}`);
    }

    const scriptBlock = `
      <script src="/options/change-history.js" crossorigin></script>
      <script src="/options/${inlineFileName}" crossorigin></script>
    `;
    $(el).replaceWith(scriptBlock);
  }
});

fs.copyFileSync(changeHistoryPath, `${nuxtAssetsDir}/change-history.js`);
fs.writeFileSync(indexHtmlPath, $.html(), 'utf-8');

console.log('Updated index.html with externalized inline scripts.');

I run it right after generating the static Nuxt build:

npx nuxi generate && cp -r .output/public/* extension/options && node externalizeInlineScripts.js

At this point, all files are safely embedded inside the extension. No CSP errors. No fetch calls. No remote servers. Just local code, working offline, respecting user privacy, and saving me hours of boring tax research.

Final Thoughts

You’d think generating a static Nuxt site and embedding it in a Chrome extension would be plug-and-play. It’s not. Between CSP limitations, broken navigation handling, and the complete absence of server-side capabilities, the setup needed several workarounds. But it works now. And I get instant, contextual tax info while browsing properties—which was the whole point.

The code is open source and the extension is released.

If you’re building something similar, I hope this saves you a few hours.