Free · no editions, no licensing code
A PDF should not be a link
“Download our brochure (PDF, 4.2 MB)” is not an embed — it is an invitation to leave the page and find out whether the document was worth opening. And a .docx is worse: no browser renders one at all, a private asset has no URL to hand a viewer, and a CSV needs no viewer in the first place, just a server willing to read it. Book is those four problems, solved together.
Two commands, then one tag
Install it and add a document. From there it is one Twig call, or a reference tag an author can write inside CKEditor — and the tag renders because Craft parses reference tags, not because an editor plugin is installed.
composer require justinholtweb/craft-book
php craft plugin/install book
{# One call takes an asset, a URL, a handle, a document or a field value #}
{{ craft.book.embed(entry.brochure) }}
{{ craft.book.embed('annual-report', { viewer: 'google', height: 900 }) }}
{# And in any rich-text field, with no template change at all: #}
{book:annual-report:render}
{book:annual-report:render(click,wide,no-download)}
{# What would happen, asked before you publish: #}
{% set verdict = craft.book.resolve(entry.spec) %}
{% for warning in verdict.warnings %}<p>{{ warning }}</p>{% endfor %}
Features
Everything Embed Any Document does for WordPress, done as a Craft element — plus the two things it cannot do.
Five viewers, and a verdict
Native, Microsoft, Google, Book's own inline renderer, and a download card. Auto is not a sixth viewer — it is the answer to what will actually work on this site, this format, these settings.
- Each format carries its own ordered preference
- Ruled-out viewers come with the reason they were ruled out
It never phones anybody
Book makes no outbound HTTP request. Anywhere. Google's and Microsoft's viewers are iframe URLs the reader's browser loads; inline rendering reads bytes through your volume's own filesystem.
- No SSRF surface, no host allowlist, no proxy settings
- Turn both third-party viewers off and no URL ever leaves
Private assets stay private
An asset in a volume with no public URLs is served on Book's own route, with an HMAC-signed token Book minted. The token carries the access rule — read out of the token, never out of a query parameter.
- No volume has to be made public for one brochure
- access: login sends a guest to the login screen
Some files need no viewer
CSV becomes a table, Markdown becomes purified prose, JSON is pretty-printed, and text, logs and code become an escaped scrollable block. Rendered by Craft, with nobody else involved.
- Delimiter sniffed, quoted commas kept, Windows-1252 repaired
- Row limits are honoured and admitted to, not hidden
A reference tag, not a shortcode
A document has a handle, and {book:handle:render} renders in CKEditor, in Redactor and in any HTML field — because Craft parses reference tags, not because an editor plugin is installed.
- Switch every editor integration off and nothing stops rendering
- Paste a file link on its own line and it joins the library
Consent that actually defers
Click-to-load parks the frame in a template element, which is the only markup that genuinely defers the request. A hidden iframe still loads — which is the mistake most consent banners make.
- Ask before a third party downloads your file, and mean it
- A load timeout shows the file itself instead of a blank box
Frequently Asked Questions
The questions worth answering before you install it.
Yes. One edition, everything switched on, no licence key and no licensing code in the plugin. All five viewers, the element, both fields, the Twig API, the editor integrations and private-asset delivery are simply the plugin.
Book itself never makes an outbound HTTP request. Not at install, not on a page view, not ever — there is no fetcher in the codebase.
What it can do is write a URL into an <iframe src> that the reader's browser then loads. For the google and office viewers, that browser tells Google or Microsoft to go and download your file so they can render it. Book writes the URL; it never makes the request.
That distinction is why there is no SSRF surface, no host allowlist and no proxy settings to maintain — but it does not change the privacy consequence. If a third party must not have your documents, turn both viewers off. Then no URL reaches them, auto skips them, and an explicit request for one is refused with a reason.
Yes, and this is most of the reason the plugin exists. An asset in a volume with no public URLs is served through Craft on Book's own route, with an HMAC-signed token Book minted. The token carries the access rule, so it cannot be edited into permission it was not granted, and an unsigned request only ever reaches a file already published at its own volume URL.
The one thing a private document cannot do is use Google's or Microsoft's viewer, because those render by downloading the file from their own servers — there is no way to let them in without letting everybody in. Book will not offer them for a login-gated file. Use native for PDFs and images and inline for CSV, Markdown, JSON, text and code; all of them work perfectly on a private volume.
Microsoft's viewer, or Google's. No browser renders Word natively and Book does no conversion, so those are the only two options that are not a download link.
Converting on your own server means LibreOffice or Gotenberg — a very different dependency footprint, and deliberately out of scope. If you cannot use a third-party viewer and cannot install a converter, export to PDF; the browser renders that with nobody else involved.
The first viewer the format supports that is usable on this site. Each of the eighteen formats carries its own ordered preference: a PDF tries native, then google, then link; a .docx tries office, then google, then link; a CSV tries inline first. “Usable” means allowed by your settings, publicly reachable if a third party has to fetch it, and readable by Book if Book is doing the reading.
When it cannot use the one you would expect, it says so — on the edit screen, in craft.book.resolve(), and on the page under devMode. Set fallback: false if you would rather an impossible choice became a download card than quietly became a different viewer.
Because Google and Microsoft render your file by downloading it from their own servers, and a URL on .ddev.site, .test or a private-network address is not reachable from theirs.
With checkPublicUrl on — the default — Book refuses to hand those hosts over, falls back to a viewer that works, and says on the edit screen that it did. This is the single most common way a document embed fails, and it fails only in the place where nobody is watching. It is not something to fix before launch: the same document works the moment the site is on a public domain.
Only if you want to. A library document has a handle, and {book:handle:render} renders in CKEditor, in Redactor and in any other HTML field, because Craft parses reference tags in every rich-text value before it reaches the page.
For documents outside rich text, one Twig call does it: {{ craft.book.embed(entry.brochure) }}.
No, and this is deliberate. The CKEditor toolbar button, the Redactor plugin and the shared picker are editing affordances — they help an author write a reference tag. Rendering is Craft's reference-tag parsing, which happens with or without them. Set richTextIntegration to false and every existing document keeps rendering exactly as before.
A document rendered with native, google, office or link costs an element lookup and some markup; the file itself is fetched by the reader's browser, lazily by default.
inline is the one to think about: Book reads the whole file into PHP to render it, capped by inlineMaxBytes at 1 MB. That is fine for a price list and wrong for a 400 MB log, which is what the cap is for.
Serving files through Craft — automatic for private volumes, optional for public ones — puts bytes through PHP rather than the web server. Range requests are supported, so seeking in a video still works.
Embed Any Document is the WordPress plugin Book is pitched against, and it covers the same ground. The differences are the ones you would expect from doing it the Craft way: an element with a reference tag rather than a shortcode, so the same document is one thing to keep in step instead of one copy per page; assets as first-class sources, so private volumes work without being made public; a Twig API that is the point rather than an afterthought, including resolve(); and inline rendering for the text formats, with no third party involved at all.
Yes, two ways. The stylesheet is small and expressed entirely as custom properties on .book, so overriding four variables usually does it. For the markup itself, put your own template at templates/_book/document.twig — it wins everywhere at once, because every surface renders through that one template. Both the stylesheet and the runtime can be switched off if you would rather ship your own.
Craft CMS 5.3+, PHP 8.2+. No runtime dependencies beyond Craft's own, and no build step — the control panel editor, the picker and the front-end runtime are plain scripts, and the CKEditor plugin is an ES module written against the ckeditor5 import map.
Put the document on the page
Free, for Craft CMS 5.3 and later. Install it, add a document, and embed it in Twig or in rich text — with private assets that stay private and no outbound request anywhere in the plugin.