# Ombrora Convert — frontend redesign (foundation) Date: 2026-07-31 ## Goal Redesign `frontend/` into a modern, minimalist, dark-mode-capable, bilingual (FR/EN), SEO-friendly single page that embeds the existing file-conversion tool directly in the homepage, with reassurance elements built from real backend facts. This is Sub-project 1 of two: it delivers the shared shell (design system, i18n, dark mode, SEO fundamentals). A follow-up Sub-project 2 (programmatic per-format-pair landing pages, e.g. `/fr/jpg-vers-png`) is deliberately out of scope here — it needs this foundation to exist first, and needs its own content/routing spec. ## Scope decisions (from brainstorming) - **One merged page, not two.** Originally considered a marketing Home + separate `/convertir` tool page; changed after user feedback — the upload/convert flow is embedded directly in the homepage hero. There is nothing left for a separate tool route to do, so it's dropped. - **i18n via URL prefix**, not a client-side toggle: `/fr/` and `/en/` are distinct, indexable, bilingual URLs connected by `hreflang`. Chosen over a single-URL toggle specifically because Google can only index one language per URL. - **Root `/` redirects** to `/fr/` or `/en/` based on `Accept-Language`, defaulting to `/fr/` if undetected — avoids duplicate content on `/` vs `/fr/`. - **Legal pages (mentions légales, confidentialité, CGU) are deferred.** No real company/hosting/legal data exists yet to put in them; publishing placeholder legal text would be worse than not having the pages. Revisit once that content is ready. - **Reassurance claims are limited to what the backend actually guarantees today**, verified in code, not invented: - Auto-deletion: `src/config.js` → `retentionHours` (`RETENTION_HOURS` env, default `1`), enforced by `src/cleanup.js` (cron-driven in production per `README.md`). - Max file size: `src/config.js` → `maxFileSizeMb` (`MAX_FILE_SIZE_MB` env, default `100`). - No account required: true today — `POST /api/jobs` requires no auth. - 100% online / nothing to install: true — browser upload, server-side conversion. - RGPD/hosting-location messaging was explicitly **not** requested by the user and is not included. - **Brand name: "Ombrora Convert"** (per user), replacing the generic "file-converter"/"Convertisseur de fichiers" placeholder title. - **No SSR/prerendering in this sprint.** The site remains a client-rendered Vite SPA; SEO relies on on-page tags, structured data, and Googlebot's JS rendering. Flagged as a known limitation, not a gap to silently fix — a candidate for Sub-project 2 if organic performance requires it. - **No new frontend test harness.** `frontend/` has no test setup today; adding one is a separate decision, not bundled into a redesign. Verification for this sprint is manual (`npm run dev`, both languages, both themes, multiple viewport widths). ## Architecture ### Dependencies (new, all pure JS — no native bindings, safe for o2switch) Added to `frontend/package.json`: - `react-router-dom` — routing. - `react-i18next`, `i18next`, `i18next-browser-languagedetector` — i18n. - `@phosphor-icons/react` — icon set (outline style, per design system below). ### Routing - `frontend/src/main.jsx` wraps `App` in a `BrowserRouter`. - `App.jsx` becomes a route table: - `/` → redirect component: reads `navigator.language`, maps anything starting `en` to `/en/`, everything else (including undetected) to `/fr/`. - `/:lang/` → `HomePage` (only `fr`/`en` accepted; anything else renders a `NotFound`, since there's no other route to fall back to). - `HomePage` renders inside a shared `Layout` (header + footer), and is code-split via `React.lazy` (per stack guidance: lazy-load route-level components rather than importing everything upfront) — a single-page site doesn't gain much from this today, but it establishes the pattern Sub-project 2 needs when more routes exist. ### i18n - `frontend/src/locales/fr.json`, `frontend/src/locales/en.json` — flat key/value dictionaries for all UI strings (headline, reassurance bullets, format-family labels, button/label text, tool status messages). - `react-i18next` initialized with the `:lang` route param as the single source of truth for the active language (via `i18n.changeLanguage(lang)` in a `useEffect` keyed on the param) — no separate persisted-locale state, so the URL and displayed language can never disagree. - Language switcher in the header: a two-item control (FR/EN) that navigates to the sibling `/:otherLang/` path, preserving no query/hash state (there is none to preserve). ### Dark mode - Toggle in the header, plus first-load detection of `prefers-color-scheme`. Per the design system's own guidance ("avoid dark mode by default"), the *default* is light — `prefers-color-scheme: dark` is honored as an initial value, not forced. - Implementation: a `data-theme="light"|"dark"` attribute on ``, set on mount from (in priority order) `localStorage.getItem('theme')` → `matchMedia('(prefers-color-scheme: dark)')` → `"light"`. Toggling writes the explicit choice back to `localStorage`, which then wins on every subsequent load regardless of system preference. - `frontend/src/index.css` currently already defines a CSS-variable token set with a `prefers-color-scheme: dark` media query (leftover from the Vite template scaffold, purple accent). This is replaced outright by the new token set (below), driven by `[data-theme="dark"]` attribute selectors instead of the media query alone, so the manual toggle can override system preference. ## Design system From `ui-ux-pro-max --design-system "file conversion SaaS tool modern minimal trustworthy"` (Flat Design × Minimal Single Column pattern): - **Typography:** Plus Jakarta Sans (Google Fonts `@import`), replacing the current `system-ui` stack. - **Color tokens** (light, with dark override for each): - `--color-primary: #2563EB` / dark `#3B82F6` - `--color-accent: #D97706` (CTA — the upload/convert action) - `--color-background: #F8FAFC` / dark `#16171d`-class dark surface - `--color-foreground: #0F172A` / dark light-gray text - `--color-muted`, `--color-border`, `--color-destructive: #DC2626` (error states — reuses the existing `.error` semantic role in `FileCard.jsx`) - **Icons:** Phosphor, outline weight, never emoji — `UploadSimple` (dropzone), `File`/`FileText` (per-file cards), `DownloadSimple` (download link), plus one icon per reassurance bullet (e.g. a clock for auto-delete, a shield or lock for no-account). - **Effects:** flat surfaces, no shadows/gradients, 150–300ms transitions, `prefers-reduced-motion` respected (transitions become instant when set). - **Accessibility:** 4.5:1 minimum contrast in both themes, visible focus rings (not removed for aesthetics), all interactive controls ≥44×44px touch target. ## Page structure (single page, FR content shown — EN mirrors via i18n keys) 1. **Header** — logo/wordmark "Ombrora Convert", language switcher (FR/EN), dark-mode toggle. Single row at every breakpoint (no nav items to overflow, so no hamburger menu needed). 2. **Hero** — headline + one-line description + the **upload dropzone embedded directly in the hero** (drag-and-drop via native HTML5 DnD events on a styled dropzone, or click to open the file picker — no drag-drop library needed). 3. **Selected-files state** (conditionally rendered, same trigger as today's `pendingFiles.length > 0`): per-file cards directly below the dropzone, each with the existing target-format ``, and PDF-compression checkbox+slider — same logic as current `App.jsx`, restyled into a card layout instead of an inline `
  • ` row (today's single-row-per-file layout is what breaks on narrow viewports; this is a real layout fix, not cosmetic). "Convertir" button triggers the same `handleConvert`. 4. **Submitted-jobs state** — `FileCard` components (existing polling/download logic unchanged), restyled to match the new card visual language. 5. **Reassurance strip** — 4 items, icon + short label, sourced only from the verified facts above: auto-delete after 1h, no account required, 100% online/nothing to install, up to 100MB per file. 6. **Supported-formats section** — grouped by family, grid layout (1 col mobile → 2 tablet → 4 desktop): - **Images:** jpg, jpeg, png, webp, gif, tiff, avif, heic/heif, ico (all-pairs within the image set, plus HEIC/ICO as documented in `src/converters/heic.js`/`ico.js`, plus image→PDF from `imageToPdf.js`) - **Documents:** docx, txt, html, md, pdf, csv, xlsx (per `src/converters/document.js`'s registered pairs) - **Polices:** ttf, otf, woff, dfont (per `src/converters/font.js`/`dfont.js`) - **Ebooks:** epub, fb2, lrf, mobi, pdb, rb, snb, tcr, azw3, pdf (per `src/converters/ebook.js`'s `EBOOK_FORMATS`) - This list is **hand-maintained in the frontend**, not fetched live from `GET /api/formats` — that endpoint answers "given this one source format, what targets exist," which isn't the right shape for a "here's everything we support" marketing grid. Risk: it can drift from the registry if a converter is added/removed later without updating this section — accepted for this sprint (YAGNI a generated-list build step until the format catalog changes often enough to justify one). 7. **Footer** — reassurance recap (short form) + format-family recap + language switcher. No legal links (deferred, see above). ## Responsive behavior - Mobile-first, breakpoints 375 / 768 / 1024 / 1440px. - Dropzone and per-file cards: full-width, stacked vertically on mobile; format/quality controls stack under the filename instead of the current single inline row. - Formats grid: 1 → 2 → 4 columns across the breakpoints above. - Header: fixed single-row layout at all sizes. - All interactive elements (dropzone, toggle, language switcher, sliders, selects) sized for ≥44×44px touch targets. ## SEO - `frontend/index.html`: real `` ("Ombrora Convert — Conversion de fichiers en ligne" / EN equivalent set dynamically per route — see note below), `<meta name="description">`, OpenGraph (`og:title`, `og:description`, `og:type=website`, `og:locale`), Twitter card tags. - Per-language `<title>`/meta description and `<link rel="alternate" hreflang="fr">` / `hreflang="en"` / `hreflang="x-default"` are set client-side per route via a small `useEffect`-based head manager (no new dependency — direct `document.title`/meta-tag DOM updates keyed on the `:lang` param), since this is a single static `index.html` shell shared by both language routes. - JSON-LD `SoftwareApplication` schema injected on `HomePage` (name "Ombrora Convert", `applicationCategory: "UtilitiesApplication"`, `offers` with price `0`). - `frontend/public/robots.txt` (allow all) and `frontend/public/sitemap.xml` (2 URLs: `/fr/`, `/en/`) added as static files. - Existing PostHog snippet in `index.html` is untouched. ## Out of scope (explicitly deferred) - Sub-project 2: programmatic per-format-pair SEO landing pages, sitemap generation at scale, internal linking between them. - Legal pages (mentions légales, confidentialité, CGU) — pending real company/hosting data. - SSR/prerendering. - A frontend automated test harness. - RGPD/hosting-location trust messaging (not requested). ## Testing Manual only, per the "no new test harness" decision above: - `npm run dev` in `frontend/` (proxying `/api` to a locally running `src/server.js`/`src/worker.js` per existing `vite.config.js` proxy). - Verify both `/fr/` and `/en/` render correctly, `/` redirects appropriately, language switcher round-trips. - Verify dark-mode toggle, and first-load respecting `prefers-color-scheme` before any manual toggle has been made (test via OS-level dark mode with `localStorage` cleared). - Verify upload → format/quality selection → convert → poll → download flow still works end-to-end (functional logic is carried over from `App.jsx`/`FileCard.jsx`/`api.js`, not rewritten). - Verify responsive layout at 375/768/1024/1440px widths, particularly the per-file card controls and formats grid. - Verify reassurance figures shown in the UI (1h, 100MB) match whatever `RETENTION_HOURS`/`MAX_FILE_SIZE_MB` actually resolve to in the running backend, rather than being hardcoded strings that could drift from `.env` — confirm during implementation whether these should be hardcoded copy (matching today's defaults) or fetched from a small backend config-exposure endpoint; hardcoding is the default assumption for this spec since no such endpoint exists today and adding one is a backend change beyond "frontend redesign," but flag this as a decision point for the implementation plan.