Empty chair time becomes immediate revenue loss
+A scheduled appointment only creates value if the patient actually shows up.
+diff --git a/README.md b/README.md index f267edc..33b41dc 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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`. diff --git a/config/generate-site-pages.mjs b/config/generate-site-pages.mjs new file mode 100644 index 0000000..8b5eac8 --- /dev/null +++ b/config/generate-site-pages.mjs @@ -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"); +} diff --git a/config/site-postbuild.mjs b/config/site-postbuild.mjs index 1259b01..2c0138e 100644 --- a/config/site-postbuild.mjs +++ b/config/site-postbuild.mjs @@ -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`); @@ -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*\n?/m, "") - .replace(/^\s*\n?/m, "") - .replaceAll("__SOCIAL_IMAGE__", "./social-preview.png"); + .replace(/^\s*\n?/m, "") + .replace(/^\s*\n?/m, "") + .replaceAll("__SOCIAL_IMAGE__", socialImageRelative); } await writeFile(filePath, html, "utf8"); diff --git a/config/vite.site.config.mjs b/config/vite.site.config.mjs index 100cc5a..e152586 100644 --- a/config/vite.site.config.mjs +++ b/config/vite.site.config.mjs @@ -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, }, }, }); diff --git a/docs/website/deployment.md b/docs/website/deployment.md index b280775..eb3d133 100644 --- a/docs/website/deployment.md +++ b/docs/website/deployment.md @@ -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 @@ -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 @@ -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 diff --git a/package.json b/package.json index adb11a6..110da9f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/site/404.html b/src/site/404.html index 709cde7..ccb81af 100644 --- a/src/site/404.html +++ b/src/site/404.html @@ -1,63 +1,93 @@ - +
- -
- Page not found
-- The safest next step is to go back to the clinic site and continue from the core Renvoo - overview. -
-
+ Operationele SaaS voor rustigere agenda's
+
+
+ 404
+Ga terug naar de funnel en kies opnieuw of u eerst product, vertrouwen of direct het validatiegesprek wilt bekijken.
+
+ Operational SaaS for calmer schedule recovery
+
+
+ 404
+Go back into the funnel and choose whether you want to review the product, the trust layer, or the meeting planner next.
+ +
+ Operational SaaS for calmer schedule recovery
+
+
+ For Dutch dental clinics
+Renvoo helps Dutch dental clinics spot no-show risk earlier, handle late cancellations sooner, and recover empty chair time without adding front-desk work.
+ +The real problem
+Loss compounds when empty chair time, late signals, and recovery work all hit the same day. That is why reminders alone are not enough.
+A scheduled appointment only creates value if the patient actually shows up.
+A patient may warn the clinic, and the slot can still be too late to save.
+Confirmations, chasing replies, rescheduling, and backfill all create extra operational load.
+How Renvoo thinks
+The pilot is designed as a lightweight operations layer. First surface risk, then confirm or reschedule more intentionally, then recover capacity faster.
+Start with scheduling and communication data rather than clinical data.
+Surface higher-risk appointments earlier than a generic reminder flow does.
+Give staff a clearer moment to confirm, reschedule, or intervene earlier.
+Help the clinic recover lost capacity while keeping schedule control inside the practice.
+Why this feels credible
+The current case is grounded in clear operating logic and scenario math. The pilot exists to prove real reduction, recovery, and admin relief with clinic workflows.
+Renvoo is deliberately framed around non-clinical scheduling and communication data, not diagnoses, notes, or treatment decisions.
+Choose your route
+See the full Connect → Detect → Confirm → Replace workflow and why it is different from reminder software.
+ Go to Product +Review the validation meeting, pilot framing, and local booking flow.
+ Go to Pilot +Review controller/processor boundaries, FAQs, materials, and legal notes.
+ Go to Trust +Next step
+We are looking for a short validation meeting with a dental clinic owner or practice manager to review the current no-show and cancellation workflow.
+
+ Operational SaaS for calmer schedule recovery
+
+
+ Pre-launch note
+This page shows the short-form patient message note Renvoo plans to use for clinic-triggered reminders, confirmations, and secure rescheduling links during the first pilot phase.
+Your clinic may use Renvoo to help manage appointment confirmations, rescheduling, and recovery of otherwise unused appointment time. Renvoo acts on behalf of the clinic for those operational messages.
+ +Before live pilot use, clinic-specific wording, contact details, and approved data categories should be finalized in the formal legal notice.
+ +
+ Operational SaaS for calmer schedule recovery
+
+
+ Pilot
+The first conversation is not a heavy product demo. It is a short operator review of the current no-show, confirmation, and recovery workflow to see whether a lightweight pilot is worth it.
+ +What the first meeting should do
+Walk through how no-shows, late cancellations, and recovery currently get handled.
+Review where the clinic gets visibility too late and what data or tooling already exists.
+Decide whether the operational pain is strong enough for a measurable pilot.
+Local booking flow
+This planner currently works as a request flow backed by local availability blocks. It stores a clean handoff while live routing is still being connected.
+Book Meeting
+Live routing is not connected yet. The planner therefore stores a clean local handoff for launch prep and demo use.
+Request ready for follow-up
+The planner has created a clean handoff. You can copy or download the summary while live routing is still inactive.
+ +Not ready to book yet?
+Review the trust page first for the data boundary, proof posture, FAQ, materials, and legal notes before returning to the planner.
+
+ Operational SaaS for calmer schedule recovery
+
+
+ Pre-launch note
+This page describes the narrow privacy posture currently intended for Renvoo's own website, clinic lead capture, and pilot outreach flow. The final `Renvoo B.V.` contact details and retention choices still need to be added before launch.
+This note covers Renvoo's own website, clinic lead capture, and business-development communication. It does not replace a clinic's own patient privacy notice.
+ +The intended default launch posture is no non-essential cookies by default. If that changes, an explicit consent flow should be added first.
+ +Before public launch, replace this pre-launch note with the finalized website privacy notice from the legal pack, including legal contact details, retention choices, and any approved vendor list.
+ +
+ Operational SaaS for calmer schedule recovery
+
+
+ Product
+Renvoo is meant to be an operations layer around attendance uncertainty. The point is not more messages. The point is earlier visibility, better intervention timing, and calmer schedule recovery.
+ +Before and after
+Without Renvoo
+With Renvoo
+The four steps
+The architecture is deliberately small for early pilots: lightweight intake, operational visibility, and no patient-platform behavior in v1.
+Start with scheduling, appointment, and communication fields that are operationally useful enough to surface risk.
+Surface higher-risk appointments earlier than a standard reminder program would.
+Give the team a clearer moment to confirm, reschedule, or intervene before chair time is lost.
+When a slot opens up, the workflow helps the clinic think about recovery earlier while keeping actual schedule mutation under clinic or EHR control.
+Low friction by design
+Administrative scheduling and communication data comes first. That keeps the scope narrow and operational.
+The first step does not need to become a heavy integration project before the pilot case is clear.
+Patient interaction stays in clinic-triggered messages and secure links, not in a new consumer product.
+Why dental first
+Next step
+If yes, the best next step is a short meeting where we do not demo first. We review the current no-show, cancellation, and recovery workflow.
+
+ Operational SaaS for calmer schedule recovery
+
+
+ Trust
+This page is for the skeptical route: what Renvoo does and does not claim, what data is and is not in scope, and what clinic materials already exist for real conversations.
+ +Data boundary & role split
+Renvoo is designed around administrative scheduling and communication data, not diagnoses, treatment decisions, or clinical notes.
+For patient and appointment operational data, the clinic stays controller and Renvoo acts as processor under clinic instruction.
+The first pilots stay away from patient accounts, marketplace behavior, and unnecessary consumer-facing product sprawl.
+Proof posture
+Founder
+Renvoo is being built by Mohamed Ibrahim, former Electrical Subteam Lead for Team Polar at TU/e and part of the NVIDIA 6G Developer Program.
+Frequent questions
+That makes sense. Reminders are part of the baseline. The gap Renvoo is focused on is what happens when reminders are not enough: no response, late response, and manual recovery work around lost capacity.
+That is exactly why the current direction is low friction: CSV-first or read-only onboarding, no patient accounts in v1, and a short workflow validation before anything heavier.
+That caution is right. Renvoo is designed around non-clinical administrative scheduling and communication data, not diagnoses, clinical notes, or treatment decisions.
+No. The v1 posture is clinic SaaS, not a patient platform. Patient interaction should happen through clinic-triggered reminders, confirmations, and secure links.
+That is part of the problem Renvoo is trying to solve. The first meeting is not a heavy rollout. It is there to see whether the operational pain is strong enough for a lightweight pilot.
+That is the central pilot question. The current case is logical, but the pilot should validate whether no-show reduction, recovery, and admin relief become visible in your clinic.
+Clinic materials
+The website is not the only artifact. This funnel sits alongside the same one-pager, deck, and operator framing already used in founder-led outreach.
+Compact leave-behind for operators after a short intro or follow-up email.
+ Download the one-pager +Clinic-facing deck for workflow reviews and pilot conversations.
+ Download the clinic deck +Ready-to-forward PDF version of the one-pager.
+ Download the PDF +
+
+
+ Legal base layer
+The legal pages stay deliberately simple: website privacy, patient message note, and pointers into the broader legal pack for controller/processor posture, DPIA-lite, and contract templates.
+Next step
+The funnel is intentionally simple: understand the workflow, validate the trust layer, and only then request the conversation.
+
-
-
-