Overview
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.
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
Architecture
Project Structure
Directory layout and the purpose of each top-level folder.
Directory Layout
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
-
1Clone the repository:
git clone [repo-url] -
2Install dependencies:
npm install -
3Create
.env.local— see Environment Variables below. -
4Run development server:
npm run dev -
5
Environment Variables
# 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) |
.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:
- Submit information via a custom form
- Browse and search all entries
- Receive match suggestions based on shared interests
- 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 Feedbackare 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 |
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:
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.
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}}] |
<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} |
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 |
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 |
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:
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:
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. |
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.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 animatedarrow on hover. - Without a link — Renders a static blue block for project titles. No arrow or hover effects.
Special Intercept Logic
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:
"NOTE: The window for submissions is open between April 1 and April 30"
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.
// 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
-
1Connect the GitHub repository to Vercel.
-
2Add all environment variables in the Vercel dashboard (mirror your
.env.local). -
3Vercel auto-deploys on every push to the
mainbranch.
Custom Domain
Currently using: cybernetics.cmu.edu
- DNS records are managed by CMU IT.
- Vercel handles the SSL certificate automatically.
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_TOKENis 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 .nextthen 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
Before Making Changes
-
1Read this documentation fully.
-
2Review the PRD (
L4C_Website_PRD.md). -
3Set up your local environment and test it works end-to-end.
-
4Make changes in a feature branch.
-
5Test thoroughly before merging to
main.
Contacts
Important Links
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
Component Colors
Used exclusively within specific UI components — not general-purpose tokens.
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
font-sans
0123456789
font-special-condensed
Cyber
Text Scale
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
Usage in Tailwind v4
<!-- 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.
| 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>)
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 |