The official landing page for DevSafe, a software development and cybersecurity agency based in Cameroon that also builds and owns its own products (e.g. BookBridge). Built with SvelteKit and styled with Tailwind CSS v3 to deliver a high-contrast, premium "SOC dashboard" dark tech aesthetic.
- Framework: SvelteKit (latest stable with Runes)
- Styling: Tailwind CSS v3 (using fluid grids, glassmorphism, and radial glows)
- Icons: @lucide/svelte (packaged with custom brand SVGs for GitHub and WhatsApp)
- Fonts:
Bricolage Grotesque(Headings)Inter(Body text)JetBrains Mono(Code blocks, technical tags, inputs)
- Interactive Consultation Gateway: A custom glassmorphic request form with animated submit states ("Establishing Secure Link...") and feedback panels.
- Scroll-Triggered Entrance Animations: A
use:revealaction animates elements as they enter the viewport. Content is always server-rendered, and the hidden pre-animation state only applies when JS is running (html.js), so crawlers and no-JS visitors see everything. - SEO: The site is prerendered to static HTML.
Seo.sveltehandles the title, description, canonical, Open Graph and Twitter tags,schema.tsadds the JSON-LD graph (ProfessionalService in Yaoundé, plus BookBridge and Eventra), andsitemap.xmlandrobots.txtare generated at build time. - Sticky Glassmorphism Header: Responsive navigation bar with dynamic borders, a language toggle, and a hamburger menu below the
lgbreakpoint. - Client Work vs. Owned Products: Separate showcase sections for agency client work (e.g. Eventra) and DevSafe-owned products (e.g. BookBridge), rendered by a shared
ProjectShowcasecomponent. - Bilingual (English / French): English lives at
/and French at/fr, with an FR/EN toggle in the header. Each version has its own<html lang>, canonical,hreflangalternates,og:localeand JSON-LDinLanguage, and both are listed in the sitemap. - Centralized Data Layer: All copy lives in
src/lib/data/content/(one file per language), avoiding hardcoded strings inside markup.
src/
├── lib/
│ ├── actions/
│ │ └── reveal.ts # SSR-safe scroll reveal action
│ ├── assets/
│ │ ├── devsafe-emblem.webp # DS mask emblem (dark theme)
│ │ ├── devsafe-emblem-light.webp # Same emblem with dark strokes (light theme)
│ │ ├── Founder.jpg # Profile picture for founder profile
│ │ └── screens/ # Product screenshots (WebP)
│ ├── components/
│ │ ├── BrandIcon.svelte # GitHub / LinkedIn / WhatsApp SVG marks (Lucide has no brand icons)
│ │ ├── CTABanner.svelte # Consultation form banner + WhatsApp button
│ │ ├── Footer.svelte # Footer links and WhatsApp integration
│ │ ├── Hero.svelte # Hero with the Cameroon hero photo (full colour)
│ │ ├── Navbar.svelte # Responsive glass header & mobile drawer
│ │ ├── PhoneFrame.svelte # Device frame for real product screenshots
│ │ ├── ProjectCard.svelte # Single project card with product screenshot
│ │ ├── ProjectShowcase.svelte # Client work & products sections (+ NDA client card)
│ │ ├── SectionPhoto.svelte # Decorative palette-tinted background photo
│ │ ├── SectionVideo.svelte # Silent looping background video (WhyDevSafe)
│ │ ├── Seo.svelte # <svelte:head> meta, canonical, OG, JSON-LD
│ │ ├── Services.svelte # Bento grid of services
│ │ ├── TrustStrip.svelte # Awards and client proof points under the hero
│ │ ├── Team.svelte # Founders photo, bios, highlights, LinkedIn/GitHub
│ │ ├── WhatsAppButton.svelte # Floating WhatsApp chat button
│ │ └── WhyDevSafe.svelte # Corporate differentiator grids
│ ├── config/
│ │ └── site.ts # Site URL, languages, locale helpers, indexable routes
│ ├── data/content/
│ │ ├── en.ts # English copy (source of truth for the content shape)
│ │ ├── fr.ts # French copy, typed as SiteContent
│ │ ├── types.ts # Shared content types
│ │ └── index.ts # t() / getContent(lang) helpers
│ └── seo/
│ ├── schema.ts # JSON-LD structured data
│ ├── sitemap.ts # Sitemap XML renderer (hreflang alternates, escaping)
│ └── sitemap-routes.ts # Static routes + dynamic collections published in the sitemap
├── routes/
│ ├── +layout.svelte # Imports global CSS & favicon link
│ ├── +layout.ts # prerender = true for the whole site
│ ├── [[lang=lang]]/ # Home page for / (en) and /fr (fr)
│ │ ├── +page.svelte # Main assembly page with SEO headers
│ │ └── +page.ts # Prerender entries for /fr
│ ├── robots.txt/+server.ts # Prerendered robots.txt
│ └── sitemap.xml/+server.ts# Prerendered sitemap (from sitemap-routes.ts)
├── params/lang.ts # Route matcher: only "fr" is a valid prefix
├── hooks.server.ts # Sets <html lang> per page
├── app.css # Global styles, variables & utility classes
├── app.html # HTML shell template
static/
├── apple-touch-icon.png # iOS home-screen icon
├── favicon.png # 64px emblem favicon
├── images/ # Hero photo + grayscale section photos (AVIF/WebP/JPEG)
├── videos/ # Silent grayscale loop (WebM/MP4) + poster used by SectionVideo.svelte
│ # hero-cameroon-* (full-colour hero photo), cta-code-* (contact section)
├── logo-512.png # Logo referenced by JSON-LD
└── og-image.png # 1200x630 social share imageColours live as RGB-channel CSS variables in src/app.css (--ds-*, with overrides under :root.light) and are exposed to Tailwind as ds-* colours, so opacity modifiers like bg-ds-accent/10 work. Use the semantic classes instead of raw Tailwind colours:
| Class | Use for |
|---|---|
text-ds-fg / text-ds-fg-muted / text-ds-fg-subtle |
headings / body copy / captions |
bg-ds-bg, bg-ds-surface, bg-ds-elevated, border-ds-border |
page, cards, raised elements, borders |
text-ds-accent, text-ds-accent-2 |
accent labels and links |
bg-ds-primary text-ds-on-primary hover:bg-ds-primary-hover |
primary buttons |
ds-success, ds-warning, ds-danger |
status colours |
Every text/background pairing meets WCAG AA (4.5:1) in both dark and light mode.
The palette is Cameroonian: a warm-black "Toghu night" with gold accents in dark mode, and a Sahel sand background in light mode. app.css also has a few cultural motif utilities, all original SVGs:
| Class | What it draws |
|---|---|
pattern-bg |
Faint Ndop-cloth diamond lattice behind a section (content must be relative z-10) |
toghu-band |
Toghu embroidery strip (black, red edges, gold braid) under the navbar and above the footer |
cm-rule |
Green, red and yellow flag rule under section headings |
cm-flag |
Tiny Cameroon flag used in the hero badge and footer |
The flag colours (--cm-green, --cm-red, --cm-yellow) are decorative only; never use them for text.
The site defaults to dark mode; light mode only applies after a visitor picks it with the theme toggle (saved in localStorage and applied in app.html before first paint).
The hero photo (hero-cameroon-*) is shown in full colour: on mobile it sits under the navbar at its natural 3:2 ratio, on desktop it fills the right 62% of the hero with the Reunification monument and statue in view. Other photos stay on-palette by shipping them in grayscale and tinting them in CSS with mix-blend-mode: luminosity over ds-bg (src/lib/components/SectionPhoto.svelte, used by the contact section; on mobile it is shown at its natural ratio instead of cropped). To add or replace a photo, export {name}-{width}.avif|webp for two widths plus {name}-{smallWidth}.jpg:
vips thumbnail source.jpg tmp.v 2400 && vips colourspace tmp.v gray.png b-w
avifenc -q 50 --yuv 400 gray.png static/images/cta-code-1920.avifBackground videos (SectionVideo.svelte) use the same tint. They have no audio track and are skipped for visitors with reduced motion or Save-Data enabled, who see the poster instead. The video only loads near the viewport and pauses off-screen. Encode as {name}-loop.webm|mp4 plus {name}-poster.avif|webp|jpg:
ffmpeg -i trimmed.mp4 -an -vf "fps=24,hue=s=0" -c:v libvpx-vp9 -crf 50 -b:v 0 static/videos/workshop-loop.webm
ffmpeg -i trimmed.mp4 -an -vf "fps=24,hue=s=0" -c:v libx264 -crf 28 -preset veryslow -pix_fmt yuv420p -movflags +faststart static/videos/workshop-loop.mp4English (src/lib/data/content/en.ts) defines the content shape. The French file is typed as SiteContent, so npm run check fails if a key is missing from fr.ts.
- Edit copy: change the string in both
en.tsandfr.ts. - Add a string: add it to
en.ts, add the translation tofr.ts, then read it in a component withconst c = $derived(t()). - French typography: use
’for apostrophes and a non-breaking space (U+00A0) before: ? !and inside« ».
Visitors are never redirected based on browser language; they choose with the toggle, and search engines pick the right version from the hreflang tags.
Routes are registered in src/lib/seo/sitemap-routes.ts:
- Static pages: add
{ path: '/services', changefreq: 'weekly', priority: 0.8 }tostaticRoutes. - Collections (case studies, docs, …): add a
dynamicSourcesentry with the route pattern (e.g./work/[slug]) and aload()that returns one entry per item.loadmay be async (CMS/API).
A registered route is only published once its +page.svelte exists, so the sitemap never lists a 404. The build log prints [sitemap] not yet published (no page): … for routes still waiting on a page. /services, /about, /contact and /work/[slug] are pre-registered.
Pages under src/routes/[[lang=lang]]/ are published in English and French with hreflang alternates. Pages outside it are English-only. Give each page a <Seo path="/your-path" lang={c.meta.lang} />; because prerender.origin is set in svelte.config.js, its hreflang links make the prerenderer build the French version too.
npm installnpm run devOpen http://localhost:5173 in your browser.
npm run checknpm run buildPreview the production build locally:
npm run previewThis project uses @sveltejs/adapter-auto, meaning it is configured out-of-the-box for serverless hosting on Vercel:
- Push this repository to GitHub/GitLab/Bitbucket.
- Go to Vercel and select Add New Project.
- Import the repository.
- Click Deploy. Vercel will automatically configure the build output settings and host the site.