Technical Documentation
Zineroo
A browser-based React print shop for making printable foldy zines, square zines, saddle stitch booklets, trifold brochures, and Unigrid folded maps from images, with auto-fill, layered photo and text placement, curved text overlays, undo/redo, end-to-end encrypted cloud projects with recoverable sync, booklet preview, imposition layout, and export-ready PDF, PNG, ZIP, and editable project output
The Idea
Zineroo is a focused publishing tool for turning a small image set into a printable folded object. It keeps the core workflow in the browser: drop images in, choose a foldy zine, square zine, saddle stitch booklet, trifold brochure, or Unigrid map format, auto-fill the editor, add photo and text layers, reorder pages or spreads where the format supports it, tune the frame, preview the booklet or sheet, and export the files. The editor remains local-first by default. Images are kept in the browser, autosaved to IndexedDB, and exported by the user as files unless the user signs in for optional Plus cloud projects.
The interface is built around the physical object. The app has fixed page maps, paper presets, format-aware spread controls, full-sheet imposition views, and export controls for the printable artifact. The newer workspace separates the loop into Grid, Editor, and Preview modes, with a separate public homepage, project manager, account menu, and admin surface around the editor. It is not a general design canvas. Its job is to make the zine-making loop fast enough that layout decisions can stay visual and tactile. Cloud persistence follows that same principle: a project keeps working in the browser while an encrypted, resumable sync job moves it to the user's private workspace.
Make the folded sheet the source of truth.
Features
Folded formats
- Eight-page Foldy Zine format with front cover, six interior pages, and back cover
- Square Foldy Zine format using the same folded page map with square page geometry
- Saddle Stitch Booklet format with page counts from 4 to 100 pages in four-page increments
- Trifold Brochure format with outside and inside sheet sides
- Unigrid (NPS Map) format with two 6-by-2 sides, side switching, large-format paper presets, and title band support
- Format switching updates page labels, editor grid, preview views, valid spreads, and export sides
- Drag-and-drop page assignment from the image tray
- Auto-Distribute fills the editor from the sorted asset tray
- Re-Order mode moves or swaps single pages, two-page spreads, and valid trifold spread groups
- Undo and redo controls cover layout edits, placement changes, text edits, imports, and reordering
Image placement
- Fill and fit modes for fast page framing
- Freeform mode for manual pan, zoom, and rotation
- Layered photo placement supports a background image plus additional foreground photo layers
- Rotation-aware freeform crop constraints and snap guides keep images inside the printable frame
- Per-page margin and image stroke controls with apply-to-all actions
- Image overlay colors can tint the whole page or only the placed photo
- Freeform and aligned crop states are preserved when switching modes
Text and color layers
- Non-destructive text overlays stored with each page or spread
- Full-frame editor with direct dragging over the image and separate design/text panes
- Font, color, opacity, highlight, size, tracking, line-height, padding, alignment, rotation, and curve controls
- Self-hosted display, serif, mono, script, and typewriter font options
- Text overlays render through the same canvas path as previews, PDF, and PNG output
Layer stacking
- Layers panel lists every photo and text element on the active page or spread in a single stacking order
- Drag-and-drop reordering moves any layer above or below another, freely interleaving photos and text instead of a fixed photos-then-text stack
- A canonical bottom-to-top layer order per page slot drives canvas rendering, hit-testing, and PDF/PNG export from the same stack
- Text-only pages support overlay color, text layers, and reordering without requiring a background photo
Preview and export
- Editor view for the active format's complete page set
- Booklet and brochure preview modes with format-specific navigation
- Full-sheet imposition preview using the print order required for folding, booklets, double-sided trifold printing, or Unigrid sides
- Standard 144 PPI PDF export via jsPDF for sharing and simple printing
- Print shop PDF export at 300 PPI with optional bleed, crop marks, and local CMYK conversion through uploaded ICC or ICM profiles
- Single-sheet PNG export and zipped multi-sheet PDF or PNG export for booklet, brochure, and Unigrid output
- Optional ZIP export bundles the rendered PDF or PNG files with a portable project JSON archive
- Print guidance explains direct printing, PDF printing, and print-shop export paths before download
- Download-complete dialog shows zine folding instructions, trifold settings, or print-shop PDF notes
- Lightweight preview images keep editing responsive while exports render from original uploads
Local and cloud persistence
- Autosaves the working project to IndexedDB with visible save status
- Restores the saved project on reload
- Stores original image blobs locally and regenerates runtime preview URLs when restored
- New Zine and project import preserve cloud projects in Projects while warning before a browser-only draft is replaced
- Object URLs and image caches are cleaned up on reset, removal, and import
- Project import reports progress and leaves an actionable recovery path when an asset cannot be restored
- Optional Plus cloud projects encrypt project state, originals, previews, thumbnails, titles, and text in the browser before upload; Zineroo and its storage providers cannot read the project contents
- A private cloud key, recovery key, and optional trusted-device unlock keep encrypted projects available across devices without a server-side content key
- Cloud autosave persists a local sync queue with byte-level progress, resumable upload plans, bounded retry backoff, offline resume, and generation-conflict handling
- The Projects library separates regular work from interrupted uploads, so failed or conflicted syncs can be retried, resolved in the editor, checked again, or removed without cluttering the library
- Projects supports search, recent and favorite views, grid or list layouts, opening, renaming, duplicating, manual versions, version restore, and a 30-day Trash before permanent deletion
- Desktop beta gate blocks phones and too-small windows while desktop-responsive scaling keeps the editor usable across larger viewports
Account and Pro workflows
- Supabase Auth powers sign in, sign up, password reset, session revocation after a password change, subscriptions, entitlement grants, and admin role checks
- Stripe checkout and billing portal routes support monthly and yearly Plus and Pro plans
- Access code redemption grants Pro without changing account roles
- Admin APIs manage users, manual Pro grants, revocation, access codes, and audit events server-side; hardened request paths keep implementation details out of client-facing errors
- Version checks detect a newer deployed build and show a full-screen reload modal without risking local project data
Tech Stack
| Framework | React 18 with Vite |
| Language | TypeScript |
| Rendering | HTML Canvas for page previews, booklet spreads, and imposition sheets |
| Persistence | IndexedDB local autosave, resumable sync jobs, and workspace bindings with runtime URL rehydration; optional end-to-end encrypted Supabase cloud projects, versions, and 30-day trash for Plus users |
| Export | jsPDF for standard PDF output, Canvas blob export for PNG sheets, local CMYK PDF generation with ICC output intents, JSON archive/import, and a small in-app ZIP writer for bundled exports |
| Image pipeline | Canvas-generated preview blobs for editing, original uploads for print export |
| Image input | JPEG, PNG, and WebP uploads via file picker or drag-and-drop |
| Layout sizing | ResizeObserver-driven editor frame sizing for stable page previews |
| Paper presets | US Letter, US Legal, US Tabloid, A5, A4, A3, and Unigrid-specific 18" x 24", 21" x 35", 24" x 36", 30" x 42", and 420 x 594 mm presets |
| Formats | Foldy Zine, Foldy Zine (Square), Saddle Stitch Booklet, Trifold Brochure, and Unigrid (NPS Map) |
| Typography | Self-hosted Anton, Bebas Neue, Bungee, Playfair Display, EB Garamond, Space Grotesk, Courier Prime, Special Elite, and Caveat web fonts |
| Hosting | Vercel-hosted Vite build with serverless API routes |
| Auth and billing | Supabase Auth, Postgres, and Storage; browser-side Web Crypto vault and recovery keys; Stripe Checkout, webhooks, and access-code entitlements |
Architecture
Source structure
api/ Vercel API routes for auth-protected admin, billing, webhooks, and access codes src/ main.tsx React Router shell for homepage, editor, projects, and admin routes App.tsx editor state, format switching, workspace modes, project history, and export orchestration canvas/ shared preview, booklet, imposition, and export rendering components/ asset tray, page grid, frame editor, inspector, previews, account menu, and export modals context/ auth, private cloud vault, update state, and the active cloud-project workspace/sync bridge dom/ drag targets and text-overlay geometry shared by DOM and canvas rendering domain/ project model, format definitions, spread and crop rules hooks/ viewport gating, update checks, and confirmation helpers pages/ public homepage, Projects manager, and admin console crypto/ browser-worker encryption format, key hierarchy, recovery keys, and authenticated cloud objects services/ assets, IndexedDB storage and sync journal, encrypted cloud projects, entitlements, project JSON, ZIP, PDF, ICC, and downloads styles/ app shell, homepage, tray, page grid, frame editor, inspector, preview, account, and export UI
Core project model
type ZineProject = {
version?: number;
format?: 'zine' | 'square-zine' | 'trifold' | 'bifold' | 'unigrid';
pageCount?: number;
unigridTitleBand?: {
enabled: boolean;
text: string;
};
title: string;
assets: ZineAsset[];
pages: PageSlot[];
settings: DesignSettings;
};
type TextOverlay = {
id: string;
text: string;
fontFamily: string;
fontSize: number;
color: string;
opacity: number;
backgroundColor?: string;
backgroundOpacity: number;
tracking: number;
lineHeight: number;
padding: number;
x: number;
y: number;
rotation: number;
curve: number;
align: 'left' | 'center' | 'right';
};
type PhotoLayer = {
id: string;
assetId?: string;
fit: 'cover' | 'contain' | 'spread';
crop: { x: number; y: number; zoom: number; rotation: number };
freeformCrop?: { x: number; y: number; zoom: number; rotation: number };
freeform: boolean;
margin: number;
imageBorder?: number;
imageStroke?: string;
overlayColor?: string;
overlayOpacity: number;
overlayPhotoOnly?: boolean;
};
type ZineAsset = {
id: string;
name: string;
type: string;
blob: Blob;
width: number;
height: number;
previewBlob?: Blob;
previewWidth?: number;
previewHeight?: number;
};
type PageSlot = {
id: number;
assetId?: string;
fit: 'cover' | 'contain' | 'spread';
crop: { x: number; y: number; zoom: number; rotation: number };
freeformCrop?: { x: number; y: number; zoom: number; rotation: number };
freeform: boolean;
margin: number;
imageBorder?: number;
imageStroke?: string;
overlayColor?: string;
overlayOpacity: number;
overlayPhotoOnly?: boolean;
photoLayers?: PhotoLayer[];
textOverlays?: TextOverlay[];
layerOrder?: string[];
spreadId?: string;
spreadSide?: 'left' | 'center' | 'right';
spreadPageIds?: number[];
spreadIndex?: number;
spreadLength?: number;
spreadColumns?: number;
spreadRows?: number;
spreadColumnIndex?: number;
spreadRowIndex?: number;
};
The data model is intentionally small. A project is a title, a format, a local asset library, page slots, optional Unigrid title-band state, and design settings. Page slots own their background image placement, additional photo layers, overlay color, text layers, and spread metadata, including the row/column geometry needed for Unigrid spreads. Runtime object URLs are stripped before autosave, project export, and cloud persistence, then rebuilt when the project is restored. Uploaded originals stay available for export while generated preview blobs keep the editor responsive.
Layer model
type SlotLayer =
| { id: string; kind: 'photo'; placement: PhotoPlacement }
| { id: string; kind: 'text'; overlay: TextOverlay };
function allLayersForSlot(slot: PageSlot): SlotLayer[];
function reorderLayer(slot: PageSlot, id: string, beforeId: string | null): PageSlot;
Each page slot keeps a single bottom-to-top layerOrder list of element ids spanning both its photo layers and its text overlays. allLayersForSlot resolves that order into a typed stack that canvas rendering, hit-testing, and the Layers panel all read from instead of walking photos and text separately, so dragging a layer in the panel changes the same order the page actually renders in. normalizeLayerOrder reconciles the stored order against each slot's current layer ids on every edit, so projects saved before layer ordering existed, and pages missing a stored order, still resolve to a sensible default stack.
Preview asset pipeline
const PREVIEW_MAX_LONG_EDGE = 1200;
function imageForAsset(asset: RuntimeAsset, source: 'preview' | 'original' = 'preview') {
const url = source === 'preview' ? asset.previewUrl ?? asset.url : asset.url;
return loadImageFromUrl(url, `Could not load ${asset.name}`);
}
The editor renders from preview-sized blobs when possible, but PDF and PNG export request the original image source. That keeps drag, crop, and preview interactions light without lowering print output quality.
Format-aware sheets
const IMPOSITION_PAGES = [7, 6, 5, 4, 8, 1, 2, 3]; const TRIFOLD_OUTSIDE_PAGES = [2, 6, 1]; const TRIFOLD_INSIDE_PAGES = [3, 4, 5]; const UNIGRID_COLUMNS = 6; const UNIGRID_ROWS = 2; const UNIGRID_SIDE_PAGE_COUNT = 12;
The full-sheet preview and PDF export use the same format definitions, so the visual preview matches the file that is saved. Foldable zines render one imposed sheet with the top row rotated for folding. Trifolds render separate outside and inside sides for double-sided printing. Bifold booklets render the required imposed sheet sequence so longer booklets can be exported as a set of printable PDF sheets. Unigrid projects render two large-format sides with explicit 6-by-2 cell geometry and format-specific paper choices.
Reorder model
type ReorderDropPosition = 'before' | 'after' | 'on';
type ReorderUnit = {
kind: 'single' | 'spread';
length: number;
startIndex: number;
};
Reordering treats a spread as one unit, whether it spans two pages or a full three-panel trifold side. Boundary drops move content before or after another slot, while direct drops swap compatible blocks. Spread metadata is normalized after each move so paired and grouped pages stay together. Unigrid uses side-aware spread controls instead of free reordering, because its cells are tied to a fixed folded-map grid.
History model
The editor keeps an undo/redo stack around complete project snapshots, with grouping for continuous text and control edits so sliders and typing do not flood history. The frame editor keeps temporary draft state while open, then commits the finished overlay state back into the project history when it closes.
Export flow
async function renderPdfBlob() {
const paper = sheetSize(project.settings);
const pdf = new jsPDF({
orientation: 'landscape',
unit: 'pt',
format: [paper.width, paper.height],
});
for (const [index, side] of sheetSidesForFormat(projectFormat(project)).entries()) {
if (index > 0) pdf.addPage([paper.width, paper.height], 'landscape');
const sheet = await renderImpositionCanvas(project, Math.round(paper.width * 2), {
showGuides: false,
imageSource: 'original',
sideId: side.id,
});
pdf.addImage(sheet.toDataURL('image/jpeg', 0.95), 'JPEG', 0, 0, paper.width, paper.height);
}
return pdf.output('blob');
}
Export is canvas-first. The app renders the printable side or sides, then routes that same rendering to PDF, PNG, or ZIP. Standard PDFs prioritize quick sharing and home printing. Print shop PDFs render at 300 PPI, can add bleed and crop marks, and can flatten through a local CMYK conversion path when the user supplies a printer profile. Project export is separate but can be bundled with the rendered artifact: it serializes the project state and embeds original image blobs as data URLs so the work can be moved between browsers without carrying runtime preview blobs or object URLs. Plus cloud save uses a separately encrypted manifest and authenticated objects for originals, previews, and thumbnails; the browser hydrates them back into blobs only after the private vault is unlocked.
Private cloud sync model
type CloudSyncJob = {
projectId: string;
expectedGeneration: number;
revision: number;
state: 'pending' | 'syncing' | 'error' | 'conflict';
retryCount: number;
retryAt: number;
project: ZineProject;
};
type CloudSyncStatus =
| 'Saved in browser'
| 'Syncing to cloud…'
| 'Saved in cloud'
| 'Offline — will resume'
| 'Cloud sync needs attention';
The cloud workspace is deliberately queue-based instead of treating a save as a one-shot request. After a short idle delay, a meaningful edit is serialized into IndexedDB with its expected cloud generation and an upload plan. The app reports preparation, upload, and commit progress; retries transient failures with a capped exponential delay; wakes again when the browser comes back online; and restores the active cloud-workspace binding after a reload. A generation mismatch becomes an explicit conflict rather than an overwrite, with an editor path to open the newer version or retain the local work as a separate copy.
Design Notes
The UI uses a print-shop vocabulary: heavy rules, square controls, hard shadows, paper tones, and a narrow red accent. The panels map to the workflow: image tray on the left, working sheet or frame editor in the center, design controls on the right, and a compact top bar for Grid, Editor, Preview, account, print, import, and export actions. The app favors visible mechanics over hidden menus because the user is making a physical object and needs to see the consequences of each change immediately.
The most important view is the full sheet. Individual pages matter, but the final object is the folded imposition. Keeping that view one click away makes mistakes visible before the user prints. A beta desktop gate and responsive editor scaling are also part of that design decision: until the mobile editor is ready, the app explicitly blocks phones and windows below the editor's minimum scale instead of offering a cramped, unreliable canvas.