Writing
Offer PDF and EPUB downloads of your docs pages
Readers can save any docs page as a PDF or an EPUB, and you know what each format keeps, from long code blocks and tabs to images and diagrams.
By Hayden Bleasel8 min read

Readers can save a docs page for offline use once you turn on Blume's export option. Every docs page then gets an Export action under its table of contents, with Export to PDF and Export to EPUB. PDF goes through the browser's print dialog, with a print stylesheet that strips the site down to the article. EPUB is generated in the reader's browser and downloads as a .epub file for Apple Books, calibre, or an e-reader. Neither needs a server, so a static site stays static.
Both formats export the one page the reader is on, not your documentation as a book. If you need a whole manual with a cover and chapters, that's a publishing pipeline Blume doesn't provide. For PDF alone, you may not need the option: the print stylesheet ships on every Blume site, so a reader who presses Ctrl+P (Cmd+P on a Mac) already gets the stripped-down page. export adds a menu entry that opens the print dialog, plus the EPUB generator.
This guide turns the action on, builds a test page with the content that exports worst, checks both files against a checklist, and fixes what doesn't carry over.
Turn on the Export action
Add export to your config. true turns on both formats:
import { defineConfig } from "blume";
export default defineConfig({
title: "Acme Docs",
export: true,
});To offer one format, pass an object instead. A format you leave out stays off, so this shows only the PDF entry:
export: { pdf: true },The action lives in the right-hand rail, below the table of contents, and that rail doesn't show everywhere:
- It appears only in windows at least 1,280 pixels wide. On a phone there's no Export action, but printing from the browser's own menu still applies the same print stylesheet.
- Pages whose layout mode drops the table of contents (
wide,center,frame, andcustom) have no rail, and neither do API operation pages. - Turning the outline off with
toc: falsekeeps the rail, so the action stays.
Build a test page
Exports go wrong on particular content, so test with a page that has all of it: a long command, a titled code block, a collapsed code block, tabs, a diagram, an image, a callout, and an accordion. Save this as docs/export-test.mdx, and put any screenshot at docs/images/dashboard.png:
---
title: Send your first message
description: Install the Acme SDK, send a test SMS, and check that it arrived.
---
Install the SDK with the package manager your project uses:
```package-install
npm i @acme/messages
```
## Set your API key
<Tabs>
<Tab title="macOS and Linux">
On macOS or Linux, export the key in your shell:
```bash
export ACME_KEY="sk_test_123"
```
</Tab>
<Tab title="Windows">
On Windows, set the key in PowerShell:
```powershell
$env:ACME_KEY = "sk_test_123"
```
</Tab>
</Tabs>
## Send a message
Send a test SMS from the command line:
```bash
curl -X POST https://api.acme.example/v1/messages -H "Authorization: Bearer $ACME_KEY" -H "Content-Type: application/json" -d '{"to":"+15550100","channel":"sms","template":"welcome"}'
```
Or from your code:
```ts send.ts
import { Acme } from "@acme/messages";
const acme = new Acme({ apiKey: process.env.ACME_KEY });
const message = await acme.messages.send({
to: "+15550100",
channel: "sms",
template: "welcome",
});
console.log(message.id, message.status);
```
The API answers with the new message:
```json response.json expandable
{
"id": "msg_01J9ZKQ4",
"object": "message",
"channel": "sms",
"to": "+15550100",
"template": "welcome",
"status": "queued",
"created_at": "2026-09-27T10:00:00Z",
"updated_at": "2026-09-27T10:00:00Z",
"segments": 1,
"error": null,
"metadata": {
"source": "quickstart"
},
"links": {
"self": "https://api.acme.example/v1/messages/msg_01J9ZKQ4"
}
}
```
## Check delivery status
```mermaid
sequenceDiagram
participant App
participant API as Acme API
participant Phone
App->>API: POST /messages
API-->>App: 202, status queued
API->>Phone: SMS
App->>API: GET /messages/msg_01J9ZKQ4
API-->>App: status delivered
```

| Status | Meaning |
| ----------- | ------------------------------ |
| `queued` | Accepted, not yet sent |
| `sent` | Handed to the carrier |
| `delivered` | The carrier confirmed delivery |
| `failed` | See the `error` field |
:::warning
Test keys only send to phone numbers you've verified in the dashboard.
:::
<Accordion>
<AccordionItem title="Why is my message still queued?">
A message to a new number can wait a few minutes for carrier checks.
Resend it from [Send a message](#send-a-message), or go back to
[the overview](/).
</AccordionItem>
</Accordion>Run npx blume dev and open /export-test in a window at least 1,280 pixels wide. Wait for the diagram to draw before you export. Mermaid renders in the browser after the page loads, and a diagram that hasn't finished prints as a blank space.
Check the PDF
Switch the site to the light theme first. The print stylesheet keeps the colors of the theme you're viewing, so a dark-mode export comes out as pale text on white paper, or as white text on black pages once backgrounds print.
Choose Export, then Export to PDF. In Chrome, pick Save as PDF as the destination, open More settings, and turn on Background graphics so the callout keeps its fill. In Firefox, the destination is Save to PDF and the option is Print backgrounds. Other browsers word it differently.
The header, sidebar, rail, and breadcrumbs are gone, and the article fills the page. Links stay clickable, and in Chrome they point at full URLs on your site. Compare the rest against the checklist below. On this page, expect three problems: the curl command is cut off at the right margin, response.json prints only its first ten lines under a Show more label, and the accordion prints only its title.
Check the EPUB
Choose Export, then Export to EPUB. The entry reads Generating… while Blume loads the generator and downloads the page's images, then the browser saves export-test.epub. The file is named after the page's path, with slashes turned into hyphens, so /guides/setup becomes guides-setup.epub.
None of the site's CSS or JavaScript goes into the file. Blume rewrites the article as plain HTML with a small built-in e-reader stylesheet, and the result is one chapter:
- The book's title is the page's title.
- It opens on a contents page with a single entry, then the page.
- The author reads "anonymous" and the language is always English, whatever your site's locale. Neither has a setting.
Open the file in Apple Books and in calibre's E-book viewer, and on an e-reader if your readers use one. Reading apps lay out the same file differently, so check each one against the checklist.
Export checklist
What happens to each part of the test page. The PDF column comes from Chrome's print output, and the EPUB column from the file's contents.
| On the page | In the PDF | In the EPUB |
|---|---|---|
| Header, sidebar, rail, breadcrumbs | Removed | Not included |
| Feedback, "Last updated", previous and next links, site footer | Printed | Not included |
| Code highlighting | Kept | Plain monospace |
| Long code lines | Cut off at the margin | Wrapped |
Code block titles, like send.ts | Printed | Dropped |
expandable code blocks | Collapsed to ten lines | Complete |
| Copy buttons and icons | Printed | Dropped |
| Tabs, including package installs | Every panel, under the tab strip | Every panel, with no tab labels |
| Accordions | Title only, unless open when printed | Title and body; the reader decides how to show them |
| Mermaid diagrams | Printed, once rendered | Dropped |
| Images | Printed | Downloaded into the file |
| Links to other pages | Full URLs to your site | Site paths like /, which lead nowhere in a reader |
Two EPUB rows need a closer look in each app. Blume converts colocated images to WebP, which EPUB only made a core image format in version 3.3, so check that images show on the oldest reader you care about. And an image without alt text gets the alt text "image-placeholder" in the file.
Adjust the page for readable exports
Most fixes are to the content, and they make the web page clearer too.
Wrap long code lines
Paper can't scroll, so wrap long lines. Append wrap to one fence (```bash wrap), or wrap every block on the site:
markdown: {
code: { wrap: true },
},Both wrap on screen too. The print rules further down wrap only on paper. See Long lines and long blocks for the other fence options.
Say in the text what the chrome said
The EPUB drops code block titles and tab labels, so don't let them carry information alone. Name the file in the sentence before the block ("Or from your code, in send.ts:"), and name the platform inside each tab panel. The test page's tabs already do this, which is why they still read correctly without labels.
Give every diagram a text fallback
A Mermaid diagram draws itself with JavaScript, so it never reaches the EPUB. Add a sentence after it that states what it shows. When a diagram has to survive in both formats, render it to an image and reference that instead. Move the diagram source into diagrams/delivery.mmd, outside your content folder, then run:
npx -y -p @mermaid-js/mermaid-cli@12.0.0 mmdc -i diagrams/delivery.mmd -o docs/images/delivery.svgReplace the fence with . The image doesn't follow the site's dark theme the way a live diagram does, so save this for diagrams readers need offline. The CLI also draws with Mermaid's own defaults, not Blume's dagre layout and classic look; to match the site, start the .mmd file with a front matter block that sets layout: dagre and look: classic under config:. The Mermaid guide covers writing diagrams in the first place.
Keep must-read content out of collapsed places
Accordions and expandable blocks print collapsed. Use them for optional detail, and keep a step or a required value where it prints.
Host images next to the page
Reference images by relative path, as the test page does, and give each one alt text. The EPUB export downloads every image on the page, and a browser can only download an image from another host when that host allows it with CORS headers.
Tidy the printout with theme.css
For what the content can't fix, add print rules to a theme.css in your project root. This hides the copy buttons, the Show more toggle, the site footer, and everything after the article (related pages, the "Last updated" line, feedback, and previous and next links), prints expandable blocks in full, and wraps long lines on paper only:
@media print {
[data-blume-copy],
[data-blume-code-expand],
[data-blume-footer],
#blume-content > article ~ div {
display: none !important;
}
.prose pre[data-expandable] > code {
max-height: none !important;
mask-image: none !important;
}
.prose pre,
.prose pre code {
white-space: pre-wrap !important;
overflow-wrap: anywhere;
}
}Troubleshooting
There's no Export action
Check that export is set in blume.config.ts, that the window is at least 1,280 pixels wide, and that the page uses the default layout mode. API operation pages never show it.
Export to EPUB does nothing
When the entry flips from Generating… back to Export to EPUB without a download, open the browser console. [blume] EPUB export failed followed by a fetch error (Chrome's is Failed to fetch) means an image couldn't be downloaded, usually one on another host without CORS headers. One failed image stops the whole export, and the reader sees no message. Download the image into your content folder and reference it by relative path.
An image is missing from an EPUB made in dev
In blume dev, optimized images come from Astro's image endpoint, a URL with no file extension, and the EPUB records them without a media type. Do your final EPUB check on a production build:
npx blume build
npx blume previewThe PDF has pale text or black pages
It was printed from the dark theme. Switch to light and export again.
A diagram is blank in the PDF
It was printed before Mermaid finished drawing it. Wait until the diagram shows on screen, then print again.
Next step
Turn on page export
Add export: true to blume.config.ts, then export your longest page in both formats and check it against the checklist.
Read the export docsA step here not working for you? Report a broken step.