TutorLoom
The learning platform for companies, colleges and training institutes. Free and open source, with an Enterprise edition.
翻译准备中
尚未收集仓库信息。
尚未检测到更新。GitHub 发布版本后会显示在这里。
One word, one story, every day — a daily etymology app (Next.js)
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗One word, one origin story, once a day — now as a website instead of the
original React Native app. See website-pivot-brief.md (in the project) for
the reasoning behind the pivot; this README covers what's actually built here.
/) — shows the date-math-determined word of the day. No
backend sync needed to compute it; same logic that powered the app's
notification timing, minus the notification./story/[slug]) — Origin / Journey / Related Words
sections, favorite (heart) and share (Web Share API, falls back to
copy-link) actions. Reachable from Today, History, Collection, or an emailed link./collection) — the words you've favourited, newest
first, with a language band drawn from them. Static, and no account is
needed: favourites live in localStorage (and sync to the account when
signed in). It loads their word data from /api/words./history) — the archive. Signed in, "My days" and "All
words"; signed out, just "All words". Linked from Today ("Past words")
and Collection, not the nav. Favourites aren't here: that's Collection./api/subscribe takes {email, source?}; anonymous signups confirm by email (double opt-in: a signed 7-day link, and a button on the page it opens adds the address).
/api/cron/send-daily runs once a day at 09:00 UTC (vercel.json; the
Hobby plan allows one run a day) and sends every subscriber the day's
shared word through Resend's batch endpoint. It's idempotent per UTC day
(?resend=failed, ?force=1 and ?force=1&bluesky=1, all with
CRON_SECRET, for deliberate re-sends; see handover.md). Every digest
carries a signed unsubscribe link plus List-Unsubscribe / one-click
headers; opening the link shows a confirm page, and only its button (or a
mail app's one-click) unsubscribes.localStorage./attribution) — the CC BY-SA notice for Wiktionary /
Wiktextract content, linked from the footer. This was an open compliance
item on the mobile app too — it's included from the start here rather
than left for later.@curiodaily.bsky.social) — the same daily cron posts the
word: a teaser, then word · lineage arrows, then #etymology #wordoftheday (format (a)), with the story link moved out of the text
into a link card (the story's own title, a fixed description, and its
Open Graph image as a thumbnail). npm run bluesky:preview -- [days] [from] prints what the next days' posts will look like, read-only, for
review before anything goes out./api/cron/social at
10:00 UTC, posts the same word to Threads (a text post with a link card)
and to Instagram (a 4–6 slide 1080×1350 carousel, built only from the
word's stored fields and served as JPEG from
/social/carousel/<slug>/<n>). Each channel is idempotent per UTC day,
with ?repost=threads|instagram (CRON_SECRET) for a deliberate
re-post. Nothing posts until the four THREADS_* / INSTAGRAM_* env
vars are set — without them the cron only logs, and removing a token
turns its channel off again — and the tokens are refreshed weekly into
Redis in production. npm run social:preview -- [days] [from] prints the next days' posts and slide URLs, read-only, for
review. Setup and operations are in handover.md./admin shows it; npm run traffic:report -- [days] prints it, read-only. See "Measuring growth" in handover.md.lib/languages/data.ts. The sheets come from an
offline pipeline, run in this order (details and checks in handover.md,
"Language panel"):
npm run languages:facts -- [--only "A,B"] [--limit N] [--refresh]
fetches Wikipedia and Wikidata facts;
npm run languages:draft -- [--only] [--limit] has Claude draft a sheet
from those facts (needs ANTHROPIC_API_KEY in .env.local);
npm run languages:review writes the git-ignored review.html;
npm run languages:approve -- --all-valid | --only "A,B" validates and
writes data.ts and sheetIndex.ts (never hand-edit either).
npm run languages:map regenerates the dev-only map dots. data.ts is
empty until the first reviewed batch, so no "About" link shows yet.lib/words.ts has 8 seed entries, written in Curio's voice from general
etymological knowledge. This is a stand-in for the real content pipeline —
port over the offline Wiktextract + Haiku rewrite pass from the mobile
project to generate a full word bank, then replace the WORDS array (or
swap lib/words.ts to read from wherever that pipeline writes its output).
Both lib/db.ts (subscribers) and lib/email.ts (sending) check for
credentials and fall back to something dev-friendly if they're missing:
| Configured | Fallback (no env vars) | |
|---|---|---|
| Datastore | Upstash Redis | Local JSON file at .data/subscribers.json |
| Resend | Logs to console instead of sending |
This means npm run dev works out of the box with zero setup — subscribing
writes to a local file, and the cron route logs what it would send. Add
real credentials (see .env.example) whenever you're ready to actually
send mail.
```bash npm install cp .env.example .env.local # optional for local dev — works without it npm run dev ```
Visit http://localhost:3000. To test the send flow locally:
```bash curl http://localhost:3000/api/cron/send-daily ```
The natural fit is Vercel (matches Next.js, and vercel.json already
configures the two daily crons: the digest at 09:00 UTC and the
Threads/Instagram posts at 10:00 UTC):
.env.example in the Vercel project
settings — at minimum RESEND_API_KEY, CURIO_FROM_EMAIL,
UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN, CURIO_SITE_URL
(your production domain), CRON_SECRET, and UNSUBSCRIBE_SECRET
(required: without it the daily send refuses to run). Optionally, the
four THREADS_* / INSTAGRAM_* vars: without them the social cron only
logs, so they're what switches those posts on.vercel.json automatically on deploy — no extra
configuration needed.CURIO_SITE_URL and sender address.