Documentation
GitHub

Overview

v1.1 · April 2026 · Active Development

Project Overview

The Lab for Cybernetics website is the digital hub for the lab — a full-stack web app built on Next.js, with Notion acting as both the CMS and database.

Purpose

The website serves three primary functions:

  • Information Hub — Houses course information, lab prize details, and the team directory.
  • Practitioner-Scholar Matching Platform — Connects cybernetics practitioners with students and scholars for mentorship and collaboration.
  • Knowledge Repository — Provides access to the Cybernetics of Cybernetics knowledge graph and resources.

Why This Exists

The previous system relied entirely on Notion, which created several problems:

  • Users with view-only access couldn't search or filter entries.
  • Domain names were inconsistent — e.g., "HCI" vs "Human-Computer Interaction".
  • No intelligent matching between practitioners and scholars.
  • Difficult for users to edit their submissions after creation.

This website solves these problems while keeping Notion as the backend, so lab administrators can continue managing content in a familiar interface.

Key principle: All content is managed in Notion. No code deployment is needed for content updates.

Document Info

Version 1.0
Author Narayan
Date April 2026
Status Active Development
Production URL cybernetics.cmu.edu

Architecture

Tech Stack

Technology choices and the reasoning behind each key architectural decision.

Technology Stack

Component Technology
Framework Next.js 14+ (App Router)
Language TypeScript
Styling Tailwind CSS v4
CMS & Database Notion (via Notion JavaScript SDK)
Email Service Gmail SMTP via nodemailer
Hosting Vercel (free tier)

Why These Choices

TypeScript over JavaScript

Future RAs may use AI coding assistants (Cursor, GitHub Copilot, Antigravity). Type definitions provide better context for AI code generation and reduce bugs by catching errors at compile time.

Notion as Backend

  • No need to learn new database management tools.
  • Content updates don't require code changes or redeployments.
  • Non-technical team members can manage data directly.
  • Backup and version history built-in.
  • Lab already has an active Notion Plus subscription.

Server Actions over API Routes

  • Simpler code — no need to create separate API endpoints.
  • Better performance with fewer network round-trips.
  • Type safety between client and server.
  • Modern Next.js best practice.

Request Flows

Content Rendering
User Browser
Next.js Server
Notion API
Render HTML
Browser
Form Submission
User Form
Next.js Server Action
Notion API
Entry Created
Edit Verification
Click Edit
Generate Code
Gmail SMTP
User Email
Enter Code
Verify
Edit Update Notion

Architecture

Project Structure

Directory layout and the purpose of each top-level folder.

Directory Layout

app/ — Next.js App Router pages and layouts
matching/ — Complete matching system (form, browse, suggestions)
news/ — News listing and article pages
people/ — Team directory
projects/ — Projects listing and detail pages
components/ — Reusable React components
block-renderer.tsx — Notion block HTML translation layer
EditModal.tsx — Email verification and edit form
lib/ — Utility functions and configurations
notion.ts — All Notion API calls and TypeScript interfaces
scripts/ — One-time maintenance scripts
clean-tags.js — Retroactively repairs Notion keyword tag data
public/ — Static assets (images, fonts)

Key Files to Understand

File Why It Matters
lib/notion.ts All Notion API interactions live here. Start here to understand the data model.
app/matching/ Most complex part of the codebase — form, browse, and match algorithm.
components/EditModal.tsx Handles the 6-digit email verification and edit form flow.
components/block-renderer.tsx Translates Notion block JSON into styled React/HTML elements.

Getting Started

Setup Guide

Get the L4C website running locally from scratch in five steps.

Prerequisites

  • Node.js 18+ installed
  • Git installed
  • Notion account with access to the Lab4C workspace
  • Text editor (VS Code recommended)
  • Gmail account with 2-Step Verification enabled

Initial Setup

  1. 1
    Clone the repository: git clone [repo-url]
  2. 2
    Install dependencies: npm install
  3. 3
    Create .env.local — see Environment Variables below.
  4. 4
    Run development server: npm run dev
  5. 5

Environment Variables

.env.localCopy
# Notion Integration
NOTION_TOKEN=secret_xxxxxxxxxxxxxxxx
NOTION_MATCHING_DB_ID=xxxxxxxxxxxxxxxx
NOTION_HOME_PAGE_ID=xxxxxxxxxxxxxxxx
NOTION_PROJECTS_DB_ID=xxxxxxxxxxxxxxxx
NOTION_NEWS_DB_ID=xxxxxxxxxxxxxxxx
NOTION_PEOPLE_DB_ID=xxxxxxxxxxxxxxxx

# Gmail SMTP (for verification emails)
GMAIL_USER=your@gmail.com
GMAIL_APP_PASSWORD=xxxx xxxx xxxx xxxx
Variable Description
NOTION_TOKEN Integration token from notion.so/my-integrations
NOTION_MATCHING_DB_ID Database ID for matching entries
NOTION_HOME_PAGE_ID Page ID for homepage content
NOTION_PROJECTS_DB_ID Database ID for projects
NOTION_NEWS_DB_ID Database ID for news posts
NOTION_PEOPLE_DB_ID Database ID for people directory
GMAIL_USER Email address for sending verification codes
GMAIL_APP_PASSWORD Gmail app-specific password (16 characters)
Never commit .env.local to version control. It's already in .gitignore — keep it that way.

Features

Matching System

The most complex feature. Allows practitioners and scholars to submit profiles, browse entries, receive match suggestions, and edit submissions via email verification.

Overview

The matching system has four capabilities:

  1. Submit information via a custom form
  2. Browse and search all entries
  3. Receive match suggestions based on shared interests
  4. Edit submissions after creation (via email verification)

Domain Autocomplete

Prevents data fragmentation by suggesting existing domains as users type. Instead of separate entries for "HCI", "Human-Computer Interaction", and "human computer interaction", users see existing options and select the standardized version.

Browse Interface

  • Responsive grid layout of all entries.
  • Real-time search by name or domain.
  • Filtering by user type, keywords, and collaboration mode.
  • All filtering happens client-side for fast response.
  • Domain text limited to 15 lines (clamped). Internal fields like Survey Feedback are hidden from public view.

Match Suggestions

A simple point-based algorithm compares profiles:

  • +3 points per shared domain (fuzzy string match)
  • +1 point per shared keyword
  • +2 points if collaboration mode is compatible

Returns the top 5 matches sorted by score. Prioritizes cross-role matching (Scholars Practitioners first) by default.

Edit Functionality & Email Verification

Step Action System Response
1 User clicks "Edit" on an entry Show modal: "Is this your entry?"
2 Confirm Generate 6-digit code, send to entry's email via Gmail SMTP
3 Check email Receive code — expires after 15 minutes
4 Enter 6-digit code Verify code matches and is not expired
5 Code validates Show edit form pre-filled with current data
6 Make changes and save Update Notion entry, clear verification code

Notion Database Schema

Property Type Notes
Name Title Required
Email Email Required. Used for verification codes.
Website URL Optional
User Type Select Scholar / Practitioner
Domain Rich Text Core research area. Autocompleted from existing entries.
Keywords Multi-select Auto-cleaned on submit (hashtags stripped, case normalized)
Why Important Rich Text Emotional investment
Committed To Rich Text What they want to improve
What to Conserve Rich Text What they want to preserve
Organization Rich Text University or company
About Rich Text General self-description
Collaboration Style Rich Text What makes good collaboration
Practitioner Status Select Scholar-only field
Time Commitment Select Availability
Verification Code Rich Text System use only — never displayed publicly

Tag Maintenance

The frontend auto-sanitizes tags on submission. For manual cleanup of historical data in Notion:

BashCopy
node scripts/clean-tags.js

This script removes leading # symbols, splits multi-tags like #AIgovernance #AIsafety, and updates Notion automatically.

Features

Website Sections

The seven main sections of the site and how each is powered by Notion.

Section Type Description
Home / About Static Page Lab mission, overview, contact information
Lab Prize Static Page #NewMacy Cybernetics Prize — info and past winners
Course Static Page Syllabus, enrollment info, and course materials
Projects Database Re-Braiding Project, Cybernetics of Cybernetics vault, Resources
Matching Custom Practitioner-scholar connection platform — most complex section
News Database Lab updates, announcements, blog posts
People Database Lab members, affiliates, contact information

Static Pages

Home, Lab Prize, and Course pull from a single Notion page and are rendered via the Block Renderer. No database queries — just page content blocks.

Dynamic Sections

Projects, News, and People are Notion Databases. Each entry renders as a card on the frontend, with filtering and sorting handled server-side.

Projects

  • Re-Braiding Project — AI-Cybernetics symposium series info
  • Cybernetics of Cybernetics — Link to the existing Obsidian vault knowledge graph
  • Resources — Papers, definitions, reading lists

Notion

Notion Integration

How the website reads from and writes to Notion, and how Notion property types map to form inputs.

How It Works

The website acts as a read-only viewer for most content — static pages, news, people, projects. The exception is the Matching system, where it both reads and writes entries.

Read (Static & Dynamic Pages)
Notion Page / DB
Notion API
lib/notion.ts
Block Renderer
Rendered HTML
Write (Matching System)
User Form
Server Action
lib/notion.ts
Notion API
DB Entry Created / Updated

Property Type Mapping

Notion has specific JSON formats for each property type. This table maps HTML form inputs to the correct Notion API format:

Property Type Form Input Notion API Format
Title <input type="text"> title: [{text: {content: value}}]
Rich Text <textarea> rich_text: [{text: {content: value}}]
Email <input type="email"> email: value
URL <input type="url"> url: value
Select <select> (single) select: {name: value}
Multi-select Checkboxes / tag input multi_select: [{name: tag1}, ...]
Number <input type="number"> number: value
Date <input type="date"> date: {start: isoString}
All Notion API calls are centralized in lib/notion.ts. When adding new properties, add the mapping there first.

Notion

CMS Structure

How to organize the Notion workspace to power the website. Create one root page, share it with the integration, and put everything inside.

Workspace Setup

Create a single top-level page called "L4C Website CMS" and place all sub-pages and databases inside it. Share this root page with your Internal Integration — this grants access to everything at once.

Static Pages

Page Name Key Content
Home Content Mission Statement, Intro Paragraphs, optional Hero Image (page cover)
Lab Prize Prize description, eligibility, previous winners
Cybernetics Course Syllabus, schedule, links to materials
Site-wide footer from Notion: At the bottom of the Home Content page, add a Heading 4 block with the text footer (lowercase). Any paragraphs you place directly below it become the global footer on every page of the site. The heading itself is not displayed. If the section is missing, the site falls back to the hardcoded default footer.
Property Type Notes
Name Title Required
Slug Text URL-friendly ID, e.g. re-braiding
Type Select Symposium / Knowledge Graph / Resource
Description Text Short summary for card view
Status Select Active / Archived
Cover Image Files & Media Shown in grid display

People Database

Property Type Notes
Name Title
Role Select Director / Researcher / Fellow / Alumni
Bio Text Short public bio
Website URL
Email Email Protected by verification modal
Headshot Files Or use the Page Cover / Icon

News Database

Property Type Notes
Title Title
Date Date Used for sorting
Type Select Announcement / Event / Press
Summary Text Short preview shown in the card

Full article text goes inside the database page body and is rendered via the Block Renderer.

Environment Variable IDs

After creating each database or page in Notion, copy its ID into your .env.local:

.env.localCopy
NOTION_HOME_PAGE_ID=your-id-here
NOTION_PRIZE_PAGE_ID=your-id-here
NOTION_COURSE_PAGE_ID=your-id-here
NOTION_PROJECTS_DB_ID=your-id-here
NOTION_PEOPLE_DB_ID=your-id-here
NOTION_NEWS_DB_ID=your-id-here
NOTION_MATCHING_DB_ID=already-set

Notion

Block Renderer

The BlockRenderer component translates Notion's raw block JSON into styled React elements. Located at components/block-renderer.tsx.

How It Works

When the site fetches a page from Notion, it receives an array of blocks — paragraphs, headings, dividers, etc. The component receives a single block prop and switches on its type:

TypeScriptCopy
switch (block.type) {
  case 'heading_1':
  case 'heading_2':
  case 'heading_3':
    // Uses font-special-condensed + --text-sys-subheading token
    return <HeadingBlock block={block} />;

  case 'paragraph':
    // Parses rich text array: annotations + links
    return <ParagraphBlock block={block} />;

  case 'divider':
    return <hr className="my-[var(--sys-subheading-gap)]" />;
}

Supported Block Types

Notion Type Output Notes
heading_1 Styled <h1> Uses font-special-condensed, uppercase, --text-sys-heading (36px)
heading_2 Styled <h2> Uses font-special-condensed, uppercase, --text-sys-subheading (36px — identical to heading by design)
heading_3 Blue action block Special-cased — not a standard heading. See Blue Buttons below.
heading_4 Styled <h4> Sans-serif bold, text-brand-dark, 18px, mt-6 mb-3. API-only — not creatable in Notion UI.
bulleted_list_item <li> with disc bullet 16px, ml-6 indent. Supports bold, italic, underline, links.
numbered_list_item <li> with decimal number Identical to bulleted — 16px, ml-6 indent, full annotation support.
paragraph Rich text <p> Handles bold, italic, underline annotations + embedded links
divider <hr> Margin via --sys-subheading-gap (24px)

Heading Number Visual Mapping

Notion Block HTML Element Font Size Usage
heading_1 <h1> Special Gothic Condensed, uppercase 36px (text-sys-heading) Main page titles
heading_2 <h2> Special Gothic Condensed, uppercase 36px (text-sys-subheading) Section titles — same size as h1 by design
heading_3 Blue <a> or <div> Special Gothic Condensed, uppercase Project titles & "Defining Document" buttons — repurposed as UI component
heading_4 <h4> Sans-serif (font-sans), bold 18px (text-lg) Fallback / API-generated sub-headings. Not available in Notion UI.
Notion's UI only lets editors create heading_1, heading_2, and heading_3. The heading_4 type exists in the renderer as a fallback for blocks generated programmatically via the Notion API.
Known code discrepancy: In the current block-renderer.tsx, both heading_1 and heading_2 use the text-sys-subheading Tailwind class. The heading_1 case should use text-sys-heading instead. Because both tokens resolve to 36px in globals.css, there is no visual difference — but the class name is semantically incorrect. Fix: swap text-sys-subheading text-sys-heading on the heading_1 branch in block-renderer.tsx.

Blue Buttons & Project Titles

The renderer repurposes Notion's Heading 3 blocks as stylized blue UI elements — used for "Defining Documents" buttons and Project Titles in news articles.

When the renderer encounters a heading_3 block it checks for a hyperlink (href) in the text:

  • With a link — Renders an interactive <a> button with an animated arrow on hover.
  • Without a link — Renders a static blue block for project titles. No arrow or hover effects.
Defining Document Re-Braiding Project

Special Intercept Logic

Hardcoded suppression logic exists in the paragraph block renderer. Two categories of text are intercepted and return null.

1. Redundant Text Suppression

Certain documents represented by heading_3 buttons have redundant paragraph text that immediately follows them in Notion. The renderer intercepts and suppresses paragraphs starting with specific known phrases.

2. Admin Notes

Any paragraph beginning exactly with the following string is hidden from the public site — allowing maintainers to leave backend notes in Notion:

String Match
"NOTE: The window for submissions is open between April 1 and April 30"
This is a hardcoded string match. If the note text changes in Notion, it will become visible on the live site. Keep the prefix consistent.

Compact Prop

The component accepts an optional compact boolean (default: false) that switches to tighter vertical spacing. Currently used in the site-wide footer (layout.tsx) so multi-line license or attribution text doesn't appear double-spaced.

TSXCopy
// Standard spacing (default — used on all content pages)
<BlockRenderer block={block} />

// Compact spacing — used in layout.tsx footer
<BlockRenderer block={block} compact={true} />
Element Default spacing Compact spacing
Paragraph bottom margin mb-6 (24px) mb-1 (4px)
List item bottom margin mb-3 (12px) mb-1 (4px)
Divider vertical margin --sys-subheading-gap (24px) my-2 (8px)

Operations

Deployment

How to deploy the site to Vercel and connect it to the custom CMU domain.

Vercel Setup

  1. 1
    Connect the GitHub repository to Vercel.
  2. 2
    Add all environment variables in the Vercel dashboard (mirror your .env.local).
  3. 3
    Vercel auto-deploys on every push to the main branch.

Custom Domain

Currently using: cybernetics.cmu.edu

  • DNS records are managed by CMU IT.
  • Vercel handles the SSL certificate automatically.
After setup, Notion content changes appear on the website within 1–2 minutes. No redeployment needed for content updates.

Operations

Troubleshooting

Solutions to the most common issues encountered in development and production.

"Notion API Error: Object not found"

  • Check that the integration is shared with the specific database or page.
  • Verify the database ID is correct in your environment variables.
  • Make sure NOTION_TOKEN is valid and hasn't been revoked.

"Email not sending"

  • Verify the Gmail App Password is correct (16 characters, spaces allowed).
  • Check that 2-Step Verification is still enabled on the Gmail account.
  • Gmail may rate-limit — check daily send count.
  • Ask the user to check their spam folder.

"Form submission fails"

  • Check the browser console for error details.
  • Verify all required fields are filled.
  • Check that Select / Multi-select options exist in the Notion database.
  • Review server logs in the Vercel dashboard.

Content not updating

  • Clear Next.js cache: rm -rf .next then restart.
  • Check Notion page permissions — confirm the integration still has access.

Autocomplete not working

  • Verify the Notion API can query the Matching database.
  • Check the domain extraction logic in lib/notion.ts.

Reference

Handoff Notes

Everything a future maintainer needs — what to read first, who to contact, and the key links.

For Future Maintainers

This codebase was designed for AI-assisted development. TypeScript types provide context for Copilot, Cursor, and Antigravity. Comments explain why, not just what. Server Actions are simpler than API routes for AI to understand. Notion API patterns are consistent throughout.

Before Making Changes

  1. 1
    Read this documentation fully.
  2. 2
    Review the PRD (L4C_Website_PRD.md).
  3. 3
    Set up your local environment and test it works end-to-end.
  4. 4
    Make changes in a feature branch.
  5. 5
    Test thoroughly before merging to main.

Contacts

Lab Director
Paul Pangaro
Original Developer
Narayan
Collaborator
Chris / Yixiw

Design System

Color Palette

All tokens are defined in src/app/globals.css via Tailwind CSS v4's @theme inline directive and used as text-*, bg-*, or border-* classes.

Brand Colors

Brand Dark
#323639
text-brand-dark
Primary text, dividers, prominent elements
Brand Grey
#9ba0a5
text-brand-grey
Subheadings, timestamps, secondary text
Brand Blue
#1a0dab
text-brand-blue
Active links, buttons, hover states
Brand Tan
#f6f4eb
bg-brand-tan
Secondary sections, callout backgrounds
Background
#ffffff
bg-background
Primary page background

Component Colors

Used exclusively within specific UI components — not general-purpose tokens.

Action Blue
#95cee9
Default bg for heading_3 action blocks
Action Hover
#8ac1da
Hover state for interactive action blocks
In Tailwind v4, use as: text-brand-dark, bg-brand-tan, border-brand-grey, etc.

Design System

Typography

Two font families and a strict text scale. All sizes are defined as CSS custom properties in globals.css and exposed as Tailwind utility classes.

Font Families

Body · font-sans
Aa Bb Cc
0123456789
Inter Tight · 400 / 500 / 600
Headings · font-special-condensed
L4C
Cyber
Special Gothic Condensed One · 400

Text Scale

text-sys-heading
36px · Special Gothic
<h1> · Main titles
Page Title
text-sys-subheading
36px · Special Gothic
<h2> · Section titles
Section Title
text-sys-nav
20px · Inter Tight
Navigation links
Research · Projects · People
text-sys-normal
16px · Inter Tight
<p> · Body text
Standard body paragraph used across all rendered Notion content.
text-sys-top-left
16px · Inter Tight
Breadcrumbs, dates
Documentation · April 2026
text-sys-heading and text-sys-subheading are intentionally both 36px. Hierarchy is communicated through spacing and context, not size.

Design System

Layout & Spacing

Three spacing tokens govern all rhythm and layout. Always use these instead of hardcoding values — updates in globals.css then propagate site-wide.

Spacing Tokens

--sys-padding Desktop: 64px · Mobile: 32px
Global page edge padding
64px
Content Area
px-[var(--sys-padding)]
--sys-body-width 1200px (desktop & mobile)
Max-width reading column
Viewport
max-w: 1200px
1200px max
max-w-[var(--sys-body-width)] mx-auto
--sys-subheading-gap 24px (desktop & mobile)
Subheading ↔ divider rhythm
Section Heading
24px

Body text follows with consistent 24px rhythm above and below the rule.
my-[var(--sys-subheading-gap)]

Usage in Tailwind v4

HTML / JSXCopy
<!-- Global page padding -->
<div className="px-[var(--sys-padding)]">

<!-- Max-width content column -->
<div className="max-w-[var(--sys-body-width)] mx-auto">

<!-- Subheading gap for dividers -->
<hr className="my-[var(--sys-subheading-gap)]" />

Design System

Component Patterns

Reusable UI patterns with exact specifications and annotated renders. Dimensions, radii, and spacing are marked directly on the rendered components.

Blue Action Blocks (heading_3)

Rendered by the Block Renderer when it encounters a heading_3 Notion block. Used for Project Titles and "Defining Document" links.

Default state · With link
Defining Document
48px h
14px
r: 6px
20px padding-x
Hover the button — border-radius animates to 0px
Static variant · No link (project title)
Re-Braiding Project
No hover effect. No arrow. Same dimensions as interactive variant.
Property Default Hover
Background #95cee9 #8ac1da
Border Radius 6px 0px
Padding 14px 20px
Font Special Gothic Condensed One, uppercase
Text Color text-brand-dark (#323639)
Arrow Present only when block has a link

Dividers (<hr>)

Annotated render
Section Heading
↕ 24px

↕ 24px

Body text continues here.

Property Value
Page dividers border-neutral-300
Card dividers border-neutral-200
Margin (top & bottom) my-[var(--sys-subheading-gap)] = 24px
Active link color: brand-blue · underline · offset: 3px
Nav link default: brand-dark · no underline Nav link :hover hover: brand-blue
ah