The landing page for Focale, an advanced, local-first raster image editor for Linux, Windows and macOS.
The site is a statically generated Nuxt application, published to GitHub Pages at https://focale-editor.app. It presents the editor and its downloads, and provides user guides and technical format references.
Focale Community centralizes bug reports, feature requests, improvements and questions about the editor, website, documentation and downloads. Include the page URL for website reports.
/issues redirects to the community's issue-template chooser. Its standalone
public/issues/index.html uses a meta refresh and a fallback link, so the alias
works on GitHub Pages without JavaScript or a Nuxt hydration step.
Nuxt 4 · OpenVue · @nuxtjs/i18n · SCSS · TypeScript, built with pnpm.
Six locales ship today — English, French, Spanish, Italian, Portuguese and German —
with English served unprefixed and the rest under /fr, /es, /it, /pt, /de.
The static page hydrates in its URL's language. On arrival at /,
app/plugins/browser-locale.client.ts redirects through onNuxtReady, after
hydration (including async pages), to an explicit saved language choice, or the
supported browser language (English fallback).
The redirect preserves query parameters and anchors, replaces the history entry
and leaves explicit localized or documentation links alone. Only selections in
LocaleSwitcher write the one-year focale_locale cookie, as manual:<code>.
Automatic detection and route changes do not save a preference. Older bare-code
cookies have an ambiguous origin; ignore and clear them on arrival at /, then
detect the browser language again. useLocalePreference shares this contract
between the selector and redirect plugin. Keep module-level browser detection
disabled so it cannot change translations before hydration.
pnpm install
pnpm dev # http://localhost:3000Quality gates, all expected to pass before a push:
pnpm install --frozen-lockfile
pnpm lint # ESLint, with @nuxt/eslint's stylistic rules
pnpm typecheck # vue-tsc against the generated Nuxt types
pnpm generate # the static build, exactly as CI runs it
node --experimental-strip-types --test test/browser_locale_redirect.test.ts test/download_catalog.test.ts test/changelog.test.ts
pnpm test:hydration # run after generate, against the actual static HTML and JSAfter dependency changes, verify the full frozen installation in a clean checkout. A lockfile-only check can miss missing transitive dependency snapshots.
pnpm generate writes .output/public, which can be served with any static
server (pnpm dlx serve .output/public).
The hydration test executes the generated client bundle in jsdom with HTML
scripting enabled. It covers the six locales, browser-language redirection,
saved preferences, explicit routes, query parameters and anchors, and the
download loading, empty/error and available states, including the fallback
without JavaScript. This is a DOM regression check, not a
browser layout test. Keep NoScriptDownloads' contents as escaped, opaque HTML:
with scripting enabled the HTML parser treats <noscript> contents as text,
so Vue must not try to hydrate nested components there.
The public download catalog has a default URL. Copy .env.example to .env
only when an override is needed; no signup form or secret is required.
| Variable | Purpose |
|---|---|
NUXT_PUBLIC_DOWNLOAD_CATALOG_URL |
Optional URL of the public catalog; defaults to https://get.focale-editor.app/downloads.json. |
The site is generated statically. useDownloads fetches the catalog in the
browser so releases can appear without rebuilding the website.
.github/workflows/deploy.yml runs on every push to main: it tests language
redirection, generates the site, checks hydration and uploads it to GitHub Pages.
Set Settings → Pages → Source to GitHub Actions in the repository settings.
The custom domain lives in public/CNAME, so it survives every deployment.
app/
├── assets/
│ └── styles/ SCSS tokens, mixins and the global stylesheet
├── components/ shared layout/, docs/, ui/ and local home/ sections
├── composables/ downloads, navigation, SEO and content sources
├── layouts/ the shell every page renders into
├── pages/ one route per file
└── theme/ the OpenVue preset, aligned with the editor's palette
i18n/locales/ one JSON file per language
public/ favicon.ico, apple-touch-icon, og-image, CNAME
Adding a page means adding app/pages/<name>.vue, its keys to the six locale
files, and — if it belongs in the navigation — an entry in
app/composables/useSiteNavigation.ts, which both the header and the footer read.
The sections in app/components/home/ belong to the homepage. They are excluded
from Nuxt's component auto-imports and imported explicitly by app/pages/index.vue.
Shared layout, documentation and UI components retain flat auto-imported names.
The header has three page links: Home, Documentation and Contribute (/support).
Homepage section anchors remain in the footer. SupportSection introduces ways
to help the project after the downloads and links to the localized support page;
its copy lives in support.home in each locale file. The hero and header link
to #downloads, which remains available while the catalog loads or is unavailable.
The website no longer contains an alpha signup form.
Using a new OpenVue component means naming it in primevue.components.include
in nuxt.config.ts. Auto-import is deliberately off: it registers all eighty-odd
components and inlines every one's theme CSS into every page, which tripled the
generated HTML. An undeclared component simply will not resolve.
app/assets/styles/_shared.scss is injected into every component's style block,
so tokens and mixins are available without importing them.
/changelog and its localized routes show the checked-in public/changelog.json
export. The private editor's Publish Focale workflow refreshes it from the
published source tag after desktop publication; its normal push then rebuilds
this site. No new credentials or runtime GitHub request are needed here.
An empty catalogue displays a translated fallback linking to the downloads.
Dates and page navigation are localized in all six languages; reviewed notes retain their
source language. Each version has a stable #v1.2.3 anchor. The footer links to
the localized history.
Schema version 1 contains releases in newest-first order, each with version,
ISO date and changes (plain-text kind and description). Invalid exports
fail static generation. Notes are rendered with Vue text interpolation, without
HTML or Markdown execution. The editor exporter understands its configured
dart run release template, excludes Unreleased sections and removes private
links. Coordinate format changes with tool/website_sync/changelog.py in Focale.
Do not invent release entries here; the source changelog is authoritative.
The private Focale application's Publish Focale workflow produces the logo, favicons, social image, reviewed notices and downloadable format references. It commits only selected public resources to this repository; the resulting push runs the Pages deployment. Building or developing this website requires neither private source access nor an editor checkout.
Maintainers with access to Focale can run Publish Focale → website on a chosen
source ref to refresh resources without a desktop release. Enable screenshots
only when regenerating the gallery. Release publication updates other resources
automatically from its source tag. The workflow and its FOCALE_WEBSITE_TOKEN
are configured in Focale, not here; no local directory conventions are required.
The gallery uses four scenes, both themes and French/English captures, encoded
as 1600/3200-pixel WebP images plus 320-pixel thumbnails. French routes use French
captures; other locales use English. Changes to scene IDs or image dimensions
require coordinating this public contract with the editor.
Palette tokens and the OpenVue preset are maintained in app/assets/styles and
app/theme/openvue-preset.ts.
The hero uses a translated title and accent phrase; the English social image
keeps “Retouch, compose and create” followed by the blue “with precision.”.
Its composition is generated by Focale's tool/website_sync/branding.py.
useDownloads loads https://get.focale-editor.app/downloads.json in the browser.
The Focale distribution workflow publishes this catalog only after the
macOS Apple Silicon, macOS Intel, Windows x64 and Linux x64 artifacts are available.
DownloadsSection presents their GitHub Release attachments on this main site;
the header and hero always link to the download section. While the catalog loads,
the section displays a loading status. An empty, invalid or unreachable catalog
displays a warning with a direct link to GitHub releases. No website rebuild is
needed for subsequent desktop versions. NUXT_PUBLIC_DOWNLOAD_CATALOG_URL can
override the endpoint at build time. The GetFocale repository hosts metadata only.
Download validation accepts focale-<version>-<target>.<extension> and the
previous Focale-<version>+<build>-<target> names. Windows installers retain
-setup.exe in both conventions; ZIP and MSIX packages omit that suffix.
Keep both forms so older releases remain downloadable. The target, version,
extension and public release path must match the catalog entry.
Catalog checks: node --experimental-strip-types --test test/download_catalog.test.ts.
Validation: pnpm lint, pnpm typecheck, pnpm generate (inspect .output/public).
app/pages/docs/ provides getting started, editing, shortcuts, troubleshooting,
platforms, import/export formats, native projects and native preset libraries,
alongside the existing licenses and corresponding-source pages. Each portable
library has its own page: .fbrush, .fpattern, .fshape, .fstyle, .fswatch,
.fgradient, .fcurve and .faction, with usage, schema, examples, validation
limits and interchange directions.
All pages ship in the six site languages.
DocumentationArticle renders the structured docs.articles content in the
locale JSON files; DocumentationLayout also serves the notices pages.
app/utils/documentation.ts registers stable routes, groups, code samples and
downloadable references. Add a route under app/pages/docs/, its metadata there
and the article in all six locales. Code samples stay outside translated strings
so JSON braces are not parsed as translation placeholders. useDocumentation
supplies both the index and navigation. documentationExamples.ts holds the
dedicated native-library JSON examples. Navigation back to homepage anchors
works from documentation routes too.