An on-this-day journal feature from 3,000 Day One entries
I exported 3,060 Day One journal entries as JSON, imported them into DynamoDB, and built an "on this day" widget for a home dashboard. Once it was running, the widget started surfacing things I had no memory of writing. That was the whole point.
What was happening
Day One exports give you a JSON array of entries with creationDate, text, attached photo references, and a few metadata fields. The naive shape is fine for a one-shot export but useless for "show me what I wrote on this date" because you'd scan the whole table on every request.
The fix is to index by month-day in addition to full date. Every entry gets a synthetic monthday attribute (MM-DD) so a Query can pull just the entries that match today.
What I found
DynamoDB single-table layout:
- PK:
JOURNAL - SK: ISO timestamp of the entry
- GSI1 PK:
MD#{monthday}(e.g.MD#04-28) - GSI1 SK: entry ISO timestamp
- Attributes:
text,word_count,creation_date,photo_count
Importing was a BatchWriteItem loop in chunks of 25. The hot path is the widget:
import boto3
from datetime import datetime
from zoneinfo import ZoneInfo
import random
ddb = boto3.resource("dynamodb").Table("journal_entries")
def on_this_day():
md = datetime.now(ZoneInfo("America/Chicago")).strftime("%m-%d")
resp = ddb.query(
IndexName="GSI1",
KeyConditionExpression="GSI1PK = :pk",
ExpressionAttributeValues={":pk": f"MD#{md}"},
)
items = resp.get("Items", [])
# tiered fallback: prefer substantive entries
pool = [i for i in items if i["word_count"] >= 80]
if not pool: pool = [i for i in items if i["word_count"] >= 30]
if not pool: pool = items
return random.choice(pool) if pool else None
Three tiers because some days I wrote two sentences and some days I wrote a thousand words. Always picking the longest entry was repetitive; always picking randomly surfaced "ran errands, made pasta" too often. The tiered fallback prefers anything substantive but doesn't lose the day entirely if all I had was a one-liner.
The widget renders a word-wrapped block at 416 × 186 pixels on an e-ink display. Word wrap with no JavaScript is annoying enough that I do it server-side and serve a static <pre>:
import textwrap
def wrap_for_display(text, cols=42, rows=8):
lines = []
for para in text.splitlines():
if not para.strip():
lines.append("")
continue
lines.extend(textwrap.wrap(para, width=cols))
if len(lines) > rows:
lines = lines[:rows-1] + ["…"]
return "\n".join(lines)
The response is cached six hours. The cache key is just the month-day, so the widget shows the same entry all day, which is the right behavior — it's a daily glance, not a refresh button.
What I'd do differently
Leap year. Feb 29 entries surface only one year in four with the current implementation, which is technically correct but feels weird. A future change is to fold Feb 29 into Mar 1's pool with a tag so I can tell visually that it was originally Feb 29. The math isn't hard; I just haven't done it yet.
The other thing I'd add: a "do not surface" flag. There are entries I'm fine never having appear on the dashboard. A single boolean attribute filtered at query time would handle it. I keep almost building this and then deciding it's a problem for the entries themselves, not the widget.