feat: bilingual website (DE/EN) with language switcher
CI / TypeScript — type-check (push) Successful in 8s
CI / Publish — build & push image (push) Successful in 8s
CI / Deploy — pull & restart on host (push) Successful in 2s

Add LangContext with localStorage persistence (default: DE).
DE/EN toggle in Nav (top-right, before CTA button).

All pages translated:
- Home: all 8 sections incl. NIS2 block
- Features: all 13 sections with full German bullet points
- Drivers: headings and descriptions
- Getting Started (coming soon)
- Roadmap: bilingual data arrays inline (Planned + Under consideration)
- NIS2: full Art. 21 mapping, evidence blocks, coming-soon list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Manivong
2026-06-28 17:00:22 +02:00
co-authored by Claude Sonnet 4.6
parent f194c379e4
commit d48c7143aa
11 changed files with 1011 additions and 618 deletions
+40 -114
View File
@@ -6,6 +6,7 @@ import {
PuzzlePieceIcon,
ShieldCheckIcon,
} from '@heroicons/react/24/outline'
import { useLang } from '../context/LangContext'
const DRIVERS = [
'Arista EOS', 'Cisco IOS', 'Cisco IOS-XE', 'Cisco NX-OS', 'Fritz!Box',
@@ -243,32 +244,33 @@ function MockSecurity() {
}
export default function Home() {
const { t } = useLang()
const h = t.home
return (
<>
{/* 1 — Hero */}
<section className="py-24 md:py-32">
<div className="max-w-7xl mx-auto px-6 text-center">
<h1 className="text-4xl md:text-6xl font-bold text-slate-100 leading-tight mb-6">
Network orchestration<br />
<span className="text-sky-400">for heterogeneous infrastructure.</span>
{h.hero.line1}<br />
<span className="text-sky-400">{h.hero.line2}</span>
</h1>
<p className="text-base text-slate-400 leading-relaxed max-w-2xl mx-auto mb-10">
netOrk discovers, monitors, and manages your routers, switches, access points,
firewalls, and servers from a single UI — regardless of vendor.
No SaaS dependency. Runs on your infrastructure.
{h.hero.sub}
</p>
<div className="flex flex-wrap gap-4 justify-center">
<a
href="mailto:christian@manivong.de"
className="inline-flex items-center gap-2 px-6 py-3 rounded-lg bg-sky-600 hover:bg-sky-500 text-white font-medium transition-colors"
>
Get in touch →
{h.hero.cta1}
</a>
<Link
to="/features"
className="inline-flex items-center gap-2 px-6 py-3 rounded-lg border border-slate-700 hover:border-slate-500 text-slate-300 hover:text-slate-100 transition-colors"
>
View features
{h.hero.cta2}
</Link>
</div>
</div>
@@ -284,12 +286,7 @@ export default function Home() {
{/* 2 — Problem Statement */}
<section className="py-16">
<div className="max-w-3xl mx-auto px-6 text-center">
<p className="text-base text-slate-400 leading-relaxed">
Managing a mixed network means juggling a different admin UI for every vendor —
one for OPNsense, one for HP ProCurve, one for OpenWRT, one for Proxmox.
Config changes happen directly on devices with no audit trail.
You find out something drifted when it breaks.
</p>
<p className="text-base text-slate-400 leading-relaxed">{h.problem}</p>
</div>
</section>
@@ -303,34 +300,22 @@ export default function Home() {
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-sky-600/10">
<MagnifyingGlassIcon className="h-5 w-5 text-sky-400" />
</div>
<h3 className="mb-2 text-lg font-semibold text-slate-100">Discover everything on your network</h3>
<p className="text-sm text-slate-400 leading-relaxed">
ICMP sweep, SNMP scan, and HTTP probing find devices before you add them.
Fingerprinting identifies vendor and platform automatically.
Adopt results into your inventory with a single click.
</p>
<h3 className="mb-2 text-lg font-semibold text-slate-100">{h.cap1.title}</h3>
<p className="text-sm text-slate-400 leading-relaxed">{h.cap1.body}</p>
</div>
<div className="rounded-xl border border-slate-800 bg-slate-900 p-6">
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-sky-600/10">
<ChartBarIcon className="h-5 w-5 text-sky-400" />
</div>
<h3 className="mb-2 text-lg font-semibold text-slate-100">Poll device state continuously</h3>
<p className="text-sm text-slate-400 leading-relaxed">
Every device is polled on a configurable interval via NAPALM.
Interface status, ARP tables, DHCP leases, VLAN membership,
Docker containers, and SNMP health metrics — all in one place.
</p>
<h3 className="mb-2 text-lg font-semibold text-slate-100">{h.cap2.title}</h3>
<p className="text-sm text-slate-400 leading-relaxed">{h.cap2.body}</p>
</div>
<div className="rounded-xl border border-slate-800 bg-slate-900 p-6">
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-sky-600/10">
<WrenchScrewdriverIcon className="h-5 w-5 text-sky-400" />
</div>
<h3 className="mb-2 text-lg font-semibold text-slate-100">Detect drift. Fix it.</h3>
<p className="text-sm text-slate-400 leading-relaxed">
Define desired state in netOrk. On every poll, device config is compared against it.
Drifted devices get a warning; a one-click fix stream applies the correction and
shows you live SSH output.
</p>
<h3 className="mb-2 text-lg font-semibold text-slate-100">{h.cap3.title}</h3>
<p className="text-sm text-slate-400 leading-relaxed">{h.cap3.body}</p>
</div>
</div>
</div>
@@ -341,24 +326,18 @@ export default function Home() {
{/* 4 — Driver Grid */}
<section className="py-24">
<div className="max-w-7xl mx-auto px-6 text-center">
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">Works with your hardware</h2>
<p className="text-base text-slate-400 leading-relaxed max-w-xl mx-auto mb-10">
netOrk ships with custom NAPALM drivers for 11 device types, plus all built-in NAPALM drivers.
New drivers follow a documented registration pattern.
</p>
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.driversHeading}</h2>
<p className="text-base text-slate-400 leading-relaxed max-w-xl mx-auto mb-10">{h.driversSub}</p>
<div className="flex flex-wrap justify-center gap-3">
{DRIVERS.map((d) => (
<span
key={d}
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full border border-slate-700 bg-slate-900 text-xs font-medium text-slate-300"
>
<span key={d} className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full border border-slate-700 bg-slate-900 text-xs font-medium text-slate-300">
{d}
</span>
))}
</div>
<div className="mt-8">
<Link to="/drivers" className="text-sky-400 hover:text-sky-300 transition-colors text-sm font-medium">
Full driver reference →
{h.driversLink}
</Link>
</div>
</div>
@@ -369,44 +348,28 @@ export default function Home() {
{/* 5 — Screenshot Walkthrough */}
<section className="py-24">
<div className="max-w-7xl mx-auto px-6 space-y-24">
{/* Row 1 — left text, right screenshot */}
<div className="grid md:grid-cols-2 gap-12 items-center">
<div>
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">Device detail at a glance</h2>
<p className="text-base text-slate-400 leading-relaxed">
Hostname, IP, vendor, OS version, last poll time, and active warnings on one card.
Tabbed detail view for interfaces, LLDP neighbors, ARP table, VLAN membership,
packages, services, and scheduled jobs.
</p>
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.screenshot1.heading}</h2>
<p className="text-base text-slate-400 leading-relaxed">{h.screenshot1.body}</p>
</div>
<BrowserFrame label="netork.local / devices / ap-001">
<MockDeviceDetail />
</BrowserFrame>
</div>
{/* Row 2 — right text, left screenshot */}
<div className="grid md:grid-cols-2 gap-12 items-center">
<BrowserFrame label="netork.local / vlans">
<MockVlans />
</BrowserFrame>
<div>
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">Intent-based VLAN and SSID management</h2>
<p className="text-base text-slate-400 leading-relaxed">
Define VLAN names and SSID settings once. netOrk compares them against every polled
device and pushes corrections automatically via UCI (OpenWRT) or the device's native API.
</p>
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.screenshot2.heading}</h2>
<p className="text-base text-slate-400 leading-relaxed">{h.screenshot2.body}</p>
</div>
</div>
{/* Row 3 — left text, right screenshot */}
<div className="grid md:grid-cols-2 gap-12 items-center">
<div>
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">Security visibility per device</h2>
<p className="text-base text-slate-400 leading-relaxed">
Wazuh agent status, CVE counts by severity, and recent alerts — all linked to the device
record. One-click agent install if the agent is missing.
Graylog syslog forwarding status with auto-fix.
</p>
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.screenshot3.heading}</h2>
<p className="text-base text-slate-400 leading-relaxed">{h.screenshot3.body}</p>
</div>
<BrowserFrame label="netork.local / devices / ap-001 / security">
<MockSecurity />
@@ -422,44 +385,17 @@ export default function Home() {
<div className="max-w-7xl mx-auto px-6">
<div className="grid md:grid-cols-2 gap-12 items-center">
<div>
<p className="text-xs font-semibold tracking-widest uppercase text-sky-500 mb-3">NIS2 · Art. 21</p>
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">
Evidence, not paperwork.
</h2>
<p className="text-base text-slate-400 leading-relaxed mb-8">
NIS2 Art. 21 mandates asset inventory, patch management, access
control, and audit trails as baseline technical measures. netOrk
doesn't bolt on a compliance layer — these are its day-to-day outputs.
</p>
<p className="text-xs font-semibold tracking-widest uppercase text-sky-500 mb-3">{h.nis2Label}</p>
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.nis2Heading}</h2>
<p className="text-base text-slate-400 leading-relaxed mb-8">{h.nis2Body}</p>
<div className="mb-8">
<Link to="/nis2" className="inline-flex items-center gap-2 text-sky-400 hover:text-sky-300 transition-colors text-sm font-medium">
<ShieldCheckIcon className="h-4 w-4" />
Full Art. 21 mapping →
{h.nis2Link}
</Link>
</div>
<div className="space-y-4">
{[
{
art: 'Art. 21 (2e)',
label: 'Patch & vulnerability management',
detail: 'Per-device update status, Wazuh CVE counts by severity',
},
{
art: 'Art. 21 (2h)',
label: 'Asset management & access control',
detail: 'Full device inventory, RBAC with four roles, complete audit log',
},
{
art: 'Art. 21 (2a)',
label: 'Risk analysis baseline',
detail: 'Config drift detection, SNMP health metrics, security agent coverage',
},
{
art: 'Art. 21 (2b)',
label: 'Incident detection',
detail: 'Wazuh alert history, CrowdSec decisions, Graylog syslog per device',
},
].map((m) => (
{h.nis2Items.map((m) => (
<div key={m.art} className="flex gap-4">
<span className="mt-0.5 shrink-0 w-28 text-xs font-mono text-sky-500">{m.art}</span>
<div>
@@ -486,15 +422,10 @@ export default function Home() {
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-sky-600/10">
<PuzzlePieceIcon className="h-5 w-5 text-sky-400" />
</div>
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">Built to extend</h2>
<p className="text-base text-slate-400 leading-relaxed max-w-2xl mb-6">
Integrations (Wazuh, Graylog, CrowdSec, apt-cacher-ng) are plugins that register into
the plugin system — they can be enabled or disabled per deployment without code changes.
Adding a new integration follows a documented pattern with a hook bus, typed metadata,
and a plugin registry.
</p>
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.pluginsHeading}</h2>
<p className="text-base text-slate-400 leading-relaxed max-w-2xl mb-6">{h.pluginsBody}</p>
<Link to="/plugins" className="text-sky-400 hover:text-sky-300 transition-colors text-sm font-medium">
Plugin system docs →
{h.pluginsLink}
</Link>
</div>
</div>
@@ -507,11 +438,8 @@ export default function Home() {
<div className="max-w-7xl mx-auto px-6">
<div className="grid md:grid-cols-2 gap-12 items-start">
<div>
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">Self-hosted. One command.</h2>
<p className="text-base text-slate-400 leading-relaxed">
netOrk runs in Docker Compose. Five containers: API, two worker pools, a Beat scheduler,
and an nginx UI server. No external dependencies beyond Redis and PostgreSQL.
</p>
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.deployHeading}</h2>
<p className="text-base text-slate-400 leading-relaxed">{h.deployBody}</p>
</div>
<pre className="rounded-xl border border-slate-800 bg-slate-900 p-6 font-mono text-sm text-slate-300 overflow-x-auto">
<code>{`# Clone + configure
@@ -528,18 +456,16 @@ bash scripts/deploy.sh 192.168.1.10`}</code>
<div className="border-t border-slate-800" />
{/* 8 — CTA Footer */}
{/* 8 — CTA */}
<section className="py-24">
<div className="max-w-7xl mx-auto px-6 text-center">
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">Interested?</h2>
<p className="text-base text-slate-400 leading-relaxed mb-8">
Deployment options and hosted plans are coming. Get in touch for early access.
</p>
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">{h.ctaHeading}</h2>
<p className="text-base text-slate-400 leading-relaxed mb-8">{h.ctaBody}</p>
<a
href="mailto:christian@manivong.de"
className="inline-flex items-center gap-2 px-6 py-3 rounded-lg bg-sky-600 hover:bg-sky-500 text-white font-medium transition-colors"
>
Get in touch →
{h.ctaButton}
</a>
</div>
</section>