Marketing website built with React, TypeScript, and Vite, deployed on Vercel with two serverless API endpoints:
POST /api/sendfor contact emails via ResendPOST /api/newsletterfor newsletter capture via Resend
- React 18
- TypeScript
- Vite 7
- pnpm 10
- Vercel Functions (
api/) @lottiefiles/dotlottie-reactfor Lottie animations (replaces the legacydotlottie-playerweb component)- Redux Toolkit for global state (theme, language, animations toggle)
- Node.js 20.x
- pnpm 10.x
- Vercel CLI for local project sync
-
Install dependencies:
pnpm install
-
Copy the environment template and fill in the values:
cp .env.example .env.local
-
Validate the local environment:
make check-env
Start the frontend:
make devStart the local API server in another terminal:
make apiLocal URLs:
- Frontend:
http://localhost:3000 - Local API proxy target:
http://localhost:3001
Build the production bundle:
make buildPreview the production bundle:
make previewGenerate a bundle report:
make analyzeApplication variables:
VITE_RECAPTCHA_SITE_KEY: preferred public key used by Vite buildsREACT_APP_RECAPTCHA_SITE_KEY: backward-compatible fallback while the Vercel project is still carrying the legacy CRA variableRECAPTCHA_SECRET_KEY: server-side secret used by/api/sendand/api/newsletterRESEND_API_KEY: server-side key used by/api/sendand/api/newsletter
Vercel project sync variables:
VERCEL_TOKEN(optional, only for the manual GitHub sync workflow)
The repository now versions its Vercel project context in config/vercel-project-context.json, so VERCEL_TOKEN is the only secret needed when you run the GitHub sync workflow manually. It must be a long-lived Vercel access token created in the dashboard. VERCEL_PROJECT_ID and VERCEL_TEAM_ID remain available as optional overrides.
Repo-controlled deployment settings live in vercel.json:
- Vite framework preset
- pnpm install and build commands
distoutput directoryfluid: true- automatic Git deployments disabled for every branch except
production *.vercel.apphosts remain directly browsable for deployment fallback and debugging- GitHub deployment records still point to
https://mediasmart.ch
Dashboard-only project settings are versioned in config/vercel-project-settings.json.
Custom-domain routing is versioned in config/vercel-domains.json.
Both are synced with:
make vercel-sync-dry-run
make vercel-syncThe sync scripts apply:
productionDeploymentsFastLaneresourceConfig.fluidmediasmart.chas the production originwww.mediasmart.chas a 308 redirect tomediasmart.ch
The client now pins API requests with x-deployment-id, so the code is already ready for Vercel skew protection. The dashboard-level toggle itself still requires a Pro or Enterprise plan.
- reCAPTCHA is now enforced server-side on each form submission. The public client no longer performs a standalone
/api/verify-recaptchacall. - The under-construction newsletter form now uses the server-side Resend flow instead of exposing a browser-side email delivery provider.
- Public form endpoints add
Cache-Control: no-store, hidden honeypot fields, and stricter payload length validation.
The GitHub workflow .github/workflows/sync-vercel-project-settings.yml can re-apply those settings manually when you want GitHub to enforce the tracked Vercel configuration.
Repository-managed workflows now follow a lighter trigger strategy:
Security & Quality: runs manually or on pull requests targetingproductionCodeQL: runs manually, on pull requests targetingproduction, and on the weekly security scheduleSync Vercel Settings: runs manually onlyUpdate portfolio screenshots: runs manually or on the weekly screenshot refresh schedule, pushes a dedicated automation branch, and opens or updates a pull request instead of pushing directly toproductionwhen the token is allowed to create pull requests
This repository only defines the workflow files stored in .github/workflows/. Entries shown in the GitHub Actions UI such as Dependabot or other platform-managed features are managed by GitHub and are not controlled by these workflow files.
make helpMain targets:
make installmake updatemake devmake apimake buildmake previewmake analyzemake cleanmake check-envmake testmake test-watchmake test-coveragemake vercel-sync-dry-runmake vercel-syncmake vercel-sync-project-dry-runmake vercel-sync-projectmake vercel-sync-domains-dry-runmake vercel-sync-domains
api/ Vercel Functions
config/ Deployment settings tracked in git
public/ Static assets
scripts/ Local tooling and Vercel sync scripts
src/_archive/ Inactive components kept for future reimplementation
src/assets/lotties/ .lottie animation files (light + dark variants per animation)
src/components/
common/ Shared UI components (Navbar, Footer, Contact, …)
DotAnim.tsx Single animation player (wraps DotLottieReact)
layout/ Routing infrastructure (LangLayout, ErrorBoundary, RouteSeo)
preloader/ Full-page loading spinner
presentation/ Page-specific section components
cookies/ Cookie consent banner
home/ Home page sections
itServices/ IT services page sections
privacy-policy/ Privacy policy page (single unified component)
videoServices/ Video services page sections
src/config/
Config.tsx React Router configuration
lotties.ts Animation registry: keys, lazy loaders, and dimensions
src/pages/ Route-level page wrappers
src/services/
api/ Fetch helpers and reCAPTCHA client
aos/ AOS animation timing utilities
hooks/ Custom React hooks
locales/ i18n translation files (en/, fr/) + safe accessor
router/ Language-aware link helpers
seo/ SEO route metadata
src/store/
slices/common/ animationsSlice, themeSlice, languageSlice, cookieUtils
All user-facing text is managed through translation files in src/services/locales/:
src/services/locales/
en/ English translations (navbar, footer, home, it, video, cookies, privacy, …)
fr/ French translations (mirror structure)
index.ts Master dictionary — registers each namespace for both languages
safe.ts useTranslations(lang) hook — dot-notation accessor with fallback
To add a new translated string:
- Add the key to both
en/<namespace>.tsandfr/<namespace>.ts. - The key is immediately accessible via
t.text("namespace.key")in any component that callsuseTranslations(languageReducer).
Supported languages are defined in src/config/languages.ts. The active language is stored in Redux (languageSlice) and synced to the /:lang/ URL prefix by LangLayout.
Cookie consent is managed by src/components/presentation/cookies/index.tsx (the ModernCookieBanner component). Consent state is persisted in localStorage via src/store/slices/common/cookieUtils.ts.
Three optional cookie categories are presented to the user:
| Category | What it covers | Always on? |
|---|---|---|
| Necessary | Contact form, Calendly booking, reCAPTCHA | Yes |
| Functionality | Theme preference, language preference | No |
| Performance | Google Analytics | No |
Calendly functionality is treated as a necessary cookie and is always enabled — no user action required.
To open the consent modal programmatically from any component:
import { requestCookieSettingsOpen } from "store/slices/common/cookieUtils";
requestCookieSettingsOpen();To read current consent state in a component, use the useCookieConsent hook:
import useCookieConsent from "services/hooks/useCookieConsent";
const consent = useCookieConsent(); // ConsentPreferencesAnimations are powered by @lottiefiles/dotlottie-react. The WASM runtime is bundled locally (via vite-plugin-dotlottie-wasm-url) so the player never fetches from an external CDN, which the production Content-Security-Policy would block.
- Export two
.lottiefiles from After Effects / LottieFiles: one for light mode, one for dark mode. - Drop them in
src/assets/lotties/<section>/. - Add a new entry to
LOTTIE_LOADERSinsrc/config/lotties.tswith lazyimport()calls for both variants. - Add a matching entry to
LOTTIE_PRESENTATIONin the same file with the animation's native pixel dimensions (and an optionalscalefactor if it needs a visual boost). - Use
<DotAnim anim="your.key" />anywhere in the component tree.
DotAnim resolves the correct variant for the current theme automatically and switches files with a crossfade when the theme changes.
A Redux slice (animationsSlice) tracks whether animations are enabled globally. On first load the initial value is derived automatically:
- Disabled if the OS-level Reduce Motion accessibility preference is on.
- Disabled on devices with ≤ 2 CPU cores or < 2 GB RAM.
- Otherwise enabled.
The preference is persisted in a cookie (animations=on|off, 1-year expiry) so it survives page reloads. Users can toggle it from the navbar. All DotAnim instances respond immediately via the imperative dotLottieInstance.play() / .pause() API — no remount required.
To read or update the toggle from a component, use the useInterfaceControls hook:
const { animationsEnabled, flipAnimations } = useInterfaceControls();- If the contact form fails in production, confirm that either
VITE_RECAPTCHA_SITE_KEYorREACT_APP_RECAPTCHA_SITE_KEYexists in Vercel project env vars. - If
make vercel-syncfails locally, runvercel loginand confirm the project is linked. - If the sync GitHub workflow fails, verify that
VERCEL_TOKENis a dashboard-created Vercel access token. A local Vercel CLI session token is not sufficient for GitHub Actions. - If animations fail to load in production, verify that
wasm-unsafe-evalis present in theContent-Security-Policyheader defined invercel.json. The DotLottie WASM runtime requires it. - If an animation plays in the wrong variant after a theme switch, check that both
lightanddarkentries exist for its key inLOTTIE_LOADERSand that the corresponding.lottiefiles are present insrc/assets/lotties/.