Bundling PDF.js in a WordPress Plugin (Without a CDN)

Why Composer isn't the right tool, why CDNs are out for clients with strict third-party policies, and how to ship pdf.js + the worker locally with wp_localize_script. A working pattern for any production WordPress plugin that needs to render PDFs.

Why not Composer

PDF.js is a JavaScript library. Composer is a PHP dependency manager. There's no PHP code to install — the package would just be Composer pretending to manage a folder of JS files, with all the version-pinning ceremony and zero of the actual benefit. The 'right' frontend tool is npm or yarn, but most WordPress plugins don't ship a build pipeline. So the practical answer is: download the pdf.js dist files, commit them to your plugin, and serve them directly.

Why not a CDN in production

CDNs work great in development. In production, some clients block third-party CDN domains at the firewall (cdnjs.cloudflare.com, unpkg.com) for security or compliance reasons. Some have strict CSPs that don't allow external script sources. Some are serving from a region where the CDN edge is slower than their own server. None of these are exotic edge cases — at least one will hit you on every multi-client engagement. Bundling locally means your plugin works the same on every install.

Directory layout

Inside your plugin, create `assets/vendor/pdfjs/` and drop in `pdf.min.js`, `pdf.worker.min.js`, and the `web/` folder if you want the prebuilt viewer. Total weight is around 1MB minified — not nothing, but cached forever after first load. Pin to a specific pdf.js release in a top-level VERSION file or your readme so future maintainers know which version they're auditing.

Setting workerSrc the WordPress way

PDF.js needs to know where its worker script is at runtime. Hardcoding the URL breaks under multisite, subdirectory installs, and translated permalinks. The clean pattern is `wp_localize_script` to pass the worker URL to your viewer script: `wp_localize_script('your-pdf-viewer','PDFJSConfig',['workerSrc' => plugins_url('assets/vendor/pdfjs/pdf.worker.min.js', __FILE__)]);`. Your JS then reads `PDFJSConfig.workerSrc` and passes it to `pdfjsLib.GlobalWorkerOptions.workerSrc`.

Text layer for accessibility

By default, pdf.js can render canvas-only — which is gorgeous and totally unreadable to screen readers. For any educational content, you must enable the text layer. The text layer renders an invisible HTML overlay aligned with the canvas, which screen readers and search-in-page can both consume. The tradeoff is a small performance hit and slightly larger DOM, both worth it. If the source PDF is untagged (no StructTreeRoot), the text layer is still useful for find-in-page even though screen readers won't get semantic structure.

Where the security illusion ends

Bundling pdf.js does not stop a user from downloading the PDF — it just controls the UI. Combine with an S3 bucket policy (`aws:Referer` or signed URLs) for the actual access control, and optionally a server-side proxy if your CSP forbids direct S3 fetches from the browser. We've shipped both patterns in production. The choice depends on whether the client wants the URL to be invisible to the browser entirely (proxy) or whether referer-restriction is enough (direct fetch, simpler). Either way: PDF.js is the rendering layer, not the security layer.

Last updated: