Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ memory/

The repo now also includes a clinic-facing marketing site under `src/site/`.

- Regenerate the static page set from the shared bilingual content source:
- `npm run site:generate`
- Start local website dev:
- `npm run site:dev`
- Build the static website:
Expand All @@ -69,6 +71,7 @@ The repo now also includes a clinic-facing marketing site under `src/site/`.
- `npm run site:preview`

Website build output goes to `dist/site/`.
The public funnel is Dutch-first at the root and mirrored in English under `src/site/en/`.
If you set `SITE_URL=https://your-domain.example` when running `npm run site:build`, the build also generates `sitemap.xml` and a `robots.txt` file that includes the sitemap location.

Deployment notes live in `docs/website/deployment.md`.
Expand Down
17 changes: 17 additions & 0 deletions config/generate-site-pages.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { mkdir, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

import { renderAllPages } from "../src/site/lib/site-render.js";

const configDir = dirname(fileURLToPath(import.meta.url));
const projectRoot = resolve(configDir, "..");
const siteRoot = resolve(projectRoot, "src/site");

const pages = renderAllPages();

for (const page of pages) {
const outputPath = resolve(siteRoot, page.path);
await mkdir(dirname(outputPath), { recursive: true });
await writeFile(outputPath, page.html, "utf8");
}
41 changes: 33 additions & 8 deletions config/site-postbuild.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,31 @@ import { readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

import fg from "fast-glob";

const configDir = dirname(fileURLToPath(import.meta.url));
const projectRoot = resolve(configDir, "..");
const distDir = resolve(projectRoot, "dist/site");

const rawSiteUrl = process.env.SITE_URL?.trim();
const siteUrl = rawSiteUrl ? rawSiteUrl.replace(/\/+$/, "") : "";
const socialImageUrl = siteUrl ? `${siteUrl}/social-preview.png` : "./social-preview.png";

const robotsLines = ["User-agent: *", "Allow: /"];
const htmlFiles = ["index.html", "privacy.html", "patient-notice.html", "404.html"];
const sitemapPaths = ["/", "/privacy.html", "/patient-notice.html"];

const htmlFiles = fg.sync(["**/*.html"], {
cwd: distDir,
onlyFiles: true,
});

const sitemapPaths = htmlFiles
.filter((file) => !file.endsWith("404.html"))
.map((file) => {
const normalized = file.replace(/\\/g, "/");
return normalized === "index.html"
? "/"
: normalized.endsWith("/index.html")
? `/${normalized.slice(0, -"index.html".length)}`
: `/${normalized}`;
});

if (siteUrl) {
robotsLines.push("", `Sitemap: ${siteUrl}/sitemap.xml`);
Expand All @@ -35,16 +49,27 @@ if (siteUrl) {
for (const htmlFile of htmlFiles) {
const filePath = resolve(distDir, htmlFile);
let html = await readFile(filePath, "utf8");
const normalized = htmlFile.replace(/\\/g, "/");
const pagePath =
normalized === "index.html"
? "/"
: normalized.endsWith("/index.html")
? `/${normalized.slice(0, -"index.html".length)}`
: `/${normalized}`;
const depth = normalized.split("/").length - 1;
const socialImageRelative = `${depth ? "../".repeat(depth) : "./"}social-preview.png`;
const canonicalUrl = siteUrl ? `${siteUrl}${pagePath}` : "";
const socialImageUrl = siteUrl ? `${siteUrl}/social-preview.png` : socialImageRelative;

if (siteUrl) {
html = html
.replaceAll("__SITE_URL__", siteUrl)
.replaceAll("__CANONICAL_URL__", canonicalUrl)
.replaceAll("__SOCIAL_IMAGE__", socialImageUrl);
} else {
html = html
.replace(/^\s*<meta\s+property="og:url"\s+content="__SITE_URL__\/"\s*\/>\n?/m, "")
.replace(/^\s*<link\s+rel="canonical"\s+href="__SITE_URL__\/"\s*\/>\n?/m, "")
.replaceAll("__SOCIAL_IMAGE__", "./social-preview.png");
.replace(/^\s*<meta\s+property="og:url"\s+content="__CANONICAL_URL__"\s*\/>\n?/m, "")
.replace(/^\s*<link\s+rel="canonical"\s+href="__CANONICAL_URL__"\s*\/>\n?/m, "")
.replaceAll("__SOCIAL_IMAGE__", socialImageRelative);
}

await writeFile(filePath, html, "utf8");
Expand Down
22 changes: 14 additions & 8 deletions config/vite.site.config.mjs
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

import fg from "fast-glob";
import { defineConfig } from "vite";

const configDir = dirname(fileURLToPath(import.meta.url));
const projectRoot = resolve(configDir, "..");
const siteRoot = resolve(projectRoot, "src/site");

const htmlEntries = fg.sync(["src/site/**/*.html"], {
cwd: projectRoot,
onlyFiles: true,
});

const input = Object.fromEntries(
htmlEntries.map((entry) => [entry.replace(/^src\/site\//, "").replaceAll("/", "__"), resolve(projectRoot, entry)]),
);

export default defineConfig({
base: "./",
root: resolve(projectRoot, "src/site"),
publicDir: resolve(projectRoot, "src/site/public"),
root: siteRoot,
publicDir: resolve(siteRoot, "public"),
build: {
emptyOutDir: true,
outDir: resolve(projectRoot, "dist/site"),
rollupOptions: {
input: {
main: resolve(projectRoot, "src/site/index.html"),
privacy: resolve(projectRoot, "src/site/privacy.html"),
patientNotice: resolve(projectRoot, "src/site/patient-notice.html"),
notFound: resolve(projectRoot, "src/site/404.html"),
},
input,
},
},
});
10 changes: 10 additions & 0 deletions docs/website/deployment.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Renvoo Website Deployment

The clinic website is a static Vite build.
The root site is Dutch-first, with mirrored English routes under `/en/`.

## Build

Expand All @@ -10,11 +11,19 @@ SITE_URL=https://your-domain.example npm run site:build
```

The static output goes to `dist/site/`.
`npm run site:build` regenerates the bilingual HTML pages before Vite builds them.

## What the build includes

- `index.html`
- `product.html`
- `pilot.html`
- `trust.html`
- `404.html`
- `en/index.html`
- `en/product.html`
- `en/pilot.html`
- `en/trust.html`
- `robots.txt`
- `site.webmanifest`
- `sitemap.xml` when `SITE_URL` is set
Expand Down Expand Up @@ -46,6 +55,7 @@ This repo now includes:
## Verification Checklist

- Homepage loads without broken assets
- Product, pilot, and trust pages load in Dutch and English
- `downloads/renvoo-clinic-one-pager.pdf` opens
- `downloads/renvoo-clinic-deck.pptx` downloads
- `404.html` renders correctly
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
"scripts": {
"build": "tsc -p tsconfig.json",
"dev": "tsx src/cli.ts",
"site:dev": "vite --config config/vite.site.config.mjs",
"site:build": "vite build --config config/vite.site.config.mjs && node config/site-postbuild.mjs",
"site:preview": "vite preview --config config/vite.site.config.mjs",
"site:generate": "node config/generate-site-pages.mjs",
"site:dev": "npm run site:generate && vite --config config/vite.site.config.mjs",
"site:build": "npm run site:generate && vite build --config config/vite.site.config.mjs && node config/site-postbuild.mjs",
"site:preview": "npm run site:generate && vite preview --config config/vite.site.config.mjs",
"typecheck": "tsc --noEmit -p tsconfig.json",
"test": "vitest run",
"memory:init": "npm run build && node dist/src/cli.js init",
Expand Down
138 changes: 84 additions & 54 deletions src/site/404.html
Original file line number Diff line number Diff line change
@@ -1,63 +1,93 @@
<!doctype html>
<html lang="en">
<html lang="nl">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>Renvoo | Page not found</title>
<meta
name="description"
content="The page you requested was not found. Return to the Renvoo clinic website."
/>
<link
rel="preconnect"
href="https://fonts.googleapis.com"
/>
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossorigin
/>
<link
href="https://fonts.googleapis.com/css2?family=Public+Sans:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;700&display=swap"
rel="stylesheet"
/>
<link
rel="icon"
type="image/png"
href="./icons/favicon.png"
/>
<link
rel="stylesheet"
href="./styles.css"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Renvoo | Pagina niet gevonden</title>
<meta name="description" content="De gevraagde Renvoo-pagina bestaat niet of is verplaatst." />
<meta name="theme-color" content="#f4efe7" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Renvoo | Pagina niet gevonden" />
<meta property="og:description" content="De gevraagde Renvoo-pagina bestaat niet of is verplaatst." />
<meta property="og:url" content="__CANONICAL_URL__" />
<meta property="og:image" content="__SOCIAL_IMAGE__" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Renvoo | Pagina niet gevonden" />
<meta name="twitter:description" content="De gevraagde Renvoo-pagina bestaat niet of is verplaatst." />
<meta name="twitter:image" content="__SOCIAL_IMAGE__" />
<link rel="canonical" href="__CANONICAL_URL__" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Newsreader:opsz,wght@6..72,400;6..72,500;6..72,700&display=swap" rel="stylesheet" />
<link rel="icon" type="image/png" href="./icons/favicon.png" />
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<div class="page-glow page-glow-one"></div>
<div class="page-glow page-glow-two"></div>

<main class="section-shell not-found-page">
<div class="not-found-card">
<img
class="not-found-logo"
src="./assets/renvoo-logo-horizontal.png"
alt="Renvoo"
/>
<p class="eyebrow">Page not found</p>
<h1>The page you were looking for is not here.</h1>
<p class="lead">
The safest next step is to go back to the clinic site and continue from the core Renvoo
overview.
</p>
<div class="hero-actions">
<a class="button button-primary" href="./index.html">Return to the homepage</a>
<a class="button button-secondary" href="./downloads/renvoo-clinic-one-pager.pptx" download>
Download the one-pager
</a>
<body class="page-notFound lang-nl" data-lang="nl" data-page="notFound" data-prefix="." data-switch-lang="en">
<a class="skip-link" href="#content">Ga naar inhoud</a>
<div class="page-aura page-aura-left" aria-hidden="true"></div>
<div class="page-aura page-aura-right" aria-hidden="true"></div>
<header class="site-header" data-header>
<div class="site-header-inner">
<a class="brand-mark" href="./index.html" aria-label="Renvoo Start">
<img src="./assets/renvoo-logo-horizontal.png" alt="Renvoo" width="1200" height="320" />
<span>Operationele SaaS voor rustigere agenda's</span>
</a>
<button class="nav-toggle" type="button" data-nav-toggle aria-expanded="false" aria-controls="site-nav" aria-label="Open navigatie">
<span></span>
<span></span>
<span></span>
</button>
<div class="site-nav-shell" data-site-nav-shell>
<nav class="site-nav" id="site-nav" aria-label="Primary">
<a class="" href="./index.html">Start</a><a class="" href="./product.html">Product</a><a class="" href="./pilot.html">Pilot</a><a class="" href="./trust.html">Vertrouwen</a>
</nav>
<div class="site-header-actions">
<a class="language-switch" href="./en/404.html" data-track="language_switched">EN</a>
<a class="header-cta" href="./pilot.html#booking" data-track="nav_book_meeting">Plan gesprek</a>
</div>
</div>
</div>
</header>
<main id="content" class="site-main">

<section class="chapter hero-page hero-not-found">
<div class="hero-grid hero-grid-wide">
<div class="hero-copy" data-motion="intro">
<p class="eyebrow">404</p>
<h1>Deze pagina bestaat niet meer.</h1>
<p class="hero-lead">Ga terug naar de funnel en kies opnieuw of u eerst product, vertrouwen of direct het validatiegesprek wilt bekijken.</p>
<div class="hero-actions">
<a class="button button-primary" href="./index.html">Terug naar start</a><a class="button button-secondary" href="./pilot.html">Naar planner</a>
</div>
</div>
</div>
</section>

</main>
<footer class="site-footer">
<div class="footer-grid">
<div class="footer-summary">
<p class="footer-kicker">Renvoo</p>
<p>Renvoo is een vroege B2B clinic SaaS voor Nederlandse tandartspraktijken die no-shows, late uitval en leeg stoeluur rustiger wil opvangen.</p>
</div>
<div>
<p class="footer-title">Funnel</p>
<ul class="footer-links"><li><a href="./index.html">Start</a></li><li><a href="./product.html">Product</a></li><li><a href="./pilot.html">Pilot</a></li><li><a href="./trust.html">Vertrouwen</a></li></ul>
</div>
<div>
<p class="footer-title">Juridisch</p>
<ul class="footer-links"><li><a href="./privacy.html">Privacyverklaring website</a></li><li><a href="./patient-notice.html">Notitie patiëntbericht</a></li></ul>
</div>
<div>
<p class="footer-title">Taal</p>
<ul class="footer-links"><li><a href="./en/404.html">English</a></li></ul>
</div>
</div>
<div class="footer-meta">
<p>Gebouwd rond pilot-proof, niet rond opgeblazen claims.</p>
<p><span data-year></span> Renvoo</p>
</div>
</footer>
<script type="module" src="./main.js"></script>
</body>
</html>
Loading