Skip to the document

Exporting from the command line

Exporting PDFs covers the button. This page is for scripts and CI: plicine export-pdf, the flags that make an export fail loudly, the settings a document and a vault can carry, why two exports of one document are byte-identical, and choosing the browser that renders.

The command

plicine export-pdf handbook/welcome.md

This writes welcome.pdf in the current directory. Useful flags:

-o, --out <file>    Where to write the PDF (default: <doc-name>.pdf)
--theme <id>        Theme id or hash to render with, instead of the document's own
--vault <dir>       The vault root, if it can't be found by walking up from the document
--frozen            Fail instead of updating blocks.lock if resolution has drifted
--strict            Exit non-zero if the export produced any warnings

If --vault is omitted, Plicine finds the vault root by walking up from the document's own folder looking for a .vault/ folder. The command prints how many pages it produced and how long it took, and prints any warnings to stderr, for example when a block wasn't approved and printed as a placeholder instead.

Page size

The page size is A4 or Letter, chosen in this order: the document's own frontmatter page:, then the vault's vault.json page, then the theme's own default, then A4 if none of those say otherwise.

Covers, headers and footers

A document gets the theme's cover page when its frontmatter sets cover: true and the theme defines a cover template. Without the key there is no cover. Running headers and footers come from the theme's header and footer templates, if it defines them, repeated on every page after the cover by the theme's own print stylesheet; a theme with no header or footer template simply has none. See How themes work and Writing a theme for how a theme controls all of this.

The credit line

A PDF in the built-in theme ends with one small line at the foot of its last page: "Made with Plicine · plicine.com". It's there once, it's never on the cover or in the footer, and it never adds a page. A document in any other theme doesn't have it, unless that theme's author chose to carry it.

To turn it off for one document, add this to its frontmatter:

---
credit: false
---

To turn it off for every document in the vault, switch off "Made with Plicine" line in Settings, or write this to .vault/vault.json by hand:

{
  "credit": false
}

Switching it back on takes the key out again, since on is the default.

A document's frontmatter wins over vault.json, so credit: true puts the line back on one document in a vault that turned it off. Turning it off is free and nothing else about the PDF changes. A document exported before the line existed gains it the next time it's exported, which changes the file's bytes; set credit: false if the PDF has to stay exactly as it was.

A PDF is one document, read by someone who has neither the vault nor the app, so a link is kept only when it still has somewhere to go.

The link In the PDF
To another site, https://…, or mailto: A link, as written
To a heading in the same document: [text](#heading), [[#Heading]], a toc entry A link that jumps to the heading
To another document: [text](pricing.md), [[Pricing]], a row of a children block Text
To a file in the vault: [[sample-chart.svg]], [text](assets/terms.pdf) Text

A link printed as text keeps its words and its place in the sentence and takes the colour of the text around it. Nothing warns about it, since it's what every such link does.

The reason is the way a PDF is made. Plicine draws the pages in a browser, from a local server that stops when the export does. A link into the vault is relative, so the browser would write it into the PDF against that server's address, http://127.0.0.1:52515/print?doc=pricing.md, which is dead by the time anyone clicks it and has no business in a file you send. strings lorem.pdf | grep /URI lists every address a PDF links to.

A theme can set those links apart again: see a:not([href]) in Theme package. The paginated layout in the app keeps them as links, because a click there opens the document.

Unapproved blocks, and --strict

A block that hasn't been approved on the machine doing the export doesn't run, whether that's the app or the CLI: it prints as a plain placeholder, and a warning records that it wasn't run. By default, export-pdf still writes the PDF and exits with status 0 even with warnings like this, so an export doesn't fail just because nobody has run plicine trust yet. Pass --strict to change that: with --strict, any warning at all (an unapproved block, an unknown block, invalid block data) makes the command exit with a non-zero status instead of silently shipping a PDF full of placeholders. This is the flag to use in a script that should fail loudly rather than produce a quietly incomplete PDF.

--frozen for CI

--frozen is for automation that should never modify the vault. Normally, opening a vault (including to export a PDF) recomputes blocks.lock and writes it if resolution changed. With --frozen, that write never happens; instead, if the freshly resolved blocks don't match what's already in blocks.lock, the export fails and lists exactly what's out of date, so CI can catch a vault whose lock file is stale before it ships a PDF built against the wrong block versions.

Render cache

Exported PDFs are cached, keyed by the document's content, the theme, the page size, whether the credit line is on, every block's resolved status and hash, the {{placeholder}} values the document reads, its link, the list of files a link or an image can point to, and the version of Plicine's own renderer. Exporting the same document again, unchanged, returns the cached PDF straight away (the CLI's summary line says (cached)). Upgrading Plicine invalidates the cache automatically, since the renderer's own hash is part of the cache key.

The values, the link and the credit line are in the key because they can change without the document changing. The theme's cover, header and footer read the values merged from values in .vault/vault.json, the frontmatter of each folder's INDEX.md and the document's own frontmatter, and the link at the end of the PDF comes from publicUrl in vault.json when the frontmatter has no link. Change an organisation's name in vault.json and the next export of every document renders again, with the new cover. credit in vault.json turns the credit line off the same way.

Determinism, and its limits

Plicine normalises the PDF it produces, fixed creation and modification timestamps, no random document id, no XMP metadata block, so exporting the same document from two different data folders on the same machine produces byte-identical files. That's tested and relied on.

What it can't guarantee is identical bytes across different machines: a different Chrome version, or different fonts installed on the system doing the rendering, will change the output even from identical input. It matters most when a theme leans on a system font rather than shipping its own, which is why the built-in theme packs every font it uses and draws its mark as outlined paths. Using the downloaded renderer on every machine (rather than whatever Chrome each has) removes the version half of that, since it's the same pinned build everywhere.

Where the renderer comes from

PDF export needs a browser to render in, and Plicine looks for one in this order:

  1. PLICINE_CHROME, or PUPPETEER_EXECUTABLE_PATH if that isn't set. If it points at a file that isn't there, the export fails rather than quietly searching on.
  2. The chrome-headless-shell Plicine has downloaded, in <app data>/chrome/<version>/.
  3. Google Chrome, Chromium, Microsoft Edge or Brave, installed in the usual place for your platform.

If none of those answers, the first export downloads a pinned build of chrome-headless-shell from Chrome for Testing, about 100 MB depending on the platform, into the app data folder. The app shows "Downloading the PDF renderer" with a percentage while it runs, and so does plicine export-pdf. It happens once per machine: after that, exports start straight away. Nothing is downloaded while step 1, 2 or 3 has an answer.

You can fetch it ahead of time, or on a machine that has Chrome anyway:

plicine chrome download

plicine chrome status says which browser an export would use, and exits 1 when there isn't one, which is the check to put in a script. plicine chrome remove deletes the download.

To use a browser of your own instead, and skip downloading anything:

PLICINE_CHROME="/Applications/Chromium.app/Contents/MacOS/Chromium" plicine export-pdf handbook/welcome.md

If the download fails, on an offline machine or behind a proxy that blocks it, the error names the folder it tried to write to and points at PLICINE_CHROME. That's the way out: install a browser, or put one on the path, and export again.

Why the version is pinned

The download is one fixed Chrome for Testing release rather than whatever is newest, because the renderer decides the bytes. Two machines on the same pinned build produce far more alike PDFs than two machines on whatever Chrome each happened to install, which is what the determinism section above is about. Upgrading the pin is a deliberate change in a release, and the old folder can be deleted with plicine chrome remove.

Troubleshooting

  • Timeout: the export waits up to 60 seconds for the print page to finish. If it times out, the error includes any browser console errors captured along the way, usually the actual cause, a block or theme template that threw, rather than a generic timeout message on its own.
  • Fonts look wrong, or text isn't selectable: themes should ship their own static (non-variable) font files rather than relying on system fonts or variable font files. Chrome embeds variable fonts in PDFs as Type 3 fonts, which render and search worse than a proper static TrueType or OpenType font.
  • The download won't finish, or there's no network: set PLICINE_CHROME to an installed Chrome, Chromium or Edge, or install one and export again. plicine chrome status shows what Plicine can see.
  • Blank or broken pages: if the whole render failed rather than one block, the PDF export throws with the underlying error rather than producing an empty file; check stderr for the message. If only part of the page is missing, look for an unapproved-block warning first, that block printed as a placeholder rather than its actual content.