Screenshot Markdown
Captures a screenshot of Markdown content (with MathJaX support) using Headless Chromium. You provide an index.html template and one or more .md files. Gotenberg converts the Markdown to HTML and injects it into your template.
The index.html file must include the following Go template action where the content should appear:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>My PDF</title>
</head>
<body>
{{ toHTML "file.md" }}
</body>
</html>
See the Chromium module configuration for startup and behavior flags.
Basics
curl \
--request POST http://localhost:3000/forms/chromium/screenshot/markdown \
--form files=@/path/to/index.html \
--form files=@/path/to/file.md \
-o my.png
- 200
- 400
- 503
Content-Disposition: attachment; filename={output-filename.ext}
Content-Type: {content-type}
Content-Length: {content-length}
Gotenberg-Trace: {trace}
Body: {output-file}
Content-Type: text/plain; charset=UTF-8
Gotenberg-Trace: {trace}
Body: {error}
Content-Type: text/plain; charset=UTF-8
Gotenberg-Trace: {trace}
Body: {error}
Assets
Send images, fonts, and stylesheets alongside your HTML.
curl \
--request POST http://localhost:3000/forms/chromium/screenshot/markdown \
--form files=@/path/to/index.html \
--form files=@/path/to/file.md \
--form files=@/path/to/img.png \
-o my.png
All uploaded files are stored in a single flat directory. Reference assets by filename only: no absolute paths (/img.png) or subdirectories (./assets/img.png).
✅ Correct Reference
Since index.html and logo.png sit side-by-side in the container:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>My PDF</title>
</head>
<body>
{{ toHTML "file.md" }}
<img src="logo.png" />
</body>
</html>
❌ Incorrect Reference
Gotenberg does not recreate your local folder structure (e.g., images/) inside the container.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>My PDF</title>
</head>
<body>
{{ toHTML "file.md" }}
<img src="/images/logo.png" />
</body>
</html>
For remote URLs (CDNs, Google Fonts), make sure the container can resolve the domain. You cannot use localhost from inside the container. Use host.docker.internal or your host's network IP instead.
For small assets, Base64 data URIs eliminate network dependencies entirely. Avoid the HTML <base> element: it breaks the link between your HTML and uploaded assets.
Rendering Behavior
800600false1png100falsefalsecurl \
--request POST http://localhost:3000/forms/chromium/screenshot/markdown \
--form files=@/path/to/index.html \
--form files=@/path/to/file.md \
--form width=1280 \
--form height=720 \
--form format=jpeg \
--form quality=85 \
--form optimizeForSpeed=true \
-o my.jpeg
Viewport & Layout
The viewport defines the image. Unlike PDFs which paginate content, screenshots capture the browser's viewport exactly as rendered.
- Resolution: Ensure you set the
widthandheightform fields to match your desired target device (e.g.,1920x1080for desktop,375x812for mobile). - Pixel density: Raise
deviceScaleFactorfor high-DPI output. A value of2doubles the captured pixels, matching a retina display. - If your screenshot's content is repeated and clipped, consider setting the
skipNetworkIdleEventform field to false (see issue #1065).
If you are simulating a mobile device, remember to also set the userAgent to match a mobile browser, or the site might serve the desktop version.
Emulated Media Features
You can simulate specific browser conditions by overriding CSS media features. This is particularly useful for forcing "Dark Mode" or testing layouts with reduced motion.
The emulatedMediaFeatures form field expects a JSON array of objects, where each object contains a name and a value.
Common Media Features:
| Feature Name | Common Values | Description |
|---|---|---|
prefers-color-scheme | light, dark | Emulates the user's OS color theme preference. |
prefers-reduced-motion | no-preference, reduce | Emulates the setting to minimize non-essential motion. |
color-gamut | srgb, p3, rec2020 | Emulates the approximate range of colors supported by the output device. |
forced-colors | none, active | Emulates "High Contrast" modes where the browser restricts colors. |
Nonecurl \
--request POST http://localhost:3000/forms/chromium/screenshot/markdown \
--form files=@/path/to/index.html \
--form files=@/path/to/file.md \
--form emulatedMediaFeatures='[{"name": "prefers-color-scheme", "value": "dark"}, {"name": "prefers-reduced-motion", "value": "reduce"}]' \
-o my.jpeg
Image Format
Choosing the right output format impacts quality and file size:
- PNG: Best for screenshots containing text, UI elements, or flat colors. It is lossless, ensuring text remains sharp.
- JPEG: Best for screenshots containing photographs or complex gradients.
qualitybalances file size against visual fidelity (default is100). - WebP: Offers a modern balance of compression and quality, usually superior to JPEG.
JavaScript & Dynamic Content
Chromium captures what is currently visible. If the page relies on JavaScript to render data, charts, or external content, the conversion might trigger before the rendering is complete, resulting in blank or incomplete sections.
If the content is generated dynamically:
waitDelayadds a fixed pause before conversion.- For more precision,
waitForExpressiontriggers the conversion only when a specific JavaScript condition (e.g.,window.status === 'ready') is met.
Wait Delay
Use this as a fallback when you cannot modify the target page's code. It forces Gotenberg to wait for a fixed duration before rendering, giving JavaScript time to finish execution.
This method is "brute force". If the page loads faster, time is wasted. If it loads slower, the image will be incomplete. Use explicit waits (Expression or Selector) whenever possible.
Nonecurl \
--request POST http://localhost:3000/forms/chromium/screenshot/markdown \
--form files=@/path/to/index.html \
--form files=@/path/to/file.md \
--form waitDelay=5s \
-o my.jpeg
Wait For Expression
This is the most robust method for synchronization. It pauses the conversion process until a specific JavaScript expression evaluates to true within the page context. This ensures the image is generated exactly when your data is ready.
Example: Client-side logic
// Inside your HTML page.
window.status = "loading";
fetchData().then(() => {
renderCharts();
// Signal to Gotenberg that the page is ready.
window.status = "ready";
});
Nonecurl \
--request POST http://localhost:3000/forms/chromium/screenshot/markdown \
--form files=@/path/to/index.html \
--form files=@/path/to/file.md \
--form 'waitForExpression=window.status === '\''ready'\''' \
-o my.jpeg
Wait For Selector
Ideally suited for Single Page Applications (SPAs) or frameworks like React/Vue. This method delays the conversion until a specific HTML element - identified by a CSS selector - appears in the DOM.
Example: Dynamic Element Injection
// Inside your HTML page.
await heavyCalculation();
const completionMarker = document.createElement("div");
completionMarker.id = "app-ready"; // The selector we wait for.
document.body.appendChild(completionMarker);
Nonecurl \
--request POST http://localhost:3000/forms/chromium/screenshot/markdown \
--form files=@/path/to/index.html \
--form files=@/path/to/file.md \
--form 'waitForSelector=#app-ready' \
-o my.jpeg
HTTP & Networking
Cookies
A JSON array of cookie objects for authentication or session state.
| Key | Description | Default |
|---|---|---|
name | The name of the cookie. | Required |
value | The value of the cookie. | Required |
domain | The domain the cookie applies to (e.g., example.com). | Required |
path | The URL path the cookie applies to. | None |
secure | If true, the cookie is only sent over HTTPS. | None |
httpOnly | If true, the cookie is inaccessible to JavaScript (document.cookie). | None |
sameSite | Controls cross-site behavior. Values: "Strict", "Lax", "None". | None |
Cookies expire when the request reaches its time limit. For strict isolation between conversions, configure the API to clear the cookie jar after every request. See API module configuration.
Nonecurl \
--request POST http://localhost:3000/forms/chromium/screenshot/markdown \
--form files=@/path/to/index.html \
--form files=@/path/to/file.md \
--form 'cookies=[{"name":"yummy_cookie","value":"choco","domain":"theyummycookie.com"}]' \
-o my.png
HTTP Headers
A JSON object of headers sent with every browser request (including images, stylesheets, scripts).
To restrict a header to specific URLs, append ;scope= with a regex. The scope token is stripped before sending.
Example: "X-Internal-Token": "secret-123;scope=.*\\.internal\\.api"
https://data.internal.api/v1→ header senthttps://google.com/fonts→ header not sent
NoneNonecurl \
--request POST http://localhost:3000/forms/chromium/screenshot/markdown \
--form files=@/path/to/index.html \
--form files=@/path/to/file.md \
--form 'userAgent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)"' \
--form-string 'extraHttpHeaders={"X-Header":"value","X-Scoped-Header":"value;scope=https?:\/\/([a-zA-Z0-9-]+\.)*domain\.com\/.*"}' \
-o my.png
Invalid HTTP Status Codes
Return a 409 Conflict if the main page or its resources return specific HTTP status codes (JSON array of integers).
| Field | Description |
|---|---|
failOnHttpStatusCodes | Fails if the main page URL returns a matching code. |
failOnResourceHttpStatusCodes | Fails if any asset (image, CSS, script) returns a matching code. |