Files

30 KiB

Design Document: personal-homepage (andreknie.de)

Overview

A personal homepage for Dr. Andre Knie at andreknie.de, built as a modern single-page application with multiple routes. The site serves as a central platform for trust-building, content access, speaking visibility, consulting positioning, and lead generation. It uses the same tech stack and architectural patterns as the Projekt-KIQ-HP reference implementation: Vite 8 + React 19 (JSX), react-router-dom v7, Express 5 backend, CSS custom properties for theming, glass morphism design, and Docker + Caddy deployment.

Content is managed via markdown and YAML files in a content/ directory (file-based CMS), eliminating the need for a database for content storage. The backend handles only form submissions (contact, talk requests, newsletter signup, resource lead capture) via nodemailer.

All colors are defined exclusively through CSS custom properties, making the entire color scheme changeable from a single :root block.

Content Visibility Model

Each content object has a visibility field:

  • primary — actively featured on the homepage, content highlights, and CTAs. These are the core topics (KI, Transformation, Leadership, Digitalisierung).
  • secondary — visible in content archives and when browsing/filtering, but NOT featured on the homepage hero, highlights section, or CTAs. These are adjacent topics (Fehlerkultur, Agilität-Seminare, non-AI workshops) that demonstrate breadth without diluting the core positioning.

Default visibility is primary if omitted. The homepage highlights section and ContentHighlights component only pull primary content. The full archive pages (Kniepunkt, Podcast, Speaking, etc.) show all content regardless of visibility.

Architecture

High-Level Architecture

graph TB
    subgraph "Client (Browser)"
        SPA[React 19 SPA]
        Router[react-router-dom v7]
    end

    subgraph "Build Time"
        Vite[Vite 8 Build]
        ContentLoader[Content Loader Plugin]
        MD[Markdown Files]
        YAML[YAML Metadata Files]
    end

    subgraph "Production Server"
        Caddy[Caddy Reverse Proxy]
        Static[Static Files /opt/andreknie/site]
        Express[Express 5 Backend :3003]
        Nodemailer[Nodemailer]
    end

    subgraph "External"
        SMTP[SMTP Server]
        PodcastHost[Podcast Hosting Platform]
    end

    MD --> ContentLoader
    YAML --> ContentLoader
    ContentLoader --> Vite
    Vite --> Static

    SPA --> Router
    Router --> Caddy
    Caddy -->|/api/*| Express
    Caddy -->|static| Static
    Express --> Nodemailer
    Nodemailer --> SMTP
    SPA -->|audio links| PodcastHost

Content Pipeline

Content flows from markdown/YAML files through a Vite build-time plugin that transforms them into JSON modules importable by React components. This means:

  1. Stakeholder creates/edits .md or .yaml files in content/
  2. A Vite plugin (vite-plugin-content) reads and transforms these at build time
  3. The built SPA includes all content as static JSON data
  4. New content becomes visible after the next npm run build + deploy

Request Flow

sequenceDiagram
    participant V as Visitor
    participant C as Caddy
    participant S as Static Files
    participant E as Express Backend

    V->>C: GET /kniepunkt
    C->>S: Serve index.html (SPA)
    S-->>V: React app loads

    V->>C: POST /api/contact
    C->>E: Proxy to Express
    E->>E: Validate + store
    E->>E: Send confirmation email
    E-->>V: 201 Created

Components and Interfaces

Component Hierarchy

graph TD
    App[App.jsx]
    App --> BG[BackgroundPattern]
    App --> Nav[Navigation]
    App --> Routes[Routes]
    App --> Footer[Footer]
    App --> Dock[BottomDock]

    Routes --> Home[HomePage]
    Routes --> Kniepunkt[KniepunktPage]
    Routes --> Podcast[PodcastPage]
    Routes --> Speaking[SpeakingPage]
    Routes --> Consulting[ConsultingPage]
    Routes --> Resources[ResourcesPage]
    Routes --> Contact[ContactPage]
    Routes --> Impressum[ImpressumPage]
    Routes --> Datenschutz[DatenschutzPage]

    Home --> Hero[Hero]
    Home --> Stats[Stats]
    Home --> ContentHighlights[ContentHighlights]
    Home --> AudienceSection[AudienceSection]
    Home --> CTA[CTASection]

    Kniepunkt --> IssueList[IssueList]
    Kniepunkt --> IssueDetail[IssueDetail]

    Podcast --> EpisodeList[EpisodeList]
    Podcast --> EpisodeCard[EpisodeCard]

    Speaking --> TalkList[TalkList]
    Speaking --> TalkRequestForm[TalkRequestForm]

    Consulting --> ExampleGrid[ExampleGrid]
    Consulting --> ExampleCard[ExampleCard]

    Resources --> ResourceList[ResourceList]
    Resources --> LeadCaptureForm[LeadCaptureForm]

    Contact --> ContactForm[ContactForm]

    Footer --> NewsletterForm[NewsletterForm]
    Footer --> DHiveLink[dHive Cross-Reference]

File/Folder Structure

andreknie.de/
├── content/                        # File-based CMS (stakeholder edits here)
│   ├── posts/                      # LinkedIn posts & articles (.md)
│   │   └── 2025-01-15-ai-act.md
│   ├── kniepunkt/                  # Column issues (.md)
│   │   └── 038-sovereign-ai.md
│   ├── podcast/                    # Episode metadata (.yaml)
│   │   └── ep-012-llm-limits.yaml
│   ├── talks/                      # Talk metadata (.yaml)
│   │   └── ki-fraitag-2025.yaml
│   ├── consulting/                 # Consulting examples (.yaml)
│   │   └── mittelstand-predictive.yaml
│   ├── resources/                  # Resource metadata (.yaml)
│   │   └── ai-readiness-checklist.yaml
│   ├── events/                     # Events (.yaml)
│   │   └── 2025-09-ai-masterclass.yaml
│   └── meta/                       # Site-wide metadata
│       ├── profile.yaml            # Stakeholder profile data
│       ├── stats.yaml              # Stats section numbers
│       └── audiences.yaml          # Target audience descriptions
├── public/
│   ├── images/                     # Static images
│   ├── logos/                      # Logos (personal + dHive)
│   ├── resources/                  # Downloadable files (PDFs etc.)
│   └── patterns/                   # Background pattern SVGs
├── src/
│   ├── components/                 # Shared UI components
│   │   ├── BackgroundPattern.jsx + .css
│   │   ├── BottomDock.jsx + .css
│   │   ├── Navigation.jsx + .css
│   │   ├── Footer.jsx + .css
│   │   ├── Hero.jsx + .css
│   │   ├── Stats.jsx + .css
│   │   ├── ContentHighlights.jsx + .css
│   │   ├── AudienceSection.jsx + .css
│   │   ├── CTASection.jsx + .css
│   │   ├── NewsletterForm.jsx + .css
│   │   ├── ContactForm.jsx + .css
│   │   ├── TalkRequestForm.jsx + .css
│   │   ├── LeadCaptureForm.jsx + .css
│   │   ├── ContentFilter.jsx + .css
│   │   ├── Pagination.jsx + .css
│   │   ├── GlassCard.jsx + .css
│   │   ├── BentoGrid.jsx + .css
│   │   ├── ScrollToTop.jsx
│   │   └── SEOHead.jsx
│   ├── pages/
│   │   ├── Home.jsx
│   │   ├── Kniepunkt.jsx + .css
│   │   ├── KniepunktIssue.jsx + .css
│   │   ├── Podcast.jsx + .css
│   │   ├── Speaking.jsx + .css
│   │   ├── Consulting.jsx + .css
│   │   ├── Resources.jsx + .css
│   │   ├── Contact.jsx + .css
│   │   ├── Impressum.jsx
│   │   └── Datenschutz.jsx
│   ├── data/                       # Build-time generated JSON (via Vite plugin)
│   │   └── (auto-generated)
│   ├── hooks/                      # Custom React hooks
│   │   ├── useContent.js          # Content loading/filtering
│   │   └── useFormSubmit.js       # Form submission with validation
│   ├── utils/
│   │   ├── contentLoader.js       # Content transformation utilities
│   │   ├── validation.js          # Form validation logic
│   │   └── formatDate.js          # Date formatting (German locale)
│   ├── App.jsx
│   ├── index.css                  # Global styles + CSS variables
│   └── main.jsx
├── server/
│   ├── index.js                   # Express 5 entry point
│   ├── routes/
│   │   ├── contact.js             # Contact form handler
│   │   ├── talk-request.js        # Talk request handler
│   │   ├── newsletter.js          # Newsletter signup handler
│   │   └── resource-download.js   # Lead capture handler
│   ├── middleware/
│   │   ├── rateLimiter.js         # Rate limiting
│   │   └── validator.js           # Input validation
│   ├── services/
│   │   ├── mailer.js              # Nodemailer configuration
│   │   └── confirmationToken.js   # Token generation/verification
│   └── data/                      # Runtime data (leads, pending confirmations)
│       └── .gitkeep
├── plugins/
│   └── vite-plugin-content.js     # Vite plugin for content transformation
├── Dockerfile
├── docker-compose.yml
├── Caddyfile
├── package.json
├── vite.config.js
└── .env.example

Shared Components

Component Purpose Pattern from Blueprint
BackgroundPattern Animated drifting SVG background Same mirrored-quadrant technique
BottomDock Fixed bottom navigation pill with rotating light border Same dock pattern, adapted nav items
Navigation Top bar with logo + hamburger overlay menu Same overlay pattern
Footer CTA section + newsletter form + legal links + dHive reference Extended from blueprint
GlassCard Reusable glass-morphism card Extracted from blueprint's inline usage
BentoGrid Responsive grid layout for content cards Same bento pattern
ContentFilter Tag-based filtering UI New component
Pagination Page navigation for lists (10 items/page) New component

Page Composition

Page URL Key Components
Home / Hero, Stats, ContentHighlights, AudienceSection, CTASection
Kniepunkt /kniepunkt IssueList with ContentFilter
Kniepunkt Issue /kniepunkt/:slug Full issue content (rendered markdown)
Podcast /podcast EpisodeList with Pagination
Speaking /speaking TalkList + TalkRequestForm
Consulting /consulting ExampleGrid (BentoGrid of ExampleCards)
Resources /resources ResourceList + LeadCaptureForm (modal)
Contact /kontakt ContactForm
Impressum /impressum Static legal content
Datenschutz /datenschutz Static legal content

Data Models

Content File Schemas

Post (Markdown with Frontmatter)

# content/posts/2025-01-15-ai-act.md frontmatter
---
title: "Der AI Act ist da — was bedeutet das für den Mittelstand?"
date: 2025-01-15
tags: [ai-act, regulation, mittelstand]
summary: "Eine nüchterne Einordnung des EU AI Act für deutsche Unternehmen."
source: linkedin  # or "original"
visibility: primary  # primary = featured on homepage/highlights | secondary = visible in archive only
---
# Markdown body follows...

Kniepunkt Issue (Markdown with Frontmatter)

# content/kniepunkt/038-sovereign-ai.md frontmatter
---
title: "Souveräne KI — mehr als ein Buzzword"
issue: 38
date: 2025-06-01
tags: [sovereignty, infrastructure, germany]
summary: "Warum digitale Souveränität bei KI kein Marketing-Claim sein darf."
---
# Markdown body follows...

Podcast Episode (YAML)

# content/podcast/ep-012-llm-limits.yaml
id: ep-012-llm-limits
title: "Die Grenzen großer Sprachmodelle"
date: 2025-05-15
summary: "Was LLMs können, was nicht — und warum das wichtig ist."
duration: "42:15"
tags: [llm, limits, practical-ai]
audio_url: "https://podcast-host.example/episodes/012.mp3"
external_links:
  spotify: "https://open.spotify.com/episode/..."
  apple: "https://podcasts.apple.com/..."

Talk (YAML)

# content/talks/ki-fraitag-2025.yaml
id: ki-fraitag-2025
title: "KI im Mittelstand — Chancen ohne Hype"
description: "Keynote über praktische KI-Anwendungen für mittelständische Unternehmen."
format: keynote  # keynote | workshop | panel
target_audience: [geschaeftsfuehrung, it-leitung]
tags: [mittelstand, practical-ai, sovereignty]
visibility: primary  # primary = actively promoted on homepage | secondary = visible in archive but not featured
events:
  - name: "KI-FreiTag Nordhessen"
    date: 2025-03-14
    location: "Science Park Kassel"
  - name: "RKW Hessen Workshop"
    date: 2025-01-20
    location: "Frankfurt am Main"

Consulting Example (YAML)

# content/consulting/mittelstand-predictive.yaml
id: mittelstand-predictive
title: "Predictive Maintenance für Produktionsunternehmen"
industry: manufacturing
organization_size: "200-500"  # employees range
problem_domain: "Ungeplante Maschinenausfälle verursachten hohe Kosten."
approach: "Aufbau eines ML-Modells auf Basis vorhandener Sensordaten, souverän gehostet."
outcomes: "40% weniger ungeplante Stillstände innerhalb von 6 Monaten."
tags: [predictive-maintenance, manufacturing, ml]

Resource (YAML)

# content/resources/ai-readiness-checklist.yaml
id: ai-readiness-checklist
title: "AI Readiness Checklist für den Mittelstand"
description: "Praktische Checkliste zur Bewertung der KI-Bereitschaft."
file: "ai-readiness-checklist.pdf"  # relative to public/resources/
requires_lead: true
tags: [checklist, readiness, mittelstand]

Event (YAML)

# content/events/2025-09-ai-masterclass.yaml
id: 2025-09-ai-masterclass
title: "AI Masterclass — Science Park Kassel"
date: 2025-09-18
location: "Science Park Kassel"
event_type: workshop  # talk | workshop | conference | appearance
description: "Ganztägiger Workshop zu praktischer KI-Implementierung."
tags: [workshop, kassel, hands-on]

Profile Metadata (YAML)

# content/meta/profile.yaml
name: "Dr. Andre Knie"
roles:
  - "AI Expert"
  - "Consultant"
  - "Speaker"
  - "Content Author"
  - "Managing Director, dHive"
summary: "Nüchterne KI-Expertise für den deutschen Mittelstand. Souverän, praktisch, evidenzbasiert."
photo: "/images/andre-knie.png"
positioning:
  sovereignty: "Unabhängig von US-Hyperscalern — KI-Lösungen aus und für Deutschland."
  data_sovereignty: "100% DSGVO- und AI-Act-konform, EU-Hosting."
  practical: "Fokus auf anwendbare KI, nicht auf Hype."
dhive:
  role: "Founder & Managing Director"
  url: "https://d-hive.de"
  description: "Data Hive Cassel — souveräne KI-Infrastruktur für den Mittelstand."
audiences:
  - id: mittelstand
    label: "Mittelständische Unternehmen"
  - id: public-admin
    label: "Öffentliche Verwaltung"
  - id: social-institutions
    label: "Soziale Einrichtungen"
  - id: universities
    label: "Universitäten & Hochschulen"
  - id: hr-od
    label: "Personal- & Organisationsentwicklung"
  - id: it-leadership
    label: "IT-Leitung & CTO"
  - id: executives
    label: "Geschäftsführung"
  - id: event-organizers
    label: "Veranstalter & Speaker-Booking"

Stats (YAML)

# content/meta/stats.yaml
items:
  - value: "50+"
    label: "Projekte"
  - value: "13+"
    label: "Jahre KI"
  - value: "60+"
    label: "Publikationen"
  - value: "100%"
    label: "DSGVO & AI-Act"

API Data Models

Contact Request (POST /api/contact)

{
  "name": "string (required, max 100)",
  "email": "string (required, valid email)",
  "message": "string (required, max 2000)"
}

Talk Request (POST /api/talk-request)

{
  "name": "string (required, max 100)",
  "email": "string (required, valid email)",
  "event_name": "string (required, max 200)",
  "topic": "string (required, max 200)",
  "message": "string (required, max 2000)"
}

Newsletter Signup (POST /api/newsletter)

{
  "email": "string (required, valid email)"
}

Resource Lead Capture (POST /api/resource-download)

{
  "name": "string (required, max 100)",
  "email": "string (required, valid email)",
  "company": "string (required, max 150)",
  "resource_id": "string (required)"
}

API Endpoints

Method Path Purpose Auth
POST /api/contact Submit contact form, triggers confirmation email Public
POST /api/contact/confirm/:token Confirm contact request, forwards to stakeholder Public
POST /api/talk-request Submit talk request, triggers confirmation email Public
POST /api/talk-request/confirm/:token Confirm talk request, forwards to stakeholder Public
POST /api/newsletter Newsletter signup, triggers double opt-in email Public
GET /api/newsletter/confirm/:token Confirm newsletter subscription Public
POST /api/resource-download Submit lead data, returns download URL Public
GET /api/health Health check Public

Confirmation Token Flow

sequenceDiagram
    participant V as Visitor
    participant API as Express API
    participant DB as JSON File Store
    participant Mail as Nodemailer

    V->>API: POST /api/contact {name, email, message}
    API->>API: Validate input
    API->>DB: Store pending request + token (expires 24h)
    API->>Mail: Send confirmation email with link
    Mail-->>V: Email with confirmation link
    V->>API: POST /api/contact/confirm/:token
    API->>DB: Verify token, mark confirmed
    API->>Mail: Forward message to stakeholder
    API-->>V: 200 OK (success page)

Theming System

All visual theming is controlled via CSS custom properties in :root. Changing the accent color updates the entire site.

:root {
  /* Base colors — dark theme */
  --bg-color: #0a0a0a;
  --bg-elevated: #141414;
  --text-primary: #f0f0f0;
  --text-secondary: #999999;

  /* Accent — CHANGE THESE to retheme the entire site */
  --accent-color: #4a9eff;           /* Placeholder: neutral blue */
  --accent-color-rgb: 74, 158, 255;  /* Same as above in RGB for rgba() usage */
  --accent-gradient: linear-gradient(135deg, #4a9eff, #2563eb);
  --accent-hover: #2563eb;

  /* Glass morphism */
  --glass-bg: rgba(25, 25, 25, 0.6);
  --glass-border: rgba(255, 255, 255, 0.08);
  --glass-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);

  /* Spacing */
  --section-padding: 120px;
  --container-max: 1200px;

  /* Typography */
  --font-family: 'Inter', sans-serif;
  --font-weight-normal: 400;
  --font-weight-semibold: 600;
  --font-weight-bold: 800;

  /* Transitions */
  --transition-smooth: 0.4s cubic-bezier(0.16, 1, 0.3, 1);
  --transition-fast: 0.3s ease;

  /* Border radius */
  --radius-sm: 8px;
  --radius-md: 16px;
  --radius-lg: 24px;
  --radius-pill: 50px;
}

Design rationale: Using Inter (from Google Fonts) instead of Outfit from the blueprint, as Inter is the stakeholder's established corporate font. The placeholder accent is a neutral blue — the stakeholder will decide final colors later. All accent references use the CSS variables, so a single edit to :root changes everything.

Visual Effects (from Blueprint)

Effect Implementation CSS Pattern
Glass panels backdrop-filter: blur(10px) + semi-transparent bg + subtle border .glass-panel class
Gradient text background-clip: text with accent gradient .text-gradient class
Animated background Fixed SVG pattern with CSS @keyframes drift animation BackgroundPattern component
Hover glow Radial gradient overlay on hover .bento-glow pattern
Card lift transform: translateY(-8px) on hover Applied to all interactive cards
Dock light conic-gradient rotating border via ::before pseudo-element BottomDock component
Fade-up entrance @keyframes fadeUp with opacity + translateY Hero and section entrances
Smooth scrolling scroll-behavior: smooth on html Global CSS

Deployment Architecture

graph LR
    subgraph "VPS / Server"
        subgraph "Docker"
            Backend[Express Container :3003]
        end
        Caddy[Caddy Server]
        StaticFiles[/opt/andreknie/site/]
    end

    DNS[andreknie.de DNS] --> Caddy
    Caddy -->|HTTPS auto-cert| Caddy
    Caddy -->|/api/*| Backend
    Caddy -->|static| StaticFiles

Caddyfile for andreknie.de:

  • Auto HTTPS via Let's Encrypt
  • Reverse proxy /api/* to Express backend on port 3003
  • Serve static SPA files with try_files {path} /index.html
  • Security headers (HSTS, X-Content-Type-Options, CSP, etc.)
  • Gzip/zstd compression
  • Immutable cache for /assets/*

Docker Compose:

  • Single backend service running Express
  • Volume mount for server/data/ (persistent lead/confirmation data)
  • Environment variables via .env file

Build & Deploy workflow:

  1. npm run build → produces dist/ with static SPA
  2. Copy dist/ to /opt/andreknie/site/ on server
  3. docker compose up -d for backend
  4. Caddy serves both static files and proxies API

Content Management Workflow

graph TD
    A[Stakeholder creates .md or .yaml file] --> B[Commits to git repository]
    B --> C[CI/CD triggers build]
    C --> D[Vite plugin transforms content to JSON]
    D --> E[Static site built to dist/]
    E --> F[Deploy to server]
    F --> G[Content visible on andreknie.de]

How the stakeholder adds content:

  1. Create a new file in the appropriate content/ subdirectory
  2. Follow the schema (frontmatter for markdown, full YAML for metadata-only content)
  3. Commit and push (or manually trigger deploy)
  4. The Vite build picks up the new file automatically
  5. No code changes required for new content

Content validation: The Vite plugin validates content files at build time against expected schemas. Missing required fields cause build warnings (not failures, to avoid blocking deploys for minor issues).

Correctness Properties

A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.

Property 1: Content rendering completeness

For any content object of a given type (Post, Talk, Consulting_Example, Newsletter_Issue, Podcast_Episode, Resource, Event), rendering that object must produce output containing all required fields defined for that type (e.g., a Post must show title, date, and body; a Talk must show title, description, and at least one past event; a Consulting_Example must show title, industry, org-size, problem, approach, and outcomes).

Validates: Requirements 3.1, 3.2, 3.5, 4.1, 5.2, 6.1, 6.3, 7.2, 8.2, 8.4

Property 2: Content list sorting

For any list of content objects displayed within a content area (posts, kniepunkt issues, podcast episodes, or any other content type), the items must be ordered by publication date in descending order (newest first).

Validates: Requirements 3.3, 7.3, 8.2, 14.5

Property 3: Form validation rejects invalid input

For any form submission (contact, talk request, newsletter, resource download) where one or more required fields are empty or the email field does not conform to a valid email format, the system must reject the submission, display error indications for each invalid field, and retain all previously entered valid data.

Validates: Requirements 4.4, 10.5, 11.5, 12.5

Property 4: Confirmation token expiration

For any confirmation token (contact: 24h, talk request: 48h, newsletter: 48h) that has exceeded its expiration period, attempting to confirm that token must be rejected and the associated pending record must be discarded without forwarding to the stakeholder or activating the subscription.

Validates: Requirements 10.4, 11.4, 12.4

Property 5: Valid submission triggers confirmation email

For any valid form submission (contact, talk request, or newsletter signup) with all required fields present and a correctly formatted email address, the system must generate a unique confirmation token, store the pending request, and send exactly one confirmation email to the provided address.

Validates: Requirements 10.3, 11.2, 12.2

Property 6: Valid token confirmation forwards to stakeholder

For any confirmation token that is valid and unexpired, confirming that token must result in the associated message/request being forwarded to the stakeholder via email notification, and the pending record being marked as confirmed.

Validates: Requirements 11.3, 12.3

Property 7: Content filtering returns only matching items

For any tag-based filter criterion and any set of content objects, applying the filter must return exactly those content objects whose metadata tags include the selected criterion — no more, no less.

Validates: Requirements 13.1, 13.2

Property 8: Dynamic filter options derived from content

For any set of content objects with tags, the available filter options presented to the visitor must equal the complete set of distinct tags present across all content objects (up to the 20-tag maximum).

Validates: Requirements 13.4, 13.5

Property 9: Event chronological grouping

For any set of events containing both upcoming (date ≥ today) and past (date < today) entries, the display must show all upcoming events before all past events, with each group internally sorted by date descending, and events grouped by year.

Validates: Requirements 9.2

Property 10: Content file validation

For any content file, if it is missing a required attribute (title, date, content-area for general content; title, date, location, event_type for events) or specifies an invalid content-area value, the validation system must reject it with an error message indicating the specific missing or invalid fields.

Validates: Requirements 9.4, 14.1, 14.4

Property 11: Lead data storage integrity

For any successful resource download lead capture (valid name, email, company, resource_id), the system must store a record containing all submitted fields plus the resource identifier and a timestamp, and that record must be retrievable.

Validates: Requirements 4.5

Property 12: Pagination constraint

For any list of podcast episodes (or other paginated content) with more than 10 items, each page must contain at most 10 items, and pagination controls must be present to access all items.

Validates: Requirements 8.3

Property 13: Duplicate subscription detection

For any email address that is already associated with an active newsletter subscription, submitting that email again must inform the visitor that the email is already subscribed and must not create a duplicate entry.

Validates: Requirements 10.6

Error Handling

Client-Side Errors

Scenario Handling
Form validation failure Inline error messages next to invalid fields; valid data retained
Content not found (404 route) Friendly error page with navigation back to home
Network error on form submit Toast notification with retry option
Content loading failure Graceful fallback message per section

Server-Side Errors

Scenario Handling
Invalid request body 400 response with field-level error details
Rate limit exceeded 429 response with retry-after header
SMTP failure Log error, return 500 with user-friendly message; store submission for retry
Invalid confirmation token 404 response, display "link expired or invalid" page
Expired confirmation token 410 Gone response, display "link expired" message
Server crash Express error middleware catches unhandled errors, returns 500

Rate Limiting

  • Contact form: 3 submissions per IP per hour
  • Talk request: 3 submissions per IP per hour
  • Newsletter signup: 5 submissions per IP per hour
  • Resource download: 10 submissions per IP per hour

Data Persistence

  • Pending confirmations stored in JSON files in server/data/
  • Confirmed leads stored in server/data/leads.json
  • Expired tokens cleaned up by a scheduled interval (every hour)
  • All data files are volume-mounted in Docker for persistence across container restarts

Testing Strategy

Property-Based Testing

This feature is suitable for property-based testing in the following areas:

  • Content transformation logic (Vite plugin parsing, validation, sorting)
  • Form validation logic (email validation, field presence, length constraints)
  • Token management (generation, expiration, confirmation)
  • Content filtering (tag matching, dynamic filter derivation)
  • Pagination logic (page size constraints, offset calculation)

Library: fast-check (JavaScript property-based testing library)

Configuration:

  • Minimum 100 iterations per property test
  • Each test tagged with: Feature: personal-homepage, Property {number}: {property_text}

Unit Tests (Example-Based)

Area What to Test
Component rendering Hero shows name, photo, summary; Audience section shows all segments
Form UI All required fields present; submit button disabled when invalid
Empty states Sections hidden when no content; appropriate messages shown
dHive link Correct href, target="_blank", role text present
Routing All pages accessible, 404 for unknown routes

Integration Tests

Area What to Test
Content pipeline Add content file → build → verify in output
Form submission flow Submit → confirmation email → confirm → stakeholder notification
Lead capture flow Submit lead data → receive download URL
Build validation Invalid content files produce warnings

End-to-End Tests

  • Full contact form flow (submit → email → confirm → delivered)
  • Full talk request flow
  • Newsletter double opt-in flow
  • Resource download with lead capture
  • Content browsing and filtering

Test Tools

  • fast-check for property-based tests
  • Vitest for unit and integration tests
  • React Testing Library for component tests
  • Playwright for E2E tests (optional, for critical flows)