Private betav0.1.0
Docs
Format developer documentation

Assets

How to bundle assets from Format Studio.

You might've already read about Format's approach to assets. This page runs you through how to apply those concepts to the documents you have built in Format Studio and want to deploy to production servers.

Compiling assets

When you bundle your documents, you have a few choices about how to handle your assets. This is controlled using the --assets flag.

This flag gives you three choices about how to deal with the assets your documents use.

  • Static: zips up your assets upfront, at compile time
  • Dynamic: outputs raw assets and automatically zips them at runtime, inside your app, right before calling the API
  • None: outputs no assets or ZIP, leaving you to handle

Static

Using the static setting is meant for when every asset is known upfront at build time. Imagine an invoice PDF with a company logo at the top, a font face file and a few other logos in the footer.

Assuming these have all been referenced correctly, as per the Images and Fonts pages, running compile will create a ZIP archive of assets from:

  • Module imports (import banner from './banner.jpg')
  • The document's assets directory
  • The sharedAssetsDir

An assets ZIP will be created per-document. It's a portable archive of all the assets that document needs to render.

npx format compile --assets static

# or just:

npx format compile

Results in:

_generated
invoice
assets.zip
index.js
another-pdf
assets.zip
index.js

Note: The above is a simplification of the actual compiled output.

Using static is safe and performant, as we know our assets always exist when we generate our PDF and the ZIP has been created upfront, so that compute doesn't eat into our end-to-end generation.

There are two main downsides to using static:

  1. Everything in your document's assets directory and sharedAssetsDir is zipped, regardless of whether its actually used in the document. This can potentially increase the file size of the ZIP for no good reason.

  2. You can't use assets that aren't known upfront, such as a logo that's been uploaded by a client or a customer avatar.

Remote references at compile time

Static mode bundles every asset upfront, so remote URLs have to be fetched then too. By default they aren't. Compile warns and lists any remote references it finds, and those assets will be missing from the generated PDF.

Pass --remote-assets to fetch each remote reference and bundle it into the document's assets.zip:

npx format compile --remote-assets

This flag applies to static mode only. In dynamic mode the ZIP is built at render time, so remote assets are opted into there instead, using remoteAssets (see below).

Dynamic

Dynamic mode builds the assets ZIP at render time, inside your own runtime, instead of at compile time. When you compile, the known asset files are placed next to your bundle with a record each one lives. It won't zip anything yet.

npx format compile --assets dynamic

Results in:

_generated
invoice
assets
banner.jpg
another-png
assets
signature.png
shared-assets
logo.png

When your bundle is called at runtime, it scans the HTML that render produced and builds a ZIP on the fly. The ZIP will only contain the assets that the document references.

There are two reasons to choose dynamic over static.

  1. Smaller, per-render ZIPs. Static mode zips every asset in your document assets directories, used or not. Dynamic mode zips only what the rendered page references. A statement template that ships 500 tenant logos but uses one per render builds a ZIP holding that single logo.

  2. Remote and uploaded assets. A reference can point at a URL you only know at render time, like a customer-uploaded avatar passed in as data. See below.

The trade-off is that building the ZIP now happens during PDF generation, in your runtime, rather than once upfront. Although, for most documents this is negligible.

Remote references at render time

By default, the runtime ZIP build only includes local assets. If a rendered document references a remote URL with remote assets off, the build throws rather than reaching out to the network unprompted. A ZIP missing that asset would produce a broken PDF, so Format fails loudly at the source instead of shipping a silent gap.

This matters for assets you only know at render time. A user's avatar is the classic case. They uploaded it earlier, it lives on your CDN, and the URL is different for every render, so you can't bundle it upfront. The document has to point at the live URL.

Pass that URL in as render data and reference it in your document:

<img src={data.avatarUrl} />

Then set remoteAssets.enabled via setZipOptions for the render. @format.dev/zip fetches the avatar and bundles it into the assets ZIP for the final PDF render:

import { FormatClient } from '@format.dev/client'
import { membershipCard } from './_generated'

const format = new FormatClient()

// Fetch remote references and bundle them into each render's assets.
membershipCard.setZipOptions({ remoteAssets: { enabled: true } })

// avatarUrl comes from your database, a photo the user uploaded earlier.
const card = await membershipCard.render({
  name: user.name,
  avatarUrl: user.avatarUrl // e.g. https://cdn.yourapp.com/avatars/8c1f2a.jpg
})

const pdf = await format.pdf(card)

The avatar lands in the PDF alongside your bundled fonts and icons. Leave remoteAssets off and the same render throws, surfacing the remote reference instead of silently dropping it.

None

None mode tells Studio to leave assets alone. It emits no ZIP, builds no asset map, and skips asset checks. You're responsible for making sure every asset is reachable when the PDF is generated.

npx format compile --assets none

Reach for this in two cases.

Your assets are already hosted. If every image and font in your documents is an absolute URL, such as https://cdn.example.com/logo.png, there's nothing for Studio to bundle. The renderer fetches them directly. We do not recommend this.

You manage the ZIP yourself. If you'd rather build and host the assets archive on your own terms, compile with none and point each render at your archive with setAssetsUrl().

Because nothing is bundled or checked, a broken or unreachable asset path won't surface until render time. None mode trades any Studio safety nets for full control.

Choosing a mode

Here's how the three modes compare.

static (default)dynamic
When the ZIP is builtCompile time, onceEvery render, in your runtime
What's in the ZIPEvery imported asset, and everything in your document assets directoriesOnly what the rendered page actually references
Remote or uploaded assetsFetched at compile time, opt in with --remote-assetsFetched per render, opt in with setZipOptions()
Reference validationYesYes
Per-render costNo, but potentially larger ZIPYes, but potentially smaller ZIP

And here's which one to pick.

If…Use
All your assets are known upfront and ship with the documentstatic
You want the simplest setup and predictable, fast rendersstatic
You have many shared assets but each document uses only a fewdynamic
References point at uploaded or remote URLsdynamic
Every asset is already hosted at a URLnone
You build and host the assets archive yourselfnone

CDN-hosted ZIP

If you're using the static assets mode and your runtime can't read from the filesystem (browser clients, edge functions, or serverless platforms without persistent disk), upload your asset ZIPs to a CDN or object storage during CI and point to them at runtime with setAssetsUrl().

  1. Compile your documents with --assets unset or set to static. Asset ZIPs are generated per-document in _generated.
  2. Upload the ZIPs to a CDN (S3, R2, Vercel Blob, etc.).
  3. Call setAssetsUrl at runtime before render().
invoice.setAssetsUrl('https://cdn.example.com/assets/invoice/assets.zip')

Uploading assets

Upload the asset ZIPs after compiling, as part of your build or CI pipeline:

# S3
aws s3 sync _generated/ s3://my-bucket/format-assets/ \
  --include "*/assets.zip" --exclude "*"

# Cloudflare R2
wrangler r2 object put assets/invoice/assets.zip \
  --file _generated/invoice/assets.zip

# Vercel Blob
# Upload programmatically using @vercel/blob

Cache headers

Asset ZIPs are reproducible: the same assets always compile to the same bytes. So you can cache them aggressively, as long as each URL changes when its contents change. Put a version or a content hash in the upload path, then serve the ZIPs with immutable cache headers so browsers and CDNs never re-download one they already have.

Cache-Control: public, max-age=31536000, immutable

Version the path per release:

aws s3 sync _generated/ s3://my-bucket/format-assets/v1.2.0/ \
  --include "*/assets.zip" --exclude "*"

Because the bytes are reproducible, you can also hash each ZIP and use the hash as its path. Unchanged assets keep the same URL from one build to the next, so you skip both the re-upload and the re-download.

Was this page helpful?