High-fidelity Google Docs exporter to plain HTML+CSS
JavaScript
8
97 commits
updated Sep 21, 2026
A Node.js script that exports Google Docs to HTML+CSS with high fidelity, preserving essential formatting like headings, line spacing, alignment, bullet lists, images, and more. Perfect for creating an offline or self-hosted version of your docs that closely matches the original.
Why this script? Because simpler exports often lose alignment, line spacing, or right-to-left details. This script pulls detailed styling info (like heading levels, inline font sizes, doc-based image sizes, and bullet indentation) directly from the Google Docs API. It then merges that styling into HTML and inserts a corresponding CSS that replicates Google Docs formatting while also neutralizing default browser quirks.
paragraphStyle.lineSpacing, spaceAbove, spaceBelow, indentation, alignment.dir="rtl" and flip alignment (START → right).<table> with <tr> and <td>, keeping paragraph formatting in each cell.<div class="doc-toc">, indenting each line by its heading level.<ul dir="rtl"> so bullets align on the right.<link> to fonts.googleapis.com so text families match.<h3>. We override heading tags (h1..h6 { font-size: 1em }) so Google Docs' inline style alone sets the final size.<main class="doc-content"> so injected navigation can remain outside the page's primary-content landmark.Prerequisites:
npm install to install the declared dependencies.Get the Script:
service_account.json beside gdocs-me-up.js, or set
SERVICE_ACCOUNT_KEY_FILE to another path. The default works regardless of
the directory from which the exporter is invoked.Authenticate:
service_account.json.node gdocs-me-up.js <DOC_ID> <OUTPUT_DIR> [options]
For help:
node gdocs-me-up.js --help
<DOC_ID>: The unique ID from your doc's URL. For example:
https://docs.google.com/document/d/1UnR2zKf3Z_DDRS6vLgBkSHUeqI3IGOEhWYh7rAIvsb8/edit
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<OUTPUT_DIR>: The folder where the script will write index.html and an images/ subfolder.--stylesheet <HREF>: Inserts an external stylesheet after the generated styles. Repeat the option to include multiple stylesheets in argument order. Hrefs are resolved relative to the generated HTML file.--script <SRC>: Inserts an external script immediately before </body>. Repeat the option to include multiple scripts in argument order. Sources are resolved relative to the generated HTML file.--head-file <FILE>: Reads a UTF-8 HTML fragment and inserts it immediately before </head>, after the generated styles and external stylesheets. Repeat the option to insert several files in argument order. File paths are relative to the current working directory. {{title}} is replaced with the Google Doc's title, escaped for HTML text or quoted attributes; the rest of the fragment is inserted unchanged. Use fragments without enclosing <head> tags.--html-name <NAME>: Replaces the default index.html filename.--images-dir <NAME>: Replaces the default images directory name.Example:
node gdocs-me-up.js 1UnR2zKf3Z_DDRS6vLgBkSHUeqI3IGOEhWYh7rAIvsb8 docs_export
To apply stylesheets and scripts stored next to the output directory:
node gdocs-me-up.js 1UnR2zKf3Z_DDRS6vLgBkSHUeqI3IGOEhWYh7rAIvsb8 docs_export \
--stylesheet ../style.css \
--stylesheet ../theme.css \
--script ../script.js \
--html-name article.html \
--images-dir assets
On completion:
docs_export/index.html: Your doc in near-pixel HTML+CSS fidelity.docs_export/images/: Downloaded images.Open docs_export/index.html in your browser. You'll see headings, bullet-lists, alignment, images, and more, closely mirroring the original doc.
Store shared metadata in a file such as shared-head.html:
<meta property="og:title" content="{{title}}">
<meta property="og:site_name" content="My writing">
<meta property="og:type" content="article">
<meta property="og:image" content="https://example.com/writing/preview.png">
<meta name="twitter:card" content="summary_large_image">
Reuse the same file for every export:
node gdocs-me-up.js 1UnR2zKf3Z_DDRS6vLgBkSHUeqI3IGOEhWYh7rAIvsb8 writing \
--html-name article.html \
--head-file shared-head.html
The title comes from Google Docs automatically. The shared file supplies the
site name and preview image, so new pieces need no separate metadata entries.
Use absolute public URLs for preview images. {{title}} is intended for HTML
text and attributes, not JavaScript or CSS. Exports without --head-file keep
their existing output.
Programmatic callers can pass headFiles: ['shared-head.html'] in the options
object to exportDocToHTML.
The project includes comprehensive testing to ensure export quality:
npm test
Tests core utility functions (escapeHtml, color conversion, list detection, etc.) with 97% coverage.
npm test tests/content-verification.test.js
Compares Google Docs API data with exported HTML to verify:
Uses two stable reference documents as golden standards.
Full HTML regression detection - catches any change to output:
npm test # Runs automatically with other tests
npm test -- -u # Update snapshots after intentional changes
npm run test:visual
Generates side-by-side screenshots and analysis reports comparing Google Docs with exported HTML. Reports include:
See tests/visual/README.md for details.
To run the local visual-comparison corpus:
GDOCS_CORPUS_DIR=/path/to/fixtures npm run compare:corpus
The corpus runner deduplicates document IDs, captures the Google preview and local export at the same viewport, and writes screenshots, a contact sheet, and a JSON report under tests/visual/corpus/. Use -- --limit=5 for a short run or -- --names=story,butterflies to select fixture names. Fixture files and their location are not part of the repository.
Line Spacing: The script reads paragraphStyle.lineSpacing and maps Google Docs' font-dependent metrics to browser line boxes. It also applies spaceAbove + spaceBelow as margin-top + margin-bottom.
Right-to-Left Paragraphs: If paragraphStyle.direction = RIGHT_TO_LEFT, we add dir="rtl". If alignment=START, it becomes right; alignment=END => left. Lists also carry dir="rtl" so bullets go on the right side.
Images: Supports both inline images and positioned objects (header photos, wrapped images). Images are constrained to container width while retaining their explicit aspect ratio. We read size info from both imageProperties.size and embedded.size, converting points to pixels (~1.333 ratio) and respecting transforms. All four crop edges and crop rotation are rendered through a responsive clipping frame. Positioned objects render at their anchor paragraph. Raster resizing targets twice the displayed dimensions, accounting for the page width and crop, without enlarging the source. If resampling makes a PNG larger, the smaller losslessly optimized original is kept instead. JPEGs are encoded as quality-84 WebP (effort 6, smart subsampling). PNGs are compressed losslessly, removing redundant alpha or color channels and using an exact palette when smaller; palette candidates must preserve every decoded pixel. Opaque PNGs whose grayscale entropy indicates photographic content are tested against the same WebP encoding and converted only when the candidate is at least 20% smaller than the optimized PNG. Existing static WebP images are resized losslessly only when that saves bytes; animated and other formats are preserved.
TOC Indentation: For each line in the doc’s table of contents, the script checks the heading level of the link target. It then adds a <div class="toc-level-3"> (for example) with a margin-left rule in the CSS.
Compact Semantic Markup: Google Docs often splits text into many runs. Consecutive compatible runs are merged, common paragraph formatting is inherited, emphasis uses semantic tags such as <strong> and <em>, and repeated declarations are deduplicated into generated CSS classes. Exported content does not repeat inline style attributes.
Heading Size: We reset browser heading defaults, then a generated class supplies the document's exact size and weight without the browser multiplying them.
Fonts: We request document fonts through a single Google Fonts stylesheet with display=block. Families used in italic text request upright, italic, bold, and bold-italic faces, including when italic runs inherit their family from a paragraph. Google Fonts supplies the available faces; fonts without an italic face retain the browser's usual behavior. Common system fonts such as Arial, Georgia, and Consolas remain in the document CSS but are omitted from Google Fonts requests, where restricted families can prevent the whole stylesheet from loading. Preconnect links to fonts.googleapis.com and fonts.gstatic.com precede the stylesheet, with crossorigin on the font-file connection. These links are emitted only when the document requests web fonts. Text waits briefly for the document fonts to load before showing a fallback font.
computeDocContainerWidth() to remove the + 50, or set a fixed width.line-height:1.2, remove or comment out the lines in renderParagraph referencing paragraphStyle.lineSpacing.<h1>.. <h6>, replace them with <p class="doc-heading-level-X"> in the code. Then style them in CSS as you like.HEADING_1..6. If your doc uses custom styles, the script may not see them as headings.<h3> CSS override is present, or remove any conflicting styles from your own stylesheet.getAuthClient() to your flow.Doc: State of Text Rendering 2024 by Behdad Esfahbod.
Command:
node gdocs-me-up.js 1UnR2zKf3Z_DDRS6vLgBkSHUeqI3IGOEhWYh7rAIvsb8 docs_export
Result:
docs_export/index.html: The exported document, preserving headings, lists, images, and paragraph formatting.docs_export/images/: Images in their source format, with space-saving WebP conversion for JPEG and suitable opaque PNG photographs.Open the HTML in your browser or upload it to a web server.
We welcome improvements or bug fixes. This script is licensed under MIT, so feel free to adapt or include it in your projects, with attribution appreciated.
Thanks for checking out GDocs-Me-Up! We hope it helps you create accurate offline or self-hosted versions of your docs. If you have suggestions, issues, or ideas, please open an issue or PR. Happy exporting!
97 commits
JavaScript
100.0%
High-fidelity Google Docs exporter to plain HTML+CSS
JavaScript
8
97 commits
updated Sep 21, 2026
A Node.js script that exports Google Docs to HTML+CSS with high fidelity, preserving essential formatting like headings, line spacing, alignment, bullet lists, images, and more. Perfect for creating an offline or self-hosted version of your docs that closely matches the original.
Why this script? Because simpler exports often lose alignment, line spacing, or right-to-left details. This script pulls detailed styling info (like heading levels, inline font sizes, doc-based image sizes, and bullet indentation) directly from the Google Docs API. It then merges that styling into HTML and inserts a corresponding CSS that replicates Google Docs formatting while also neutralizing default browser quirks.
paragraphStyle.lineSpacing, spaceAbove, spaceBelow, indentation, alignment.dir="rtl" and flip alignment (START → right).<table> with <tr> and <td>, keeping paragraph formatting in each cell.<div class="doc-toc">, indenting each line by its heading level.<ul dir="rtl"> so bullets align on the right.<link> to fonts.googleapis.com so text families match.<h3>. We override heading tags (h1..h6 { font-size: 1em }) so Google Docs' inline style alone sets the final size.<main class="doc-content"> so injected navigation can remain outside the page's primary-content landmark.Prerequisites:
npm install to install the declared dependencies.Get the Script:
service_account.json beside gdocs-me-up.js, or set
SERVICE_ACCOUNT_KEY_FILE to another path. The default works regardless of
the directory from which the exporter is invoked.Authenticate:
service_account.json.node gdocs-me-up.js <DOC_ID> <OUTPUT_DIR> [options]
For help:
node gdocs-me-up.js --help
<DOC_ID>: The unique ID from your doc's URL. For example:
https://docs.google.com/document/d/1UnR2zKf3Z_DDRS6vLgBkSHUeqI3IGOEhWYh7rAIvsb8/edit
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
<OUTPUT_DIR>: The folder where the script will write index.html and an images/ subfolder.--stylesheet <HREF>: Inserts an external stylesheet after the generated styles. Repeat the option to include multiple stylesheets in argument order. Hrefs are resolved relative to the generated HTML file.--script <SRC>: Inserts an external script immediately before </body>. Repeat the option to include multiple scripts in argument order. Sources are resolved relative to the generated HTML file.--head-file <FILE>: Reads a UTF-8 HTML fragment and inserts it immediately before </head>, after the generated styles and external stylesheets. Repeat the option to insert several files in argument order. File paths are relative to the current working directory. {{title}} is replaced with the Google Doc's title, escaped for HTML text or quoted attributes; the rest of the fragment is inserted unchanged. Use fragments without enclosing <head> tags.--html-name <NAME>: Replaces the default index.html filename.--images-dir <NAME>: Replaces the default images directory name.Example:
node gdocs-me-up.js 1UnR2zKf3Z_DDRS6vLgBkSHUeqI3IGOEhWYh7rAIvsb8 docs_export
To apply stylesheets and scripts stored next to the output directory:
node gdocs-me-up.js 1UnR2zKf3Z_DDRS6vLgBkSHUeqI3IGOEhWYh7rAIvsb8 docs_export \
--stylesheet ../style.css \
--stylesheet ../theme.css \
--script ../script.js \
--html-name article.html \
--images-dir assets
On completion:
docs_export/index.html: Your doc in near-pixel HTML+CSS fidelity.docs_export/images/: Downloaded images.Open docs_export/index.html in your browser. You'll see headings, bullet-lists, alignment, images, and more, closely mirroring the original doc.
Store shared metadata in a file such as shared-head.html:
<meta property="og:title" content="{{title}}">
<meta property="og:site_name" content="My writing">
<meta property="og:type" content="article">
<meta property="og:image" content="https://example.com/writing/preview.png">
<meta name="twitter:card" content="summary_large_image">
Reuse the same file for every export:
node gdocs-me-up.js 1UnR2zKf3Z_DDRS6vLgBkSHUeqI3IGOEhWYh7rAIvsb8 writing \
--html-name article.html \
--head-file shared-head.html
The title comes from Google Docs automatically. The shared file supplies the
site name and preview image, so new pieces need no separate metadata entries.
Use absolute public URLs for preview images. {{title}} is intended for HTML
text and attributes, not JavaScript or CSS. Exports without --head-file keep
their existing output.
Programmatic callers can pass headFiles: ['shared-head.html'] in the options
object to exportDocToHTML.
The project includes comprehensive testing to ensure export quality:
npm test
Tests core utility functions (escapeHtml, color conversion, list detection, etc.) with 97% coverage.
npm test tests/content-verification.test.js
Compares Google Docs API data with exported HTML to verify:
Uses two stable reference documents as golden standards.
Full HTML regression detection - catches any change to output:
npm test # Runs automatically with other tests
npm test -- -u # Update snapshots after intentional changes
npm run test:visual
Generates side-by-side screenshots and analysis reports comparing Google Docs with exported HTML. Reports include:
See tests/visual/README.md for details.
To run the local visual-comparison corpus:
GDOCS_CORPUS_DIR=/path/to/fixtures npm run compare:corpus
The corpus runner deduplicates document IDs, captures the Google preview and local export at the same viewport, and writes screenshots, a contact sheet, and a JSON report under tests/visual/corpus/. Use -- --limit=5 for a short run or -- --names=story,butterflies to select fixture names. Fixture files and their location are not part of the repository.
Line Spacing: The script reads paragraphStyle.lineSpacing and maps Google Docs' font-dependent metrics to browser line boxes. It also applies spaceAbove + spaceBelow as margin-top + margin-bottom.
Right-to-Left Paragraphs: If paragraphStyle.direction = RIGHT_TO_LEFT, we add dir="rtl". If alignment=START, it becomes right; alignment=END => left. Lists also carry dir="rtl" so bullets go on the right side.
Images: Supports both inline images and positioned objects (header photos, wrapped images). Images are constrained to container width while retaining their explicit aspect ratio. We read size info from both imageProperties.size and embedded.size, converting points to pixels (~1.333 ratio) and respecting transforms. All four crop edges and crop rotation are rendered through a responsive clipping frame. Positioned objects render at their anchor paragraph. Raster resizing targets twice the displayed dimensions, accounting for the page width and crop, without enlarging the source. If resampling makes a PNG larger, the smaller losslessly optimized original is kept instead. JPEGs are encoded as quality-84 WebP (effort 6, smart subsampling). PNGs are compressed losslessly, removing redundant alpha or color channels and using an exact palette when smaller; palette candidates must preserve every decoded pixel. Opaque PNGs whose grayscale entropy indicates photographic content are tested against the same WebP encoding and converted only when the candidate is at least 20% smaller than the optimized PNG. Existing static WebP images are resized losslessly only when that saves bytes; animated and other formats are preserved.
TOC Indentation: For each line in the doc’s table of contents, the script checks the heading level of the link target. It then adds a <div class="toc-level-3"> (for example) with a margin-left rule in the CSS.
Compact Semantic Markup: Google Docs often splits text into many runs. Consecutive compatible runs are merged, common paragraph formatting is inherited, emphasis uses semantic tags such as <strong> and <em>, and repeated declarations are deduplicated into generated CSS classes. Exported content does not repeat inline style attributes.
Heading Size: We reset browser heading defaults, then a generated class supplies the document's exact size and weight without the browser multiplying them.
Fonts: We request document fonts through a single Google Fonts stylesheet with display=block. Families used in italic text request upright, italic, bold, and bold-italic faces, including when italic runs inherit their family from a paragraph. Google Fonts supplies the available faces; fonts without an italic face retain the browser's usual behavior. Common system fonts such as Arial, Georgia, and Consolas remain in the document CSS but are omitted from Google Fonts requests, where restricted families can prevent the whole stylesheet from loading. Preconnect links to fonts.googleapis.com and fonts.gstatic.com precede the stylesheet, with crossorigin on the font-file connection. These links are emitted only when the document requests web fonts. Text waits briefly for the document fonts to load before showing a fallback font.
computeDocContainerWidth() to remove the + 50, or set a fixed width.line-height:1.2, remove or comment out the lines in renderParagraph referencing paragraphStyle.lineSpacing.<h1>.. <h6>, replace them with <p class="doc-heading-level-X"> in the code. Then style them in CSS as you like.HEADING_1..6. If your doc uses custom styles, the script may not see them as headings.<h3> CSS override is present, or remove any conflicting styles from your own stylesheet.getAuthClient() to your flow.Doc: State of Text Rendering 2024 by Behdad Esfahbod.
Command:
node gdocs-me-up.js 1UnR2zKf3Z_DDRS6vLgBkSHUeqI3IGOEhWYh7rAIvsb8 docs_export
Result:
docs_export/index.html: The exported document, preserving headings, lists, images, and paragraph formatting.docs_export/images/: Images in their source format, with space-saving WebP conversion for JPEG and suitable opaque PNG photographs.Open the HTML in your browser or upload it to a web server.
We welcome improvements or bug fixes. This script is licensed under MIT, so feel free to adapt or include it in your projects, with attribution appreciated.
Thanks for checking out GDocs-Me-Up! We hope it helps you create accurate offline or self-hosted versions of your docs. If you have suggestions, issues, or ideas, please open an issue or PR. Happy exporting!
97 commits
JavaScript
100.0%