Webvork — a Next.js 16 design & engineering studio site with 3D hero, services, tools, contact API, and SEO.
Webvork is a design and engineering studio based in Leeuwarden, Friesland. This repository (package name "v11") is the studio's marketing website, built with Next.js 16 (App Router), React 19, Tailwind CSS v4, and Three.js. The site presents services, free checklists, a contact form, a project estimator, and a client-side Lighthouse audit tool.
The codebase is organized into app routes (pages), reusable section components, UI primitives, and a small 3D scene. Content is centralized in lib/site.ts, SEO logic in lib/seo.ts, and validation in lib/validation.ts. The site is static-first with dynamic routes for services and tools, and a single API route for handling contact form submissions via Resend.
All content is in Dutch primarily, with some English UI labels. The target audience is businesses in Northern Netherlands.
Prerequisites: Node.js 18+ (Next.js 16 requires Node 18 or 20), pnpm (or npm/yarn). The repository includes pnpm-workspace.yaml, so pnpm is recommended.
git clone https://github.com/Justinkarso/webvork-v11 cd webvork-v11 pnpm install
The package.json defines the following scripts:
Environment variables are not required for basic rendering, but the contact API and Site Auditor require keys:
Copy .env.example to .env.local and fill in values if you plan to use the contact form or Site Auditor without hitting rate limits.
After installation, run the development server:
pnpm dev
Open http://localhost:3000 to see the home page. The site includes the following routes:
To create a production build:
pnpm build pnpm start
Deploy to Vercel: connect the repository, set environment variables, and Vercel will automatically run next build. The next.config.ts sets security headers for all routes.
The contact API will return 500 if RESEND_API_KEY is missing. Set it in the Vercel environment or local .env.local.
The project uses a clear separation of concerns:
Sections are generally server components (no "use client") except those requiring interactivity (Nav, ContactForm, CookieBanner, SiteAuditor, Estimator, Checklist, BackToTop, Cta3DShowcase, Magnetic, Marquee, Reveal). The 3D scene is dynamically imported with ssr: false to avoid SSR issues.
All sections are composed in app/page.tsx (home) and other page components. The layout (app/layout.tsx) wraps everything with Nav, Footer, CookieBanner, and JSON-LD organization data.
Content for services, tools, process steps, stats, testimonials, FAQ, and navigation is defined statically in lib/site.ts as TypeScript constants. Pages import these constants and pass them to components.
Dynamic routes:
Client-side interactivity:
SEO and structured data are added via JsonLd component and Next.js metadata exports. lib/seo.ts contains functions for building JSON-LD objects and metadata. The root layout includes organization and website JSON-LD; pages add breadcrumbs, FAQ, or service schema.
The site is fully static except for the API route and client-side fetching; no database is used. Content updates require editing lib/site.ts and redeploying.
The design system is defined in app/globals.css using Tailwind CSS v4 with custom properties and @theme inline. The palette is dark with accent colors: lime (#c6f73b), cyan (#2de2e6), violet (#8b5cf6), and magenta (#ff4d8d).
:root { --ink: #06060a; --ink-2: #0a0a12; --ink-3: #101019; --mist: #ececf1; --muted: #9696ab; --faint: #7b7b92; --lime: #c6f73b; --cyan: #2de2e6; --violet: #8b5cf6; --magenta: #ff4d8d; }
These tokens are mapped into Tailwind utility classes via @theme inline, providing classes like text-lime, bg-ink, border-line, etc. Custom utilities include .text-iridescent, .border-glow, .glass, .glow-lime, .reveal, .mask-fade-x.
Accent variants are managed through lib/accent.ts which exports ACCENT record with style strings per accent (text, border, soft, dot, glowHover, hex, rgb). Components use these to apply accent-specific classes.
The 3D scene samples the same palette constants (PALETTE in scene-canvas.tsx) to keep visual consistency.
The site includes reduced-motion support: animations are disabled or minimized when the user prefers reduced motion. The 3D scene also stops on small screens/touch devices.
All site content lives in lib/site.ts. The main types and arrays are:
1. Add a new entry to SERVICES with all required fields. Accent must be one of lime, cyan, violet, magenta.
2. Ensure slug is unique and matches the desired URL (/services/[slug]).
3. Optionally add SEO overrides in lib/seo.ts SERVICE_SEO map; otherwise fallback to service.title and service.intro.
The dynamic page app/services/[slug]/page.tsx will automatically generate a static page due to generateStaticParams.
Similar to services, add a Tool object to TOOLS. The tool page uses the Checklist component which stores progress per slug in localStorage.
The content model is strongly typed; editors can modify lib/site.ts without touching components.
SEO is implemented through:
lib/seo.ts centralizes:
export const SITE_URL = "https://www.webvork.nl"; export const TARGET_KEYWORDS = ["website laten maken", "maatwerk website", ...]; export const DEFAULT_TITLE = "Maatwerk websites & web apps · Noord-Nederland | Webvork"; export const DEFAULT_DESCRIPTION = "Design- & ontwikkelstudio in Leeuwarden...";
Functions include pageMetadata(), serviceMetadata(), organizationJsonLd(), webSiteJsonLd(), breadcrumbJsonLd(), faqJsonLd(), serviceJsonLd(), allSitemapPaths(). These are used in page headers.
app/sitemap.ts uses allSitemapPaths() to generate URLs for /, /contact, /tools, all services, and all tools. The homepage has weekly change frequency and priority 1; services priority 0.9; others 0.7.
app/robots.ts allows all user agents and AI crawlers, but disallows /api/.
Metadata base is set in app/layout.tsx as https://www.webvork.nl. Ensure this matches production.
The site is optimized for Core Web Vitals:
// hero-scene.tsx const canRunWebGL = window.matchMedia("(min-width: 768px) and (pointer: fine)").matches; if (!canRunWebGL) return () => io.disconnect(); const idleId = scheduleIdle(() => setLoadScene(true));
The SceneCanvas uses dpr={[1, 1.5]}, frameloop={active ? "always" : "never"}, and sets powerPreference to "high-performance". It also respects prefers-reduced-motion.
The project uses Tailwind v4 with @import statement; no custom postcss config beyond standard.
The contact form sends emails via Resend:
// app/api/contact/route.ts (excerpt) const fromEmail = process.env.RESEND_FROM_EMAIL ?? "Webvork <onboarding@resend.dev>"; const toEmail = process.env.CONTACT_TO_EMAIL ?? CONTACT.email; const { error } = await resend.emails.send({ from: fromEmail, to: [toEmail], replyTo: email, subject, text: body });
The form displays success state and allows reset. It also catches network errors and shows user-friendly message.
Validate on both client and server. Server never trusts client input.
headers: async () => [{ source: "/(.*)", headers: [ { key: "Content-Security-Policy", value: csp }, { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains" }, { key: "Cross-Origin-Opener-Policy", value: "same-origin" }, { key: "X-Content-Type-Options", value: "nosniff" }, { key: "X-Frame-Options", value: "DENY" }, { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, ], }]
CSP includes allowlisting for scripts (unsafe-eval for Three.js), styles, images, fonts, and connect-src https: for API calls.
Sends a contact email via Resend.
200 OK: { "ok": true }
400 Bad Request: { "error": "..." } (invalid JSON, missing fields, invalid email, short message)
500 Internal Server Error: { "error": "Email service is not configured." } (missing RESEND_API_KEY)
502 Bad Gateway: { "error": "Could not send your message right now." } (Resend send error)
// Example valid request { "name": "Jane Doe", "email": "jane@example.com", "company": "Acme Inc", "type": "Web App", "message": "We need a dashboard for internal use." }
The API does not implement rate limiting. Consider adding if deployed publicly.
Root layout (app/layout.tsx) includes fonts, global metadata, Nav, Footer, CookieBanner, and JSON-LD.