feat: Mollie betaalflow, inschrijfpagina, SEO en deploy

Betaalflow via Tally formulier (e-mailverzameling) + Mollie Payment Links.
Inschrijfpagina (/inschrijven), bedankt-pagina (/bedankt), OpenGraph tags,
favicon, Umami analytics, base path config en deploy script toegevoegd.
Site live op frankmeeuwsen.com/workshopclaudecode/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Frank Meeuwsen 2026-02-10 15:16:49 +01:00
parent 13eca07719
commit e791e06f1d
16 changed files with 347 additions and 25 deletions

View file

@ -66,7 +66,40 @@
- Workshop details (datum/tijd/locatie) compact weergegeven met iconen
### Volgende sessie
- [ ] Inschrijfformulier/betaallink toevoegen
- [ ] Deploy naar productie
- [ ] SEO meta tags toevoegen (Open Graph, Twitter Cards)
- [ ] Formulier validatie en betalingsflow
- [x] Inschrijfformulier/betaallink toevoegen
- [x] Deploy naar productie
- [x] SEO meta tags toevoegen (Open Graph, Twitter Cards)
- [x] Formulier validatie en betalingsflow
## 2026-02-10 - Sessie 4: Mollie betaalflow, SEO en deploy
### Wat is gebouwd
- Mollie Payment Links aangemaakt (test + live) voor workshop betaling
- Tally formulier geintegreerd voor e-mailverzameling voor inschrijving
- Inschrijfpagina (/inschrijven) met embedded Tally formulier
- Bedankt-pagina (/bedankt) voor na succesvolle betaling
- Payment config bestand (src/config/payment.js) voor centrale URL configuratie
- CTA-knoppen (Pricing, FinalCTA, StickyBar) gelinkt naar inschrijfpagina via React Router Link
- Hero CTA scrollt nog steeds naar pricing sectie
- OpenGraph en Twitter Card meta tags in index.html
- OG-afbeelding (1200x630) gemaakt in Canva met site-kleuren en headline
- SVG favicon (coral CC icoon)
- Umami analytics script toegevoegd (zelfde ID als frankmeeuwsen.com)
- Canonical URL ingesteld
- Base path config (/workshopclaudecode/) alleen voor production builds
- .htaccess voor SPA-routing in submap
- Deploy script (deploy.sh) voor build + upload naar Coolify Docker container
- Site live gedeployd op https://frankmeeuwsen.com/workshopclaudecode/
### Technische beslissingen
- Tally als tussenstap voor e-mailverzameling (geen backend nodig, AVG-compliant, gratis)
- Betaalflow: CTA -> /inschrijven (Tally embed) -> Mollie betaalpagina -> /bedankt
- Vite base path conditioneel: / voor dev, /workshopclaudecode/ voor build
- React Router basename via import.meta.env.BASE_URL (werkt automatisch in beide modes)
- Hero afbeelding pad via import.meta.env.BASE_URL voor correcte submap-verwijzing
- Deploy via rsync + docker cp naar WordPress container op Coolify
### Volgende sessie
- [ ] Testimonials toevoegen zodra beschikbaar
- [ ] Structured data (JSON-LD Event schema) toevoegen
- [ ] Eerste testbetaling via live link verifiëren

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 KiB

26
deploy.sh Executable file
View file

@ -0,0 +1,26 @@
#!/bin/bash
# Deploy script voor Claude Code Workshop sales page
# Bouwt de site en deployt naar de Coolify WordPress container
#
# Gebruik: ./deploy.sh
set -e
SERVER="coolify"
CONTAINER="wordpress-d0wko4gskokosssogcw8040g"
REMOTE_PATH="/var/www/html/workshopclaudecode"
TMP_PATH="/tmp/workshopclaudecode"
echo "1/4 - Building..."
npm run build --silent
echo "2/4 - Uploading naar server..."
rsync -avz --quiet dist/ "$SERVER:$TMP_PATH/"
echo "3/4 - Kopieren naar container..."
ssh "$SERVER" "docker cp $TMP_PATH/. $CONTAINER:$REMOTE_PATH/ && docker exec $CONTAINER chown -R www-data:www-data $REMOTE_PATH/"
echo "4/4 - Opruimen..."
ssh "$SERVER" "rm -rf $TMP_PATH"
echo "Done! https://frankmeeuwsen.com/workshopclaudecode/"

View file

@ -2,11 +2,34 @@
<html lang="nl">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Claude Code Workshop - Van nieuwsgierig naar praktisch aan de slag</title>
<meta name="description" content="Leer Claude Code in 1 ochtend. Van installatie tot werkende applicaties. Kleine groep, hands-on, 6 maart 2026 in Utrecht." />
<!-- Canonical -->
<link rel="canonical" href="https://frankmeeuwsen.com/workshopclaudecode/" />
<!-- OpenGraph -->
<meta property="og:type" content="website" />
<meta property="og:locale" content="nl_NL" />
<meta property="og:title" content="Claude Code Workshop - Maak zelf de tools die je nu inhuurt" />
<meta property="og:description" content="In 1 ochtend van nieuwsgierig naar praktisch aan de slag met Claude Code. Kleine groep, hands-on. 6 maart 2026 in Utrecht." />
<meta property="og:url" content="https://frankmeeuwsen.com/workshopclaudecode/" />
<meta property="og:image" content="https://frankmeeuwsen.com/workshopclaudecode/og-image.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:site_name" content="Frank Meeuwsen" />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Claude Code Workshop - Maak zelf de tools die je nu inhuurt" />
<meta name="twitter:description" content="In 1 ochtend van nieuwsgierig naar praktisch aan de slag met Claude Code. Kleine groep, hands-on. 6 maart 2026 in Utrecht." />
<meta name="twitter:image" content="https://frankmeeuwsen.com/workshopclaudecode/og-image.png" />
<!-- Umami Analytics -->
<script defer src="https://umami.dutchstack.nl/script.js" data-website-id="bceaa80a-f2be-4215-8421-3a78d14601c3"></script>
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

13
public/.htaccess Normal file
View file

@ -0,0 +1,13 @@
# SPA routing: stuur alle requests naar index.html
# zodat React Router de routes /bedankt, /inschrijven etc. kan afhandelen
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /workshopclaudecode/
# Als het bestand of directory bestaat, serveer het direct
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# Alles anders naar index.html
RewriteRule ^ index.html [QSA,L]
</IfModule>

4
public/favicon.svg Normal file
View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="6" fill="#F25C3D"/>
<text x="16" y="22" font-family="system-ui, sans-serif" font-weight="700" font-size="18" fill="white" text-anchor="middle">CC</text>
</svg>

After

Width:  |  Height:  |  Size: 257 B

BIN
public/og-image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 KiB

View file

@ -2,9 +2,12 @@
* FinalCTA.jsx - Afsluitende call-to-action sectie
*
* Coral-500 achtergrond met witte tekst.
* Bevat datum, locatie en primaire CTA button (wit met coral tekst).
* Bevat datum, locatie en CTA button die naar Mollie betaalpagina linkt.
*/
import { Link } from 'react-router-dom';
import { PAYMENT_CONFIG } from '../config/payment';
function FinalCTA() {
return (
<section className="py-16 lg:py-24 bg-coral-500 relative overflow-hidden">
@ -55,16 +58,16 @@ function FinalCTA() {
</div>
</div>
{/* CTA Button - wit met coral tekst */}
<a
href="#inschrijven"
{/* CTA Button - naar inschrijfpagina */}
<Link
to={PAYMENT_CONFIG.SIGNUP_URL}
className="inline-flex items-center gap-2 px-8 py-4 bg-white text-coral-600 font-semibold text-lg rounded-xl shadow-lg hover:bg-coral-50 hover:shadow-xl active:bg-coral-100 transition-all duration-200"
>
Doe mee op 6 maart
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</a>
</Link>
{/* Contact info */}
<p className="mt-8 text-coral-200">

View file

@ -122,7 +122,7 @@ function Hero() {
<div className="hidden lg:block">
<div className="relative">
<img
src="/frank-workshop-claude-code.jpg"
src={`${import.meta.env.BASE_URL}frank-workshop-claude-code.jpg`}
alt="Frank Meeuwsen geeft een workshop over Claude Code"
className="rounded-2xl shadow-lg w-full"
style={{ filter: 'brightness(1.12) contrast(1.05) saturate(0.95)' }}

View file

@ -2,9 +2,12 @@
* Pricing.jsx - "Investering" sectie
*
* Toont de prijs, inclusief lijst en urgency element.
* Bevat primaire CTA button naar inschrijving.
* Bevat primaire CTA button die naar Mollie betaalpagina linkt.
*/
import { Link } from 'react-router-dom';
import { PAYMENT_CONFIG } from '../config/payment';
function Pricing() {
// Wat is inbegrepen
const included = [
@ -113,13 +116,13 @@ function Pricing() {
</div>
</div>
{/* CTA Button */}
<a
href="#inschrijven"
{/* CTA Button - naar inschrijfpagina */}
<Link
to={PAYMENT_CONFIG.SIGNUP_URL}
className="btn-primary w-full text-center block"
>
Doe mee op 6 maart
</a>
</Link>
</div>
</div>
</div>

View file

@ -7,6 +7,8 @@
*/
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { PAYMENT_CONFIG } from '../config/payment';
function StickyBar() {
// State om te bepalen of de bar zichtbaar moet zijn
@ -63,12 +65,12 @@ function StickyBar() {
<span className="font-display font-bold text-xl text-warm-900">
399 <span className="text-sm font-normal text-warm-500">excl. BTW</span>
</span>
<a
href="#inschrijven"
<Link
to={PAYMENT_CONFIG.SIGNUP_URL}
className="px-6 py-2 bg-coral-500 text-white font-semibold rounded-lg shadow-sm hover:bg-coral-600 transition-colors"
>
Inschrijven
</a>
</Link>
</div>
</div>
</div>
@ -89,12 +91,12 @@ function StickyBar() {
</div>
{/* Right: CTA */}
<a
href="#inschrijven"
<Link
to={PAYMENT_CONFIG.SIGNUP_URL}
className="flex-shrink-0 px-6 py-3 bg-coral-500 text-white font-semibold rounded-lg shadow-sm hover:bg-coral-600 transition-colors"
>
Inschrijven
</a>
</Link>
</div>
</div>
</div>

11
src/config/payment.js Normal file
View file

@ -0,0 +1,11 @@
/**
* payment.js - Betaalflow configuratie
*
* CTA-knoppen linken naar de inschrijfpagina met embedded Tally formulier.
* Na het formulier redirect Tally naar de Mollie betaalpagina.
* Test/live Mollie link wordt ingesteld in Tally's redirect URL.
*/
export const PAYMENT_CONFIG = {
SIGNUP_URL: '/inschrijven',
};

View file

@ -5,14 +5,18 @@ import './index.css'
import App from './App.jsx'
import Privacy from './pages/Privacy.jsx'
import Terms from './pages/Terms.jsx'
import ThankYou from './pages/ThankYou.jsx'
import Signup from './pages/Signup.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<BrowserRouter>
<BrowserRouter basename={import.meta.env.BASE_URL}>
<Routes>
<Route path="/" element={<App />} />
<Route path="/privacy" element={<Privacy />} />
<Route path="/voorwaarden" element={<Terms />} />
<Route path="/inschrijven" element={<Signup />} />
<Route path="/bedankt" element={<ThankYou />} />
</Routes>
</BrowserRouter>
</StrictMode>,

90
src/pages/Signup.jsx Normal file
View file

@ -0,0 +1,90 @@
/**
* Signup.jsx - Inschrijfpagina met embedded Tally formulier
*
* Bezoekers komen hier via de CTA-knoppen op de sales page.
* Na het invullen van het formulier redirect Tally naar de Mollie betaalpagina.
*/
import { useEffect } from 'react';
import { Link } from 'react-router-dom';
function Signup() {
// Laad het Tally embed script zodra de pagina mount
useEffect(() => {
const scriptUrl = 'https://tally.so/widgets/embed.js';
// Check of het script al geladen is
if (typeof window.Tally !== 'undefined') {
window.Tally.loadEmbeds();
return;
}
// Script nog niet geladen? Voeg het toe
if (!document.querySelector(`script[src="${scriptUrl}"]`)) {
const script = document.createElement('script');
script.src = scriptUrl;
script.onload = () => {
if (typeof window.Tally !== 'undefined') {
window.Tally.loadEmbeds();
}
};
document.body.appendChild(script);
}
}, []);
return (
<div className="min-h-screen bg-warm-50">
{/* Header */}
<header className="bg-white border-b border-warm-200 py-6">
<div className="container-page">
<Link
to="/"
className="text-coral-500 hover:text-coral-600 transition-colors inline-flex items-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Terug naar workshop
</Link>
</div>
</header>
{/* Content */}
<main className="container-page py-12">
<div className="max-w-2xl mx-auto">
<h1 className="heading-hero mb-4 text-center">Inschrijven</h1>
<p className="text-center text-warm-600 mb-10">
Vul je gegevens in en je wordt doorgestuurd naar de betaalpagina.
</p>
{/* Embedded Tally formulier */}
<div className="card">
<iframe
data-tally-src="https://tally.so/embed/0Q6v8A?alignLeft=1&hideTitle=1&transparentBackground=1&dynamicHeight=1"
loading="lazy"
width="100%"
height="300"
frameBorder="0"
title="Inschrijving Claude Code Workshop"
/>
</div>
{/* Workshop samenvatting */}
<div className="mt-8 text-center text-sm text-warm-500 space-y-1">
<p>Claude Code Workshop | Vrijdag 6 maart 2026 | 9:00 - 14:00 | Utrecht</p>
<p>EUR 399 excl. BTW (EUR 482,79 incl. BTW)</p>
</div>
</div>
</main>
{/* Footer */}
<footer className="bg-warm-900 text-warm-400 py-6">
<div className="container-page text-center text-sm">
&copy; {new Date().getFullYear()} Frank Meeuwsen. Alle rechten voorbehouden.
</div>
</footer>
</div>
);
}
export default Signup;

108
src/pages/ThankYou.jsx Normal file
View file

@ -0,0 +1,108 @@
/**
* ThankYou.jsx - Bedankt-pagina na succesvolle betaling
*
* Bezoekers komen hier terecht na betaling via Mollie.
* Toont bevestiging, workshop details en vervolgstappen.
*/
import { Link } from 'react-router-dom';
function ThankYou() {
return (
<div className="min-h-screen bg-warm-50">
{/* Header */}
<header className="bg-white border-b border-warm-200 py-6">
<div className="container-page">
<Link
to="/"
className="text-coral-500 hover:text-coral-600 transition-colors inline-flex items-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Terug naar homepage
</Link>
</div>
</header>
{/* Content */}
<main className="container-page py-12">
<div className="max-w-2xl mx-auto text-center">
{/* Checkmark icoon */}
<div className="mx-auto w-20 h-20 bg-teal-100 rounded-full flex items-center justify-center mb-8">
<svg className="w-10 h-10 text-teal-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<h1 className="heading-hero mb-4">Je bent erbij!</h1>
<p className="text-xl text-warm-600 mb-10">
Je inschrijving voor de Claude Code Workshop is bevestigd.
</p>
{/* Workshop details */}
<div className="card text-left mb-10">
<h2 className="heading-3 mb-4">Workshop details</h2>
<div className="space-y-3 text-warm-600">
<div className="flex gap-3">
<svg className="w-5 h-5 text-coral-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span>Vrijdag 6 maart 2026</span>
</div>
<div className="flex gap-3">
<svg className="w-5 h-5 text-coral-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>9:00 - 14:00 uur</span>
</div>
<div className="flex gap-3">
<svg className="w-5 h-5 text-coral-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span>Utrecht</span>
</div>
</div>
</div>
{/* Vervolgstappen */}
<div className="card text-left mb-10">
<h2 className="heading-3 mb-4">Wat gebeurt er nu?</h2>
<ol className="space-y-4 text-warm-600">
<li className="flex gap-3">
<span className="flex-shrink-0 w-7 h-7 bg-coral-100 text-coral-600 rounded-full flex items-center justify-center font-semibold text-sm">1</span>
<span>Je ontvangt een bevestigingsmail met je factuur.</span>
</li>
<li className="flex gap-3">
<span className="flex-shrink-0 w-7 h-7 bg-coral-100 text-coral-600 rounded-full flex items-center justify-center font-semibold text-sm">2</span>
<span>Een week voor de workshop ontvang je een mail met praktische informatie en voorbereidingsinstructies.</span>
</li>
<li className="flex gap-3">
<span className="flex-shrink-0 w-7 h-7 bg-coral-100 text-coral-600 rounded-full flex items-center justify-center font-semibold text-sm">3</span>
<span>Op 6 maart neem je je laptop mee en gaan we aan de slag!</span>
</li>
</ol>
</div>
{/* Contact */}
<p className="text-warm-500">
Vragen? Mail naar{' '}
<a href="mailto:frank@frankmeeuwsen.com" className="text-coral-500 hover:text-coral-600">
frank@frankmeeuwsen.com
</a>
</p>
</div>
</main>
{/* Footer */}
<footer className="bg-warm-900 text-warm-400 py-6">
<div className="container-page text-center text-sm">
&copy; {new Date().getFullYear()} Frank Meeuwsen. Alle rechten voorbehouden.
</div>
</footer>
</div>
);
}
export default ThankYou;

View file

@ -2,6 +2,8 @@ import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
// base path alleen voor production build, lokaal blijft het op /
export default defineConfig(({ command }) => ({
plugins: [react()],
})
base: command === 'build' ? '/workshopclaudecode/' : '/',
}))