Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Nova Pdf Laravel Package

padocia/laravel-nova-pdf

Laravel Nova PDF adds PDF handling to Nova resources, letting you generate, preview, and download PDF documents from your admin panel using familiar Laravel and Blade workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

Verify the runtime before you write a line of code. This package is a thin Nova Action wrapper around Browsershot (headless Chromium via Node/Puppeteer). composer require gives you plumbing, not a renderer. On the same image you deploy, confirm:

node -v && npm -v
which chromium || which google-chrome
# or, if Puppeteer manages the binary:
npx puppeteer browsers list

If Chromium is missing or Node is broken, stop. No config value fixes an absent binary. This check is the difference between a 20-minute spike and a two-day debugging session.

Install and publish both config and views.

composer require padocia/laravel-nova-pdf
php artisan vendor:publish --provider="Padocia\LaravelNovaPdf\LaravelNovaPdfServiceProvider"

Publish both the config and the Blade stubs. Commit the published templates to your app. They are the artifact your team will iterate on daily; the package around them is small glue.

Read the action source first, in this order:

  1. src/Actions/* — how the action pulls the model and what it passes to the view. That is your template contract.
  2. config/laravel-nova-pdf.php — options forwarded to Browsershot (nodeBinary, chromePath, format, margins, waitUntil, timeout).
  3. The published Blade stub — your starting point for layout.
  4. handleBrowsershotOptions — the one method patched in v1.0.2, and therefore the least-tested surface in the package. Treat it as suspect until you've verified each option you rely on.

First use case: one record, one PDF, from a Nova detail view. Add the action to a resource's actions() method:

public function actions(NovaRequest $request): array
{
    return [
        \Padocia\LaravelNovaPdf\Actions\PdfAction::make()
            ->onlyOnDetail(),
    ];
}

Open a record, run the action, confirm you get bytes. That validates the whole pipeline (model → Blade → Browsershot → HTTP response) in one shot. If it works, you now have a known-good baseline to break as you customize templates.

Implementation Patterns

One Blade template per document type, not per resource. Nova resources map to domain objects; documents map to audiences. An Invoice resource may need customer-invoice.blade.php and warehouse-packing-slip.blade.php. Keep them at resources/views/pdf/<domain>/<variant>.blade.php, select the view through the action, and never branch inside one giant template. A per-document template is a clean review surface when ops asks for a tweak; a monolithic template is a weekly maintenance tax.

Pass a flattened view-model array, not the Eloquent model. The action's job is to project the model into primitives: formatted currency, pre-computed totals, absolute image URLs, string dates. Templates that reach into $model->relation->method() fire lazy loads during rendering, turn Chromium's render into an N+1 machine, and make the template untestable. If you need an audit snapshot, capture it in the action before rendering.

Wrap the render behind your own interface immediately.

interface PdfRenderer {
    public function render(string $view, array $data): string; // raw bytes
}

Have the Nova Action call $this->renderer->render(...). Every future need — queueing, swapping to dompdf, adding footers, injecting fixtures into a test — becomes one file. The package is a 2021 wrapper with a one-maintainer footprint; do not let your controllers or actions couple to its class names.

Make inline vs. attachment an explicit choice. Stream inline (Content-Disposition: inline) for preview, attachment for download. Expose it as two buttons or a select field in the action form so operators know what they're pressing. Hiding it in a config default invites support tickets.

Queue the render as soon as it exceeds ~1–2 seconds. A synchronous Chromium launch inside a Nova request will eventually meet a cold worker, a slow CDN font, or max_execution_time. The durable pattern:

  1. Action dispatches GeneratePdfJob with the record ID and template variant.
  2. Job renders and writes to a private disk under a UUID filename.
  3. Job stores metadata (owner, template, disk path, expiry) in a small pdfs table.
  4. Action returns Storage::temporaryUrl(...) or a signed, short-lived route.

If you must stay synchronous, set an explicit timeout and produce a timestamped filename — never a raw sequential ID.

Subclass the action for per-resource customization. Override template resolution, filename construction, and authorization in class InvoicePdf extends PdfAction. Never edit files under vendor/ — they vanish on the next composer update and confuse whoever reads the code later.

Authorize inside handle() even when Nova's policy is configured. Resource-level checks are not a reliable guarantee that a view policy runs when an action fires on a specific record:

$this->authorize('view', $this->resource);

An export is an egress path and must be treated like one, especially once the action is exposed on the index table — "select all, export" is one click away.

Read it as a reference implementation before adopting it as a dependency. The whole pattern is ~150 lines of your own code: an Action, a Blade view, Browsershot::html(), response()->streamDownload(). Pinning this package is a legitimate spike; pinning it as long-term infrastructure is a different decision that the maturity profile below should inform.

Gotchas and Tips

Browsershot is the entire risk surface. In Docker you will probably need --no-sandbox to start Chromium under an unprivileged user. That is a real security downgrade: an unsandboxed Chromium rendering admin-influenced HTML can reach file:// paths, localhost services, or metadata endpoints. Mitigate deliberately:

  • Base64-embed fonts and images rather than fetching remote URLs.
  • Never interpolate user-controlled URLs directly into the template.
  • Run the render in a worker whose network egress is scoped (no metadata endpoint, no cluster-internal DNS).

SSRF is a design concern here, not a footnote.

handleBrowsershotOptions was broken until v1.0.2. A patch release whose entire changelog is "fix option plumbing" tells you the config path shipped once with a real bug. Verify every option you rely on — format, margins, waitUntil, timeout, nodeBinary, chromePath — by rendering a page with known measurements and inspecting the output. Trust the pixels, not the config file.

Missing logos/CSS means the render finished before assets loaded. Use waitUntilNetworkIdle() (or the equivalent in your pinned Browsershot). The waitUntil semantics have shifted across Browsershot versions — check the version you actually have, not the docs for latest.

Missing or wrong fonts: the container has none. Slim base images ship almost no system fonts, so Chromium silently substitutes and your layout drifts. Install the exact font pack your templates reference and set font-family explicitly in CSS.

Pin everything. Pin this package, spatie/browsershot, and the Chromium image tag. A 2021-era constraint resolved against a modern Browsershot will either fail install or blow up at runtime — Browsershot::html(), path setters, and option names have all moved. If Composer drags in a newer Browsershot, expect to patch.

Nova 4/5 compatibility is unverified. The code is Nova 3 / Laravel 8 vintage (last release July 2021). Action signatures, tool registration, and field APIs have changed across majors. Before planning template work, branch a throwaway install against your Nova version and fire one action. A fatal is cheap to find now and expensive after a sprint. If it fatals, the honest fallback is your own 150-line action — you'll own it anyway.

Storage is a privacy question. If the action persists files, confirm the disk is private and the path is unguessable. storage/app/public with a sequential filename is a URL anyone with the link can enumerate. Use a private disk plus Storage::temporaryUrl() with an expiry matched to your retention policy.

Zero dependents and 23 stars means you are the QA. Run composer audit and a Chrome/Puppeteer CVE scan as part of your install gate, and write a one-paragraph ADR recording the fork-and-replace plan while the code is small. This package saves hours, not weeks — and only if you accept ongoing ownership.

Debugging order when a PDF fails or comes out blank:

  1. Render the Blade view as plain HTML in a browser. Is the markup right?
  2. Run Browsershot standalone against that HTML, outside Nova and outside Laravel. Does Chromium launch and emit bytes?
  3. Compare nodeBinary and chromePath in config against the container's real paths.
  4. Check the worker's memory limit — Chromium OOM kills show up as blank output or a silent exit, not a clean exception.
  5. Check authorization — a policy denial renders as "nothing happened," easy to mistake for a render bug.

Extension points worth knowing: template resolution (override the view name), filename construction, response mode (inline vs attachment), Browsershot options via config or an overridden handleBrowsershotOptions, and the action's fields() for adding a template selector. All reachable from a subclass; none should require touching vendor/.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource