Building Historio: Episode 1. Which Feature First?
From initializing a NextJS project to deploying a landing page on Cloudflare, everything I learned as I start building an app to change how you explore history.
This post is in my series on Building Historio, a new app to explore history through your favorite books. To learn more about what I’m building and how I get started check out this post:
Over the past few weeks I got to answer one of the most exciting questions in product development: Where to start? I contemplated the question so much that I wrote a whole post on how to prioritize early features:
In keeping with my Early Product Cycle, I identified creating AI research assistants as the high-risk/high-momentum feature that would be best to start with. These assistants are tasked with extracting dated events from a book and enriching them with descriptions, Wikipedia links, and tags. I built two researchers as separate Open AI assistants and created a script to run them on books imported via Goodreads. After working through parsing ambiguous dates, de-duplicating events, and books with incorrect ISBNs, the process is scaling well. I’m up to 2,500 AI-generated dated and detailed insights from popular history books — you can see a sampling here. If only I had a frontend demo to show them to you, but…alas, it wasn’t risky enough to get prioritized!
The first Historio product milestone is to generate 25,000 insights across roughly 600 books (and all of the models and backend utilities needed to get there). At this point, I should be able to hit this with just the scripts I’ve already created and a bit of patience as they run.
I Learned…
How to Initialize a new Next.js project with TailwindCSS and Sass
Surprisingly, the Getting Started instructions for NextJS and TailwindCSS worked out of the box. Use the app router - it’s better (that’s why they made it).
I use SASS, and NextJS has built-in support for sass. Just install sass (npm install --save-dev sass
), and then you can import classes as variables from sass files.
How to run a Typescript file as a script from the command line
TSX executes a typescript file; npx, of course, installs and executes tsx if it doesn’t exist.
My researches are pretty modularized, and I created a script that calls them in the same way I expect some APIs or server components to in the future. Executing a script is as easy as:
npx tsx scripts/scriptProcessBook.ts
How to prompt for user input in a TS script
Providing input makes scripts even more reusable. I can choose which researcher to execute and which book to research. Here’s the snippet to prompt a user for input in a TS script, and you can see an example in my script to process books here.
import readline from "readline"
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
async function promptForInput(question: string): Promise<string> {
return new Promise((resolve) => {
rl.question(question, (answer) => {
resolve(answer)
})
})
}
How not to execute an async function iteratively
Will these async functions be executed in series or parallel?
_.range(10).forEach(async (idx) => doSomething())
The answer: In parallel, which is not what I wanted. An old-fashioned for loop was the right solution:
for (let i = 0; i < 10; i++)
doSomething()
How to deploy a static site with Cloudflare pages
I’ve followed a common arc in deploying websites: My first sites were deployed by dragging a folder from my computer to a server with an FTP client; once I started using git, it was a git pull
on the server, followed by some bash scripts; the final evolution moves all of this into CI/CD pipelines.
But those pipelines (even Github pages) are really overkill if you merely want to deploy a static site.
I tried really hard to stick to basics for the first Historio Landing Page, and deploying with Cloudflare pages couldn’t be easier. Literally just drag and drop a zip folder with a static site and boom it’s deployed.
Getting Started with Drizzle
How to set common fields on all Drizzle models
One pattern I love with Django’s ORM is using abstract models to define fields that are re-used across models. This makes it easy to ensure models are consistent and core fields are predictable. To achieve a similar pattern with drizzle, I defined common fields in an object that gets spread in model definitions:
import { timestamp, uuid } from "drizzle-orm/pg-core"
export const BASE_SCHEMA_FIELDS = {
id: uuid("id").primaryKey().defaultRandom()
}
// to implement in a model:
import { BASE_SCHEMA_FIELDS } from "./common"
export const books = pgTable("books", {
...BASE_SCHEMA_FIELDS,
title: varchar("title"),
author: varchar("author"),
// ... additional fields
})
How to set created and updated timestamps that are automatically set on a Drizzle model
I lied: That’s not the entirety of my BASE_SCHEMA_FIELDS
. There are two additional fields that I include on every single model: created
and updated
that indicate when an object was created and last updated, respectively. Most DBs have a default and trigger that set these values automatically. Here’s how to implement with Drizzle:
export const BASE_SCHEMA_FIELDS = {
// ... additional fields
created: timestamp("created").notNull().defaultNow(),
updated: timestamp("updated")
.notNull()
.$onUpdate(() => new Date()),
}
How to write a complex query with Drizzle ORM without extensively reading the docs
Drizzle ORM is totally new to me, and I only know how to write basic queries by hand. For complex queries that - say - filter on the count of a grouped by join, I’ve started turning to AI to help generate and explain the query. Gemini, specifically, has been really good at this. It understands my models, and the resulting query syntax is usually (but not always) correct. Here’s an example:
What’s Next
Lots of great learnings as I get this project off the ground. As I sprint past the milestone of generating 25,000 insights, I will start working on the front end, including initializing Supabase Auth and NextUI. Let me know if there’s any topic you’d like a deeper dive on. See you in the next episode!