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).
12 KiB
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
/convertirtool 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 byhreflang. Chosen over a single-URL toggle specifically because Google can only index one language per URL. - Root
/redirects to/fr/or/en/based onAccept-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_HOURSenv, default1), enforced bysrc/cleanup.js(cron-driven in production perREADME.md). - Max file size:
src/config.js→maxFileSizeMb(MAX_FILE_SIZE_MBenv, default100). - No account required: true today —
POST /api/jobsrequires 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.
- Auto-deletion:
- 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.jsxwrapsAppin aBrowserRouter.App.jsxbecomes a route table:/→ redirect component: readsnavigator.language, maps anything startingento/en/, everything else (including undetected) to/fr/./:lang/→HomePage(onlyfr/enaccepted; anything else renders aNotFound, since there's no other route to fall back to).
HomePagerenders inside a sharedLayout(header + footer), and is code-split viaReact.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-i18nextinitialized with the:langroute param as the single source of truth for the active language (viai18n.changeLanguage(lang)in auseEffectkeyed 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: darkis 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 tolocalStorage, which then wins on every subsequent load regardless of system preference. frontend/src/index.csscurrently already defines a CSS-variable token set with aprefers-color-scheme: darkmedia 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 currentsystem-uistack. - 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.errorsemantic role inFileCard.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-motionrespected (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)
- 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).
- 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).
- 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 currentApp.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 samehandleConvert. - Submitted-jobs state —
FileCardcomponents (existing polling/download logic unchanged), restyled to match the new card visual language. - 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.
- 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 fromimageToPdf.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'sEBOOK_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).
- Images: jpg, jpeg, png, webp, gif, tiff, avif, heic/heif, ico (all-pairs within the image set, plus HEIC/ICO as documented in
- 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 smalluseEffect-based head manager (no new dependency — directdocument.title/meta-tag DOM updates keyed on the:langparam), since this is a single staticindex.htmlshell shared by both language routes. - JSON-LD
SoftwareApplicationschema injected onHomePage(name "Ombrora Convert",applicationCategory: "UtilitiesApplication",offerswith price0). frontend/public/robots.txt(allow all) andfrontend/public/sitemap.xml(2 URLs:/fr/,/en/) added as static files.- Existing PostHog snippet in
index.htmlis 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 devinfrontend/(proxying/apito a locally runningsrc/server.js/src/worker.jsper existingvite.config.jsproxy).- Verify both
/fr/and/en/render correctly,/redirects appropriately, language switcher round-trips. - Verify dark-mode toggle, and first-load respecting
prefers-color-schemebefore any manual toggle has been made (test via OS-level dark mode withlocalStoragecleared). - 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_MBactually 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.