Development
Scripts
Section titled “Scripts”Root package.json:
| Script | Command | Purpose |
|---|---|---|
npm run build |
webpack --config webpack.config.cjs |
One-off production build into dist/ |
npm run watch |
webpack … --watch |
Rebuild on save |
npm run install:docs |
cd docs && npm install |
Install the documentation site’s dependencies |
npm run docs:dev |
cd docs && npm run dev |
Astro dev server |
npm run docs:build |
cd docs && npm run build |
Static build into docs/dist |
npm run docs:preview |
cd docs && npm run preview |
Serve the built site |
The extension and the documentation site keep separate dependency trees. npm install at the root
does not install the docs dependencies.
The debug loop
Section titled “The debug loop”Content scripts are not hot-reloaded, so a change needs three actions:
npm run build(or leavenpm run watchrunning).- Press the reload icon on the extension’s card in
chrome://extensions. - Reload the Gmail tab.
Skipping step 2 leaves the old bundle registered; skipping step 3 leaves the old script running in the page.
Where to look for errors
Section titled “Where to look for errors”- Content script logs — the Gmail tab’s own DevTools console.
Mail Thread Export: content script loadedconfirms injection. - Popup logs — right-click the toolbar icon, Inspect popup.
- Service worker logs — the service worker link on the extension card. Nothing is logged, because the worker is empty.
- html2canvas internals — flip
logging: falsetotrueingenerateImagePDF()/generateImage().
Inspecting the staged clone
Section titled “Inspecting the staged clone”The render wrapper only exists for the ~500 ms settle delay. To look at it, raise the delay in
exportEmail(), or temporarily comment out the document.body.removeChild(wrapper) call in the
finally block and change z-index: -9999 to 9999 so the clone renders on top of the page. Both
changes must be reverted before shipping.
Troubleshooting
Section titled “Troubleshooting”The Export button never appears. Check the console for the load message. If it is missing, the
content script is not running: confirm the URL is under https://mail.google.com/, that the extension
is enabled, and that you loaded dist/ rather than the repository root. If the load message is
there, checkForEmailHeader() is not finding h2.hP — inspect the subject heading and see whether
Gmail renamed the class.
The button appears more than once. The idempotency guard is
subject.parentNode.querySelector('.gmail-export-trigger'). If Gmail restructures the header so the
subject’s parent changes between mutations, the guard misses. Widening the check to the whole
div[role="main"] is the fix.
Could not detect email content. detectEmail() returned null because h2.hP was absent — you
are on the message list rather than in a conversation, or the selector is stale.
Parts of the thread are missing from the export. cleanArtifacts() was too aggressive. The usual
culprits are the blanket [data-tooltip] rule and the SVG/path rule that hides any icon whose
ancestor looks like a button. Comment out the selector list and reintroduce entries until the
offending one shows up.
The export is blank. Usually a canvas size limit on a very long thread. Log
canvas.width/canvas.height after the html2canvas call; browsers cap sides at roughly 32,767 px
and also cap total area.
Fixing Gmail selector breakage
Section titled “Fixing Gmail selector breakage”Gmail ships obfuscated, unstable class names. When an export starts including interface chrome again:
- Export a thread and note exactly what leaked in.
- Inspect the offending element in Gmail’s DOM and prefer a stable hook over a class name — an
ARIA role, a
role="button"with a knownaria-label, or adata-tooltipvalue. - Add it to
selectorsToHideincleanArtifacts(). Add the German label as well as the English one: the list already pairs them (Reply/Antworten,Forward/Weiterleiten,Delete/Löschen, …), and any new entry should follow that convention. - Rebuild and re-export the same thread to confirm.
Prefer hiding (display: none !important) over removing. Removal can break layout in the clone;
hiding leaves the tree intact.
Coding conventions
Section titled “Coding conventions”- ES modules throughout
src/("type": "module"inpackage.json);webpack.config.cjsis CommonJS because webpack loads it withrequire. - No framework. Everything is plain DOM APIs — deliberate, to keep the content-script bundle as small as html2canvas and jsPDF allow.
- Interface strings are English-only in the injected UI (“Export”, “Export as PDF”, “Export as Image”, “Processing…”), while the selector lists match both English and German Gmail. Adding a locale means extending those selector lists, not just translating the button labels.
- Never mutate the live Gmail DOM beyond appending the export button and the temporary wrapper. All cleanup happens on the clone.
Documentation site
Section titled “Documentation site”The site is Astro + Starlight with the Galaxy theme.
npm run install:docsnpm run docs:devnpm run docs:build- Pages are MDX under
docs/src/content/docs/; every page needstitleanddescriptionfrontmatter. - The sidebar is declared explicitly in
docs/astro.config.mjs— a new page must be added there to appear. siteandbaseare set tohttps://rennerdo30.github.io/mail-thread-exportand/mail-thread-export. Because ofbase, internal links written by hand must include that prefix.- Accent colours are overridden in
docs/src/styles/custom.css; both dark and light modes are defined.
Deployment
Section titled “Deployment”.github/workflows/deploy.yml runs on every push to main and on manual dispatch. It checks out the
repository, sets up Node 20 with npm caching keyed on docs/package-lock.json, runs npm ci and
npm run build in docs/, uploads docs/dist as a Pages artifact, and deploys it with
actions/deploy-pages. Permissions are contents: read, pages: write, id-token: write, and
concurrent runs are grouped under pages.
GitHub Pages must be set to the GitHub Actions source in the repository settings before the first deployment can succeed.
Ideas worth doing
Section titled “Ideas worth doing”- Wire up the popup, or remove it and the unused
activeTab/scripting/downloadspermissions. - Delete the empty service worker declaration, or give it a purpose.
- Remove the unused
src/lib/UMD builds. - Emit a real multi-page PDF for long threads instead of one oversized page, sidestepping the canvas size limit.
- Expand collapsed messages and trimmed quoted text automatically before capture.