Hmmm
Quotes in, shareable images out — entirely in the browser.
Nothing is uploaded, nothing is stored, and it installs as an offline PWA.
Getting started
·
Usage
·
Self-hosting
·
Deep links
·
License
## About
**Hmmm** is a static, privacy-friendly tool for turning quotes into shareable images. It runs entirely in the browser: write text, choose a layout and background, tweak type, and export a PNG. There is no login, no database, and no server-side image storage.
Built as an Excalidraw-style workspace (toolbar + side panel + live stage) and shippable as an installable PWA with offline support for the editor shell and gallery.
### Features
- **Quote editor** — primary text, optional translation (1300-character cap), and author (100-character cap)
- **Layouts** — 1:1, 4:5, 9:16, 16:9, 2:1 with auto type-fit via [pretext](https://github.com/chenglou/pretext)
- **Backgrounds** — curated local gallery (SVG + photo packs), solids, gradients, blur, filters, scrim
- **Upload** — drag-and-drop or file picker; images stay on the device (never uploaded)
- **Typography** — curated fonts, weight, size, alignment, and colors
- **Templates** — quick-start compositions (Tpl tab)
- **Site themes** — Light, Dark, Mint, Lilac, Navy (chrome only; quote canvas is independent)
- **Export** — download a PNG of the frame
- **Deep links** — prefill from partners with `/create?q=&q2=&author=`
- **PWA** — installable, offline shell + cached backgrounds; after you redeploy, open tabs prompt **Update available → Reload**
### Built with
| Layer | Choice |
|-------|--------|
| App shell | [Astro](https://astro.build) (static) + [Vue](https://vuejs.org) islands |
| Styles | [Tailwind CSS](https://tailwindcss.com) v4 · tokens in [DESIGN.md](./DESIGN.md) |
| Language | TypeScript |
| Tooling | [Bun](https://bun.sh) (preferred) · Node ≥ 22.12 |
| Text fit | [@chenglou/pretext](https://github.com/chenglou/pretext) |
| Export | [html-to-image](https://github.com/bubkoo/html-to-image) |
| PWA | `@vite-pwa/astro` + Workbox |
| Deploy | Vercel static · Docker / nginx · any static host |
(back to top)
## Getting started
### Prerequisites
- [Bun](https://bun.sh) **or** Node.js ≥ **22.12** (npm / pnpm also work)
- For e2e tests only: Playwright Chromium (`bunx playwright install chromium`)
### Installation
```bash
git clone https://github.com/kawishbit/hmmm.git
cd hmmm
bun install
# or: npm install / pnpm install
bun run dev
```
Open the URL Astro prints (usually `http://localhost:4321`).
**Production-like local preview:**
```bash
bun run build
bun run preview
```
(back to top)
## Usage
| Region | Role |
|--------|------|
| **Top toolbar** | Logo, layout, theme, share link, help, download |
| **Left panel** | **Text** · **Type** · **Bg** · **Tpl** |
| **Center stage** | Live quote frame |
### Typical flow
1. Open `/` (or `/create` from a deep link).
2. Enter quote text (and optional author / translation).
3. Pick a layout that fits the length.
4. Choose a gallery background, solid/gradient, or upload your own.
5. Adjust type, then **Download** a PNG.
### Routes
| Path | Page |
|------|------|
| `/` | Editor |
| `/create` | Editor (partner deep-link entry) |
| unknown | On-brand 404 |
### Scripts
| Command | Description |
|---------|-------------|
| `bun run dev` | Dev server (hot reload) |
| `bun run build` | Production static build → `dist/` (+ service worker) |
| `bun run preview` | Preview production build |
| `bun run check` | Astro + TypeScript checks |
| `bun run test:unit` | Unit tests (`src/**/*.test.ts`) |
| `bun run test:integration` | Build + `dist/` contract tests |
| `bun run test:e2e` | Build + Playwright browser tests |
| `bun run test:all` | Unit + integration + e2e |
| `bun run lint` | Biome lint |
(back to top)
## Deep links
Prefill the editor from another site (plain text or `b64.`-encoded; 1300-character cap per quote field):
```html
Frame this quote
```
| Param | Meaning |
|-------|---------|
| `q` | Primary quote text |
| `q2` | Optional second line (translation / alternate) |
| `author` | Author name |
Use **Link** in the app toolbar to copy a shareable URL for the current quote.
(back to top)
## Self-hosting
Hmmm is a **static** site (`dist/`). No database, no server-side API.
### Vercel
1. Import the repo in [Vercel](https://vercel.com).
2. Framework: **Other** / static (or leave auto-detect).
3. Build command: `bun run build` (see `vercel.json`).
4. Output directory: `dist`.
5. Node version: **22** (`.nvmrc` / `engines`).
Cache headers for hashed assets, HTML shells, and the service worker are defined in `vercel.json`. Redeploys surface an in-app **Update available** prompt (service worker); no version API required.
### Docker
Multi-stage image: build with Bun, serve with **nginx** (`Dockerfile`, `deploy/nginx.conf`).
```bash
docker build -t hmmm:latest .
docker run --rm -p 8080:80 hmmm:latest
# → http://localhost:8080
```
**Compose:**
```bash
docker compose up --build -d
# → http://localhost:8080
docker compose down
```
### Reverse proxy notes
- Prefer **HTTPS** in production (required for full PWA install on most mobile browsers).
- Do not long-cache `/sw.js` or `*.webmanifest` (nginx already uses `must-revalidate`).
- HTML for `/` and `/create` also uses `must-revalidate` so cold loads pick up new shells after you ship an image.
- Open tabs check for a new service worker on focus and about every 5 minutes, then prompt **Reload**.
- `/create?q=…&q2=…&author=…` deep links work; `/` and `/create` are real app pages.
(back to top)
## Backgrounds (zero backend)
- **Gallery** — local SVGs + photo packs (Abstract, Nature, Texture, Urban, Dark, Paper) in `public/backgrounds/`
- **Upload** — stays on the device; never sent to a server
- **No** stock photo APIs, **no** paste-image-URL (avoids CORS export issues)
- Credits: [public/backgrounds/CREDITS.md](./public/backgrounds/CREDITS.md)
## Project status
**v1.0.0** — first public release. See [CHANGELOG.md](./CHANGELOG.md).
| Phase | Focus | Status |
|-------|--------|--------|
| 0–5 | Product + PWA | Complete |
| 6 | Unit tests | Complete |
| 7 | Integration + e2e | Complete |
| 8 | Self-hosting (local / Vercel / Docker) | Complete |
| 9 | Curated local backgrounds | Complete |
Full plan: [plans/PLAN.md](./plans/PLAN.md).
### Constraints (by design)
- No login / authentication
- No database
- Hard cap: **1300 characters** per quote field; **100 characters** for author
## Brand assets
Every asset below is cut from the same wordmark paths in [`logos/logo.svg`](./logos/logo.svg) — one mark, one place to change it.
| Asset | Path | Notes |
|-------|------|-------|
| README banner | [`banner.svg`](./logos/banners/banner.svg) · [`banner.png`](./logos/banners/banner.png) | 1200×420; the SVG is the editable source |
| Logo | [`logo.svg`](./logos/logo.svg) · [`logo.png`](./logos/logo.png) | 1000×1000, blush field |
| Wordmark only | [`public/logo.svg`](./public/logo.svg) | transparent background |
| Favicon | [`public/favicon.svg`](./public/favicon.svg) · [`favicon.ico`](./public/favicon.ico) | ICO carries 16 / 32 / 48 |
| App icons | [`public/icons/`](./public/icons/) | apple-touch 180 · PWA 192 / 512 · maskable 192 / 512 |
Mark palette: blush `#FFE0F9` · magenta `#D01F6A` · ink `#131313`. The full token set lives in [DESIGN.md](./DESIGN.md).
## License
Distributed under the **MIT** License. See [LICENSE](./LICENSE).
Background photography and textures: [public/backgrounds/CREDITS.md](./public/backgrounds/CREDITS.md).
## Acknowledgments
- [Cheng Lou — pretext](https://github.com/chenglou/pretext) for layout-aware text measurement
- [Astro](https://astro.build), [Vue](https://vuejs.org), [Tailwind CSS](https://tailwindcss.com), [Vite PWA](https://vite-pwa-org.netlify.app/)
- Design tokens and editorial system documented in [DESIGN.md](./DESIGN.md)
(back to top)