Skip to content

Latest commit

 

History

1,075 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Bully Pulpit

Personal website and blog for philipithomas.com.

Features

  • Three newsletters: Contraption (essays), Workshop (notes), Postcard (monthly updates)
  • MDX content with full React component support
  • Static generation with Next.js App Router
  • RSS and JSON feeds (combined + per-newsletter)
  • LLM-friendly .md endpoints for all content
  • Google sign-in (OAuth2 authorization code flow)
  • Email subscription via printing-press backend

Quick Start

nvm use 24
pnpm install
pnpm dev

Open http://localhost:3000.

Local Printing press

The admin uses the normal subscriber session in development. Bring the development database schema up to date, then start the site normally:

pnpm db:migrate
pnpm dev

Open http://localhost:3000, sign in through the normal email form, and enter 000000 when asked for the six-digit code. That fixed code works for any email only under next dev; production still requires the generated one-time code. Printing press access remains limited to addresses in ADMIN_EMAILS.

pnpm db:migrate changes whichever database is named by the loaded DATABASE_URL_UNPOOLED or DATABASE_URL. Confirm that the local environment points to the development Neon branch before running it.

Phone and SMS deployment

Inbound Twilio traffic is webhook-based. The app does not poll Twilio for new calls or messages. Vercel must expose the production routes below, and the Twilio number must point to them with HTTP POST.

Set these environment variables in Vercel before cutover:

PHONE_NUMBER=
TWILIO_SID=
TWILIO_SECRET=
OWNER_PHONE_NUMBER=

Configure the Twilio number:

  • Voice, "A call comes in": https://www.philipithomas.com/api/phone/voice
  • Messaging, "A message comes in": https://www.philipithomas.com/api/phone/sms

Every inbound webhook must include Twilio's X-Twilio-Signature header. The app validates the exact public URL and all form parameters with TWILIO_SECRET, the account auth token. Never put that token in a webhook URL.

When PHONE_NUMBER is configured, public subscribe prompts offer SMS and the voice webhook plays the generated greeting, then offers "press 1" for voicemail and "press 2" to subscribe the caller ID to SMS updates. Before the caller chooses, the prompt identifies recurring new-post texts and states the frequency, rate, HELP, and STOP disclosures. No input falls through to voicemail. The SMS webhook stores inbound replies, handles signup, HELP, and STOP words, and emails admins about normal replies. SUBSCRIBE replies with a branded confirmation that identifies the recurring new-post message type, says that frequency varies and message and data rates may apply, and includes both HELP and STOP instructions. When Twilio has not already handled the keyword, HELP returns the support address and repeats the frequency, rate, and STOP details. Voice-menu signups send the same subscription confirmation SMS when Twilio accepts it; if that confirmation send fails, the spoken confirmation still tells the caller to text STOP at any time. STOP hard-deletes the local SMS subscriber and its history instead of retaining an inactive tombstone. A later keyword or voice-menu signup creates fresh local state, although Twilio may continue blocking delivery until the handset sends START or UNSTOP.

When a number first becomes active through either a SUBSCRIBE text or the voice menu, the app also sends one Bell onboarding MMS with the contact card at https://www.philipithomas.com/bell.vcf. Repeating SUBSCRIBE while the number is active does not resend the onboarding message. STOP deletes the local SMS subscriber and its history, so a later signup creates a fresh subscription and sends the card again. Twilio may still require START or UNSTOP to lift its own delivery block. The public /bell.vcf permalink returns a vCard 3.0 contact named Bell with the sending number, the configured site organization and website, and an embedded JPEG Bell contact image. On iPhone, Messages opens the attachment in the native contact preview, where the person taps Create New Contact. The site cannot save the contact silently. New SMS opt-ins, whether they come from a SUBSCRIBE text or the voice menu, also email admins with the source path, Twilio webhook metadata such as city, state, caller name, message SID, or call SID when Twilio provides it, and an area-code hint for common US/Canada numbers. Keyword handling is both Twilio-aware and application-layer: Twilio may apply its own START, STOP, or HELP behavior and include OptOutType in the webhook. The app creates a fresh local subscriber for signup keywords and hard-deletes all local SMS data for STOP while avoiding duplicate Twilio keyword responses. A durable signup workflow sends app-owned confirmations, pauses for three seconds, and then sends the distinct one-time Bell onboarding card. Twilio-classified START keeps Twilio's own confirmation and enters the workflow at the pause. The onboarding copy tells an iPhone user to tap Create New Contact and invites questions about philipithomas.com; an SMS link to /bell.vcf is the fallback when the MMS cannot be attached. The configured Twilio Advanced Opt-Out responses are the user-visible replies for classified START, STOP, and HELP messages, so their START and HELP copy must stay aligned with the disclosures and support address above. STOP deletes pending and historical sms_sends rows for that number, and an in-flight workflow treats a missing row as unsendable.

Newsletter SMS delivery runs inside the same Vercel Workflow as email delivery: the admin send page enqueues sms_sends rows after the email pass, sends them through Twilio's REST API from PHONE_NUMBER, and records outbound texts in the Phone panel. SMS subscribers are separate from email subscribers and are opted into every newsletter as one list.

PHONE_NUMBER is the public E.164 Twilio number for the active environment. It appears on subscribe and contact surfaces, including the /contact.md mirror, and is the caller ID for click-to-call. OWNER_PHONE_NUMBER is the private E.164 number that click-to-call rings first. The admin "Send test text to me" button also sends test newsletter texts there.

After deploy, verify:

WORKFLOW_SMOKE_BASE_URL=https://www.philipithomas.com CRON_SECRET=$CRON_SECRET pnpm workflow:smoke

Then confirm the production flag is still off before launch. With the flag on in preview, send SUBSCRIBE, HELP, and STOP to PHONE_NUMBER, call it and press both menu options, and confirm /printing-press/phone shows the inbound and outbound thread history. Use a fresh number to verify that both text and voice signup paths send the Bell card once. Then send STOP and confirm that a fresh voice signup creates a new local subscription. Twilio may reject its outbound confirmation until the handset sends START or UNSTOP. Open the attachment on an actual iPhone, confirm the Bell image and fields appear, and save it manually. In the Twilio Console, also confirm the Advanced Opt-Out START and HELP replies identify the program, include the support address, and match the frequency, message-and-data-rate, HELP, and STOP disclosures above.

Content

Posts live in content/ as MDX files:

content/
├── contraption/    # Essays and launches
├── workshop/       # Work in progress notes
├── postcard/       # Monthly updates
└── pages/          # Static pages (terms, privacy)

File format: YYYY-MM-DD-slug.mdx with frontmatter:

---
title: "Post Title"
description: "Optional description"
publishedAt: "2026-01-15"
coverImage: "/images/covers/slug.jpg"  # optional
---

Deployable images live under public/images/ and should be web-sized, not camera originals. Before committing new JPG/PNG/WebP/AVIF files, run:

pnpm images:optimize public/images/path/to/image.jpg
pnpm content:check

The optimizer resizes public raster images to the site policy: longest edge at most 5120px and file size at most 8MB. It skips files that already fit the policy, so rerunning it on the same image does not keep recompressing it. Keep full-resolution originals outside this app repository.

License

Copyright (c) 2020-2026 The Contraption Company LLC. All rights reserved.

This repository is source-available for viewing and reference, but no open license is granted. See LICENSE.

About

My personal website and blog

Resources

Stars

1 star

Watchers

0 watching

Forks

Used by

Contributors

Languages