Foundation sub-project: merged homepage+tool page, FR/EN i18n via URL prefix, dark mode, SEO basics, and reassurance elements backed by actual backend guarantees (1h auto-delete, 100MB max, no account).
117 lines
12 KiB
Markdown
117 lines
12 KiB
Markdown
# 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 `<html>`, 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 `<select>`, quality slider (image formats), PNG compression slider, ICO size `<select>`, and PDF-compression checkbox+slider — same logic as current `App.jsx`, restyled into a card layout instead of an inline `<li>` 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 `<title>` ("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.
|