About Tea with Jesus
Tea with Jesus
Tea with Jesus started as a passion project from my "when life gives you lemon, make lemonade" season but i like ice lemon tea better and this was birthed as part of.
This started off as a very simple project but have developed into something more with a blog space, a curated synced music player (on the main page and bottom right.. it was so hard to do!!), a fun brew with tea, and listening aids. There were so much details that went into this. i.e the Music Player was always cut when you navigate into a post, so I put in a persistent Music Player on the dock (bottom right) and kept it in sync with the main Music Player~
Overview
Notion-powered, Next.js blog focused on slow, reflective reading with a few delightful touches. The below is mostly AI generated and summarizes the core features that was implemented and how to use/customize them. PM me if you want access to the project :)
- Tech Stack: Built on Next.js App Router, JavaScript / TypeScript, TailwindCSS, backed using Notion API (Data are stored here!), and Upstash Redis for fast caching
- Primary content storage: Undeer the hood, the content (posts) are stored on Notion's database (posts, status, dates, etc.) which is brilliant because I didn't use a conventional database such as MySQL but something that I can edit directly on Notion as a post
Reader Experience
Homepage
-
Verse with Tea
- Random scripture with a short reflection prompt.
- Shared context via
VerseProvider
so the same verse appears inside the submission flow. - “Brew another” to reshuffle client-side.
-
Post grid
- Clean, accessible post cards with date and reading time.
- Author derived from Notion (fallbacks included).
- A “Write a post” card to open public submission.
Post page
-
Listen to post (TTS)
- Text-to-speech controls using Web Speech API (
ListenControls
). - Strips markdown and reads content aloud.
- Text-to-speech controls using Web Speech API (
-
Reading aids
- Reading time based on word count.
TableOfContents
auto-generates anchors for long posts.ScriptureAnnotator
upgrades inline scripture references into interactive components.
-
Engagement
ViewsCounter
: increments and displays total views per post.Reactions
: lightweight reactions (Amen, Pray, Heart) with live counts.
Music Player System
-
Persistent dock
MusicDock
mounts once inLayout
and acts as the single playback host.- Audio continues playing across page changes.
-
Full/Mirror UIs
- Homepage
MusicPlayer
is a mirror (non-host) UI. - Host/mirror sync via
BroadcastChannel
keeps state in sync (play/pause/seek/track).- Hopefully there is no more bug here.
- Homepage
-
YouTube playback
- Uses YouTube Iframe API, robust multi-instance loader with a global callback queue.
- Resumes correctly when gaining host role: seeks to last time and continues if playing.
-
State persistence
- Restores last known track/time and infers elapsed time when resuming.
Community Submissions
-
Public submissions
- Share your tea with Jesus
- Saved to Notion with status set to “In Review” before it is published.
-
Rate limiting
- Basic IP-based limit (default: 3 submissions/hour). Adjustable in
src/app/api/submissions/route.ts
.
- Basic IP-based limit (default: 3 submissions/hour). Adjustable in
-
Validation & sanitization
- Input sanitized (
src/lib/sanitize.ts
) and length-checked. - Tags trimmed, capped in count and length.
- Input sanitized (
-
Notion mapping
- Status:
In Review
- Dates:
Published Date
+ optional rich text timestamp - Author priority: rich_text
Author
→Submitted By
→Author Name
→ PeopleAuthor
- Status:
Content Pipeline
-
Notion integration
- Loads posts filtered by
Status = Published
. - Slugs from title; description from first non-empty paragraph.
- Word count and reading time calculated at render.
- Loads posts filtered by
-
Caching
- Upstash Redis caching for lists/summaries.
- Safe
withLock
helper to prevent overlapping refresh jobs.
Public APIs
These are mainly for developers. But if you want to use: https://icelemontees.danielninetyfour.com/api/verse/random
-
Verse
GET /api/verse/random
:{ text, reference, version, prompt }
GET /api/verse/lookup
: resolve references (used byScriptureReference
)GET /api/verse/daily
: daily verse (for future scheduling)
-
Views
POST /api/views/[slug]
: increments and returns{ views }
GET /api/views/[slug]
: returns{ views }
-
Reactions
GET /api/reactions/[slug]
:{ counts }
POST /api/reactions/[slug]
: body{ type: "amen" | "pray" | "heart" }
, returns updated{ counts }
-
Submissions
POST /api/submissions
: create a Notion row with validations and rate limiting
-
Posts
GET /api/posts/[slug]
: server-side helper endpoint (used for internal/SSR fetches if needed)
-
Drafts/Admin (optional)
GET /api/drafts
: list drafts (auth/secret-gated)GET /api/drafts/[slug]
: fetch a draftPOST /api/admin/sync-views
: sync view counts back to Notion (requires secret)GET /api/admin/debug-views
: debug view dataGET /api/cron
andGET /api/cron/ping
: heartbeat/background triggers
SEO & Metadata
-
Global metadata in
src/app/layout.tsx
:- Open Graph and Twitter cards
NEXT_PUBLIC_SITE_URL
for canonical URLs- OG image at
/public/opengraph-image.png
-
robots.ts
andsitemap.ts
included.
Accessibility & UX
- Semantic HTML and focusable controls.
- Tailwind utility classes for contrast and responsive design.
- Dark mode via
ThemeProvider
. - TTS for readers who prefer listening.
- Keyboard-friendly buttons and inputs.
Configuration
Set environment variables:
NOTION_TOKEN
NOTION_DATABASE_ID
NEXT_PUBLIC_SITE_URL
- Optional (Redis caching):
UPSTASH_REDIS_REST_URL
,UPSTASH_REDIS_REST_TOKEN
- Optional admin endpoints:
ADMIN_POST_SECRET
Development
- Install:
npm install
- Run:
npm run dev
- Source directories:
src/app
: routes, pages, and API handlerssrc/components
: UI components (music player, verse, reactions, etc.)src/lib
: Notion, Redis, utilities
Roadmap Ideas
- Shareable verse/quote image generation (but honestly it would be very expensive for the AI generation cost)
- User authentications via SSO?
- Customisable Playlist management UI for the music player.
If you have any suggestion, let me know!
- Made with Tea :)