feat: redesign — one message, one audience, light pages
CI / TypeScript — type-check (push) Successful in 18s
CI / Publish — build & push image (push) Skipped
CI / TypeScript — type-check (pull_request) Successful in 17s
CI / Publish — build & push image (pull_request) Skipped

The site felt old, unfocused and bloated: 11 pages, a features page of 171
bullets, a homepage of 830 words in card grids around seven mockup-style
screenshots, and no single thing it wanted a visitor to understand.

- Message: control instead of drift. Audience: IT departments in small and
  mid-sized companies. Goal: buy a licence — netOrk itself is free, the
  licence adds vulnerability data and image updates.
- Home is under 400 words: the drift comparison of a real access point, three
  steps, the hardware it runs on, vulnerabilities with a licence, what else is
  in the box, one closing band. Features, Drivers and Roadmap are gone; their
  URLs redirect (router and nginx 301).
- New Pricing page: the free core, Starter / Pro / Enterprise on request with
  the plan differences from the licence server, four questions; buttons go to
  the licence portal. The unit-less KB request limit is left out.
- Persona pages are one template; NIS2 and Plugins are cut to half or less.
  Impressum and Datenschutz exist as marked placeholders; the unsupported
  "MIT licence" claim is gone from the footer.
- Look: light paper and ink, the dark product on a stage, Inter self-hosted,
  split sections and ruled lists instead of cards. Four real, cropped
  screenshots replace eight full-window ones.
- Language follows the browser until someone chooses; <html lang> is set.
  Scroll-to-top on navigation, a catch-all route, no dead /docs/architecture.
- CLAUDE.md, DESIGN.md, PAGES.md and PRODUCT.md describe the new rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Christian Manivong
2026-09-29 23:24:49 +02:00
co-authored by Claude Opus 5.5
parent 369f66afdc
commit f70fec496a
35 changed files with 1698 additions and 2716 deletions
+21 -5
View File
@@ -9,18 +9,34 @@ interface LangContextValue {
const LangContext = createContext<LangContextValue | null>(null)
export function LangProvider({ children }: { children: ReactNode }) {
const [lang, setLangState] = useState<Lang>(() => {
// A stored choice wins; otherwise the browser decides: German for de-*, English for everything else.
function initialLang(): Lang {
try {
const stored = localStorage.getItem('lang')
return stored === 'de' || stored === 'en' ? stored : 'de'
})
if (stored === 'de' || stored === 'en') return stored
} catch {
// storage blocked: fall through to the browser language
}
const preferred = navigator.languages?.[0] ?? navigator.language ?? 'en'
return preferred.toLowerCase().startsWith('de') ? 'de' : 'en'
}
export function LangProvider({ children }: { children: ReactNode }) {
const [lang, setLangState] = useState<Lang>(initialLang)
// <html lang> drives hyphenation and screen readers.
useEffect(() => {
localStorage.setItem('lang', lang)
document.documentElement.lang = lang
}, [lang])
// Only an explicit switch is remembered, so the browser keeps deciding until someone chooses.
function setLang(l: Lang) {
setLangState(l)
try {
localStorage.setItem('lang', l)
} catch {
// storage blocked: the choice lasts for this visit
}
}
return (