A complete photobook editor with a layouts library, photo auto-fill, live design validation, and export to print-ready PDF/X-4.

Pre-requisites#
This guide assumes basic familiarity with React and TypeScript.
- Node.js v22+ with npm – Download
- Supported browsers – Chrome 114+, Edge 114+, Firefox 115+, Safari 15.6+
See Browser Support for the full list
Get Started#
Start fresh with a standalone Photobook Editor project. This creates a complete, ready-to-run JavaScript application with a start screen, an editor, and an export pipeline.
Step 1: Clone the Repository#
git clone https://github.com/imgly/starterkit-photobook-editor-react-web.git
cd starterkit-photobook-editor-react-webgit clone https://github.com/imgly/starterkit-photobook-editor-react-web.git
cd starterkit-photobook-editor-react-webnpx degit imgly/starterkit-photobook-editor-react-web starterkit-photobook-editor-react-web
cd starterkit-photobook-editor-react-webnpx degit imgly/starterkit-photobook-editor-react-web starterkit-photobook-editor-react-web
cd starterkit-photobook-editor-react-webThe kit is split into a web app, an optional export server, and the wire types they share:
src/client/ # The web app (Vite + React)src/server/ # The optional export server (Express + @cesdk/node-native)src/shared/ # Wire types shared by the app and the serverInside src/client/src, the demo application lives in app/ and the reusable editor logic lives in imgly/:
src/client/src/├── index.tsx # Application entry point├── app/ # Demo application shell└── imgly/ ├── index.ts # Editor initialization function ├── photobook.ts # Sizes, styles, and the scene each starts from ├── photos.ts # Upload asset source for the reader's photos ├── autofill.ts # Places photos and captions into placeholders ├── placeholders.ts # What counts as unfilled placeholder content ├── photo-stash.ts # Holds photos that a smaller layout cannot fit ├── preview-mode.ts # Page-spread preview ├── block-utils.ts # Engine helpers shared across the kit ├── constants.ts # Shared identifiers and panel helpers ├── plugins/layouts/ │ └── layout.ts # Layouts asset source and layout switching ├── validation/ │ ├── validation.ts # The print validation checks │ ├── types.ts # Validation result types │ └── utils.ts # Validation helpers └── config/ ├── plugin.ts # Main configuration plugin ├── actions.ts # Export/import actions ├── features.ts # Feature toggles ├── i18n.ts # Translations ├── settings.ts # Engine settings ├── keyboard/ # Keyboard shortcut catalog └── ui/ # UI customizationStep 2: Install Dependencies#
Install the required packages:
npm installnpm installpnpm installpnpm installyarnyarnStep 3: Download Assets#
CE.SDK requires engine assets (fonts, icons, UI elements) to function. These must be served as static files from your project’s public/ directory.
curl -O https://cdn.img.ly/packages/imgly/cesdk-js/1.83.0/imgly-assets.zip
unzip imgly-assets.zip -d public/
rm imgly-assets.zipcurl -O https://cdn.img.ly/packages/imgly/cesdk-js/1.83.0/imgly-assets.zip
unzip imgly-assets.zip -d public/
rm imgly-assets.zipconst config: Configuration = { // ... baseURL: '/assets' // ...};This kit also loads its own demo assets — the layout scenes, the style scenes, and the example photos — from the IMG.LY CDN. To host them yourself, copy the kit’s asset bundle to your own server and point VITE_DEMO_ASSETS_BASE_URL at it:
VITE_DEMO_ASSETS_BASE_URL=https://your-server.example.com/assetsThe default is set in src/client/src/imgly/demo-assets.ts.
Step 4: Run the Development Server#
npm run devnpm run devpnpm run devpnpm run devyarn devyarn devOpen http://localhost:5173 in your browser. Everything, including PDF export, runs in the browser.
Step 5 (Optional): Connect a Node Server#
While exporting in the browser works, you might want to offload that to a server for a long book or a low-powered device. The kit can hand rendering to a Node server instead. Only rendering moves; the PDF/X conversion always stays in the browser.
Run the web app and the export server together:
npm run dev:servernpm run dev:serverpnpm run dev:serverpnpm run dev:serveryarn dev:serveryarn dev:serverThis starts the server on port 8080, proxied at /api, and sets VITE_USE_SERVER=true for you. Plain npm run dev never calls the server, so an export failure surfaces instead of silently falling back.
Server Endpoints#
| Route | What it does |
|---|---|
POST /api/export |
Takes the raw scene archive, checks the size and rate limits, creates a pending job, and answers 202 with a job id straight away |
GET /api/export/:id |
Returns { status, phase, error } for polling. 404 when the id is unknown |
GET /api/export/:id/file |
Sends the finished PDF and then drops the job, so an interrupted download can be retried. 409 while the job has no result yet |
DELETE /api/export/:id |
Discards a job the client abandoned. Always answers 204, and only removes a job belonging to the calling client |
GET /api/health |
Returns { status, activeJobs }. Reports degraded once any export has timed out |
Code structure:
src/server/index.ts # Composition root: wires the exporter in, listens on 8080, graceful shutdownsrc/server/http/routes.ts # The endpoints above, size and rate limits, job wiring, JSON error handlersrc/server/imgly/export.ts # Renders the archive with @cesdk/node-nativesrc/server/jobs/store.ts # In-memory job store, limits, and the expiry sweepsrc/shared/export-api.ts # Wire types shared with the clientThe bundled server renders with @cesdk/node-native and ships without TLS or authentication, so put a real gateway in front of it before exposing it.
Exporting Print-Ready PDFs#
Export produces a PDF/X-4 file with a FOGRA39 output intent. The conversion always runs in the browser, so the server path returns a plain PDF and the browser converts it.
Get Started#
Add the photobook editor to a web application that already uses CE.SDK. You take the editor logic, install the packages it needs, point it at your asset host, and decide whether you want an export server.
Step 1: Copy the Editor Logic#
cd your-projectcd your-projectClone the starter kit and copy the editor logic into your project:
git clone https://github.com/imgly/starterkit-photobook-editor-react-web.git
cp -r starterkit-photobook-editor-react-web/src/client/src/imgly ./src/imgly
rm -rf starterkit-photobook-editor-react-webgit clone https://github.com/imgly/starterkit-photobook-editor-react-web.git
cp -r starterkit-photobook-editor-react-web/src/client/src/imgly ./src/imgly
rm -rf starterkit-photobook-editor-react-webnpx degit imgly/starterkit-photobook-editor-react-web/src/client/src/imgly ./src/imglynpx degit imgly/starterkit-photobook-editor-react-web/src/client/src/imgly ./src/imglyThe imgly/ folder holds the layouts library, the auto-fill, the validation checks, and the editor configuration, with no dependency on the demo application:
imgly/├── index.ts # Editor initialization function├── photobook.ts # Sizes, styles, and the scene each starts from├── photos.ts # Upload asset source for the reader's photos├── autofill.ts # Places photos and captions into placeholders├── placeholders.ts # What counts as unfilled placeholder content├── photo-stash.ts # Holds photos that a smaller layout cannot fit├── preview-mode.ts # Page-spread preview├── block-utils.ts # Engine helpers shared across the kit├── constants.ts # Shared identifiers and panel helpers├── plugins/layouts/│ └── layout.ts # Layouts asset source and layout switching├── validation.ts # The print validation checks├── validation-types.ts # Validation result types├── validation-utils.ts # Validation helpers└── config/ ├── plugin.ts # Main configuration plugin ├── actions.ts # Export/import actions ├── features.ts # Feature toggles ├── i18n.ts # Translations ├── settings.ts # Engine settings ├── keyboard/ # Keyboard shortcut catalog └── ui/ # UI customizationValidation runs on its own: the checks in imgly/validation.ts report every problem they find without further setup. The user-facing names and descriptions for those reports are presentation, and the kit keeps them in src/client/src/app/validation-config.ts. That file is not required for validation to run — copy it if you want its wording, or write your own labels in your UI.
Step 2: Install Dependencies#
Install the required packages for the editor:
Core Editor#
Install the Creative Editor SDK:
npm install @cesdk/cesdk-js@1.83.0npm install @cesdk/cesdk-js@1.83.0pnpm add @cesdk/cesdk-js@1.83.0pnpm add @cesdk/cesdk-js@1.83.0yarn add @cesdk/cesdk-js@1.83.0yarn add @cesdk/cesdk-js@1.83.0Print Ready PDF#
Add the PDF/X conversion used by the export pipeline:
npm install @imgly/plugin-print-ready-pdfs-webnpm install @imgly/plugin-print-ready-pdfs-webpnpm add @imgly/plugin-print-ready-pdfs-webpnpm add @imgly/plugin-print-ready-pdfs-webyarn add @imgly/plugin-print-ready-pdfs-webyarn add @imgly/plugin-print-ready-pdfs-webThis plugin runs in the browser. See Print Ready PDF for its options.
Step 3: Download Assets#
CE.SDK requires engine assets (fonts, icons, UI elements) to function. These must be served as static files from your project’s public/ directory.
curl -O https://cdn.img.ly/packages/imgly/cesdk-js/1.83.0/imgly-assets.zip
unzip imgly-assets.zip -d public/
rm imgly-assets.zipcurl -O https://cdn.img.ly/packages/imgly/cesdk-js/1.83.0/imgly-assets.zip
unzip imgly-assets.zip -d public/
rm imgly-assets.zipThe kit’s layout and style scenes are a separate bundle. Host it yourself and serve the scene from that same root: the kit derives the asset root from the scene URL you pass.
Step 4: Add a Container Element#
Add a container element to your HTML where the editor will be mounted:
<div id="cesdk_container" style="width: 100%; height: 100vh;"></div>Step 5: Create the Editor Component#
Create the editor, then hand it to initPhotobookEditor with the scene to open:
import CreativeEditorSDK from '@cesdk/cesdk-js';
import { initPhotobookEditor } from './imgly';
const config = { userId: 'your-user-id', baseURL: '/assets' // license: 'YOUR_CESDK_LICENSE_KEY',};
CreativeEditorSDK.create('#cesdk_container', config) .then(async (cesdk) => { await initPhotobookEditor( cesdk, 'https://your-server.example.com/photobook-assets/style-playful-portrait.imgly' ); }) .catch((error) => { console.error('Failed to initialize CE.SDK:', error); });initPhotobookEditor takes a scene and nothing else. It configures the editor, loads the scene, and reads the rest — the page format and the asset root — from the scene itself.
Filling the book with photos is your app’s job, not the kit’s. See Set Up a Scene for the calls that do it.
Step 6 (Optional): Offload Rendering to a Node Server#
Keep browser-side export and you are done — nothing else is required.
Rendering a long book on a low-powered device is the case for moving the work off the reader’s machine. Copy the bundled server. It is five files:
src/server/index.ts # Composition root: wires the exporter in, listens on 8080, graceful shutdownsrc/server/http/routes.ts # The five routes, size and rate limits, job wiring, JSON error handlersrc/server/imgly/export.ts # Renders the scene archive with @cesdk/node-nativesrc/server/jobs/store.ts # In-memory job store, limits, and the expiry sweepsrc/shared/export-api.ts # Wire types shared with the client@cesdk/node-native is a native binary, so it runs where its platform build does: Linux x64 with glibc 2.39 or newer, macOS on ARM or x64, and Windows x64. Check that against your base image before you build.
Install what the server needs:
npm install @cesdk/node-native@1.83.0 express dotenvnpm install @cesdk/node-native@1.83.0 express dotenvpnpm add @cesdk/node-native@1.83.0 express dotenvpnpm add @cesdk/node-native@1.83.0 express dotenvyarn add @cesdk/node-native@1.83.0 express dotenvyarn add @cesdk/node-native@1.83.0 express dotenvTo render on a backend you already run instead, implement the same contract. Match the wire types in src/shared/export-api.ts and these routes:
| Route | Purpose |
|---|---|
POST /api/export |
Upload the scene archive, receive a job id |
GET /api/export/:id |
Poll the job status |
GET /api/export/:id/file |
Download the rendered PDF |
DELETE /api/export/:id |
Discard a job the client no longer needs |
GET /api/health |
Readiness probe |
The client calls these as relative paths, so serve them on the same origin or proxy them. Set VITE_USE_SERVER=true to route exports through the server.
Only rendering moves to the server. The PDF/X conversion always runs in the browser, so the server never needs @imgly/plugin-print-ready-pdfs-web.
Exporting Print-Ready PDFs#
Export produces a PDF/X-4 file with a FOGRA39 output intent.
In your own project this is a second step, not something export does for you. It needs @imgly/plugin-print-ready-pdfs-web from Print Ready PDF above. Copy toPrintReadyPDF from the kit’s src/client/src/app/print-ready-pdf.ts, then export the scene to a PDF and convert it:
const pdf = await cesdk.engine.block.export(page, { mimeType: 'application/pdf' });const printReady = await toPrintReadyPDF(pdf);Pass an AbortSignal as the second argument to cancel the conversion. This step stays in the browser whether or not you added the server.
What the Kit Offers#
- Layouts — a layouts library filtered to the loaded page format. Applying one keeps the content already on the page.
- Auto-fill — photos placed into the book automatically, each one matched to the slot that crops it least, and captions filled from your own copy.
- Validation — a sidebar reporting low-resolution photos, empty slots, and content outside the trim, each one selectable.
- PDF/X compliance — export to PDF/X-4 with a FOGRA39 output intent, the format commercial printers ask for.
Set Up a Scene#
A photobook starts from a template rather than an empty page. The kit opens the scene, then your app puts content into it:
// Any scene URL your asset bundle serves.const sceneURL = `${DEMO_ASSETS_URL}/style-chic-portrait.imgly`;
await initPhotobookEditor(cesdk, sceneURL);
const photos = await addPhotosToUploadSource( cesdk.engine, examplePhotos, uploadedFiles);
// Skip both calls to let the reader place every photo by hand.autoFillPhotobook(cesdk.engine, photos);autoFillPhotobookTexts(cesdk.engine, exampleTexts);In short: the scene decides the book, and filling it is your app’s job.
initPhotobookEditortakes only a scene and reads the format from it.- Adding a style means adding its scene to your asset bundle.
addPhotosToUploadSourceis where your own photos come in.- Both auto-fill helpers touch only blocks the template marked as placeholders.
- Size and style come from the start screen, configured in
src/client/src/app/photobook-options.ts.
Customize#
Layouts#
The layouts library is a CE.SDK asset source. Choosing a layout re-lays out the current page instead of inserting a block, and switching to a layout with fewer slots does not discard photos — the extras move to a per-page stash and flow back when a larger layout returns.
Layouts are authored per page format. Each format has its own directory with its own content.json, scenes, and thumbnails:
layouts/├── portrait/ # content.json, scenes/, thumbnails/└── square/ # content.json, scenes/, thumbnails/To add a layout, add its scene to your asset bundle and its entry to that format’s content.json:
{ "id": "portrait-001", "label": { "en": "Title" }, "groups": ["Titles"], "meta": { "blockType": "acme.layouts", "thumbUri": "{{base_url}}/portrait/thumbnails/portrait-001.png", "uri": "{{base_url}}/portrait/scenes/portrait-001.blocks" }}Authoring a Layout Scene#
A layout scene holds one page’s worth of slots, and the kit reads it as the page’s new content. Four rules make it behave once applied:
- One page per scene. The kit swaps the page’s children, so a scene with two pages contributes only the first.
- Mark every slot as a placeholder. Auto-fill, the layout stash, and the
placeholderImageandplaceholderTextchecks all key off the placeholder flag. A slot without it is never filled and never reported. - Author at the format’s page size. Layouts live under the format directory whose pages they were drawn for; a scene drawn at another size arrives scaled.
- Give text slots their default copy. The
placeholderTextcheck compares the text against what the template shipped, so unedited captions are reported.
Photos reach the pages in the order given, but within a page auto-fill assigns them by shape, not position: it pairs the photos with the slots that crop them least. Slot geometry therefore decides which photo lands where.
Validation#
Validation runs against the book while the reader edits it and reports what would go wrong in print. Eight checks ship with the kit:
| Check | Fires when |
|---|---|
outsidePage |
An element sits completely outside the page |
protrudesFromPage |
An element extends beyond the page edge |
lowResolution |
A photo is too small for its printed size |
bleedMargin |
An element crosses the bleed margin |
duplicateImage |
The same photo appears more than once |
emptyPage |
A page has no content yet |
placeholderImage |
An image slot has no photo yet |
placeholderText |
Text still shows its placeholder content |
The detectors live in src/client/src/imgly/validation.ts and report independently of any UI. Adding a check means adding its detector there, and a name and description for it wherever your interface presents the results — in this kit, src/client/src/app/validation-config.ts.
Print Export#
Export produces a PDF/X-4 file with a FOGRA39 output profile, set in PDFX_OPTIONS in src/client/src/app/print-ready-pdf.ts. The scene renders to a standard PDF first, then convertToPDFX from @imgly/plugin-print-ready-pdfs-web converts it.
Theming and Localization#
Match the editor to your brand by setting a theme in the editor configuration. See Theming for the full set of variables.
The kit’s own strings live in src/client/src/imgly/config/i18n.ts. Add a locale there alongside the editor’s built-in translations. See Localization.
Key Capabilities#
The Photobook Editor covers the path from a folder of photos to a file a printer accepts.
Layouts Library
Swap a page between authored layouts. Photos and captions carry over, and extras wait in a stash until a larger layout takes them back.
Photo Auto-Fill
Place uploaded photos into the book automatically, matching each photo to the slot whose shape crops it least.
Styled Templates
Start every book from a real scene, authored per page format and style, instead of from a blank canvas.
Design Validation
Catch low-resolution photos, off-page content, and bleed-margin problems while the reader can still fix them.
Print-Ready Export
Export PDF/X-4 with a FOGRA39 output intent, the format commercial printers ask for.
Multiple Book Formats
Offer portrait and square books at several sizes, each with its own layouts and its own style scenes.
Troubleshooting#
Editor doesn’t load#
- Check the container element exists: Ensure your container element is in the DOM before calling
create() - Verify the baseURL: Engine assets must be reachable from the CDN or your self-hosted location
- Check console errors: Look for CORS or network errors in browser developer tools
Layouts or example photos don’t appear#
- Check the demo asset location: The layouts, style scenes, and example photos come from a separate bundle. The kit derives their root from the scene URL you pass to
initPhotobookEditor, so check that URL, orVITE_DEMO_ASSETS_BASE_URLwhen you run the kit as-is - Check network requests: Open the DevTools Network tab and look for failed requests to the asset host
- Self-host assets for production: See Serve Assets to host assets on your infrastructure
Layouts don’t fill with photos#
- Check the layout’s placeholders: Auto-fill only touches blocks the layout scene marked as placeholders. A layout authored without them stays empty. See Placeholders
Export is slow or blocks the editor#
- Move rendering to the server: A large book competes with the editor for the main thread. Run the export server and set
VITE_USE_SERVER=true - Wait for content to load: Ensure photos are fully loaded before exporting
- Check CORS on images: Remote photos must allow cross-origin access
Watermark appears in production#
- Add your license key: Set
VITE_CESDK_LICENSEin your environment, or thelicenseproperty in your configuration - Get a license: Contact us at img.ly/forms/contact-sales/
API Reference#
Most of what you need is four calls: open a scene, give it photos, fill it, and export it for print.
| Call | What it does |
|---|---|
initPhotobookEditor(cesdk, sceneURL) |
Opens a photobook. Configures the editor, loads the scene, offers the layouts matching the scene’s page format, and locks the cover |
addPhotosToUploadSource(engine, remotePhotos, files) |
Fills the upload source with the photos the book may use, and returns them for auto-fill |
autoFillPhotobook(engine, photos) |
Fills every empty slot, matching each photo to the slot whose shape crops it least |
toPrintReadyPDF(pdf, signal?) |
Converts an exported PDF to print-ready PDF/X-4. From src/client/src/app/print-ready-pdf.ts |
Two more worth knowing:
VALIDATION_CHECKSinsrc/client/src/imgly/validation.ts— the print validation checks, each with the rule it applies. This is where you add a check of your own or drop one you do not want.enterPreviewMode(cesdk, options)— switches the editor into the read-only page-spread preview and returns the function that exits it.
Everything else is exported from src/client/src/imgly/index.ts:
| Export | Kind | What it does |
|---|---|---|
initPhotobookEditor(cesdk, sceneURL) |
function | Configures a CE.SDK instance for photobook editing: adds the editor configuration and asset source plugins, loads the scene, registers the layouts library for the format read from the loaded cover, stamps the template’s default texts, and locks the cover |
DesignEditorConfig |
class | The editor configuration plugin. Adds the actions, features, translations, settings, keyboard shortcuts, and UI layout. Add it with cesdk.addPlugin(new DesignEditorConfig()) |
UPLOAD_SOURCE_ID |
constant | The id of the asset source the reader’s photos go into, ly.img.image.upload |
revokeUploadedPhotoURLs() |
function | Releases the object URLs created for uploaded files and drops their cached resolutions. Call it when the editor closes |
PhotoRef |
type | A photo available for filling the book: uri, width, height |
RemotePhotoAsset |
type | A remote example photo with known pixel dimensions, plus id, label, and thumbUri |
autoFillPhotobook(engine, photos) |
function | Fills every placeholder image slot in the book, matching each photo to the slot whose shape crops it least |
autoFillPhotobookTexts(engine, texts) |
function | Writes caption copy into the placeholder text blocks, page by page |
PhotobookTextContent |
type | The copy an auto-filled book is written with: { captions: string[] } |
enterPreviewMode(cesdk, options) |
function | Switches the editor into a read-only page-spread preview with its own navigation, and returns the function that exits it. Preview writes go into a scratch history, so the mode leaves no undo steps behind |
LAYOUTS_SOURCE_ID |
constant | The id of the layouts asset source, ly.img.layouts |
LayoutsAssetSourcePlugin |
class | Registers the layouts library for one page format. Applying a layout re-lays out the current page and carries its photos and text into the new slots |
getLayoutFormat(size) |
function | Returns the page format a size is authored at, portrait or square |
getSceneLayoutFormat(engine) |
function | Returns the page format of the loaded scene, read from its cover page |
assetsBaseURLOf(sceneURL) |
function | Returns the asset root a scene hangs off: its parent directory |
addPhotosToUploadSource(engine, remotePhotos, files) |
function | Adds photos to the upload source and returns them, the reader’s files first |
getStyleSceneURL(assetsBaseURL, format, styleId) |
function | Returns the scene URL a size and style combination starts from |
PhotobookSize |
type | A page format in millimeters: { width, height } |
PhotobookStyleId |
type | 'playful' | 'chic' |
PhotoDistributionId |
type | 'autoFill' | 'byHand' |
Next Steps#
- Configuration – Complete list of initialization options
- Serve Assets – Self-host engine assets for production
- Print Ready PDF – More on PDF/X conversion
- Placeholders – Author the slots layouts fill
- Theming – Customize colors and appearance
- Localization – Add translations and language support