# HTML Viewer Export Concept
> **Status: IMPLEMENTED.** Viewer source: [frontend/export-viewer/](../frontend/export-viewer/).
> Pre-built output committed to [backend/static/export-viewer/](../backend/static/export-viewer/).
> Backend export pipeline: [backend/src/services/export.rs](../backend/src/services/export.rs).
>
> Outstanding follow-ups:
> - The export-viewer's `tailwind.config.js` does not yet extend the main app's config. Visual
> drift risk — see "Shared Tailwind Config" section below.
> - Service-worker (offline PWA caching) is still "Future" — fine for v1 since the ZIP is
> already fully offline by virtue of relative paths.
## Overview
The HTML Viewer export produces a **self-contained offline ZIP** that is a read-only clone
of the live EventSnap feed. Opening `index.html` in any modern browser shows the full event
gallery — list view, grid view, search, filter, lightbox — with no internet connection or
server required.
It **replaces the current HTML export job type**. The old HTML export produced a raw
minijinja-rendered template; the new viewer supersedes it entirely. The existing `html`
job variant in the backend is repurposed to run this flow instead of being kept alongside
it.
It is distinct from the ZIP archive export (which is raw media files). The viewer is a
polished, navigable web app bundled with the event's content.
---
## User-Facing Behavior
- Download `event_name_viewer.zip`, unzip, open `index.html`
- Full list view (chronological, newest first) and grid view with search/filter
- Likes, comments, and reaction counts shown (static snapshot from export time)
- Read-only: no uploads, no auth, no dashboards
- Works offline, no CDN or external resources
---
## Architecture
### Separate Static SvelteKit App
A new mini SvelteKit project lives at `frontend/export-viewer/` within the same monorepo.
It uses `adapter-static` and is kept completely independent of the main app.
**Why separate rather than a shared route:**
- The viewer must be distributable as a standalone static bundle; the main app uses
`adapter-node` and cannot be mixed
- Keeping it separate avoids auth, store, and routing dependencies leaking in
- Simpler to reason about: the viewer has exactly two concerns (list view, grid view)
**Why same repo:**
- Shares Tailwind config and design tokens → visual parity with the main app
- Single `pnpm` workspace, no separate CI needed
- Backend can reference the pre-built output by relative path
### Pre-Built Output Committed to Repo
The viewer is built once and its output committed to `backend/static/export-viewer/`.
The backend export job **does not run a Node build** at runtime — it just copies the
pre-built assets and injects event data alongside them.
When the viewer source changes, a developer rebuilds it locally (`pnpm build` in
`frontend/export-viewer/`) and commits the updated `backend/static/export-viewer/` output.
---
## ZIP Structure
```
event_name_viewer.zip/
├── index.html ← entry point; open this in any browser
├── _app/
│ └── immutable/
│ ├── viewer.[hash].js ← all Svelte/app logic, single bundle
│ └── viewer.[hash].css ← all styles including Tailwind output
├── data.json ← injected by backend at export time
└── media/
├── abc123_thumb.jpg ← ~400 px wide, used in grid cells
├── abc123_full.jpg ← original or capped (see Media Strategy)
├── def456_thumb.jpg
└── def456.mp4 ← videos included as-is
```
No external font CDN, no Google Fonts, no remote scripts. All assets are local.
---
## data.json Schema
Generated by the backend export job. The viewer fetches this file on startup via
`fetch('./data.json')` (relative path, works from filesystem).
```json
{
"event": {
"name": "Sommerfest 2025",
"exported_at": "2025-07-15T20:00:00Z"
},
"posts": [
{
"id": "abc123",
"uploader": "MaxMustermann",
"caption": "Tolle Stimmung! #party #spaß",
"tags": ["party", "spaß"],
"timestamp": "2025-07-15T18:30:00Z",
"likes": 12,
"comments": [
{
"author": "AnnaSchulz",
"text": "So schön!",
"timestamp": "2025-07-15T18:35:00Z"
}
],
"media": [
{
"type": "image",
"thumb": "media/abc123_thumb.jpg",
"full": "media/abc123_full.jpg",
"width": 1920,
"height": 1080
}
]
}
]
}
```
All post data (likes, comments, tags) reflects the state at export time. No live updates.
---
## Media Strategy
### Images
| Variant | Purpose | Max dimension | Format |
|---------|---------|---------------|--------|
| `_thumb` | Grid cells, list post thumbnail | 400 px wide | JPEG q75 |
| `_full` | Lightbox / full-screen view | Original, or 2000 px cap if >5 MB | JPEG q85 |
The backend applies compression only when the original exceeds a threshold (e.g. >5 MB for
images). Below that threshold the original is used as `_full` unchanged.
The full-resolution original is always available via the separate ZIP archive export.
### Videos
Included as-is (no server-side transcoding). The viewer uses a standard `