An AI-generated travel inspiration site. Users search for a place; if it isn't already in the catalogue, the destination is queued and progressively enriched by GPT into a full, structured landing page — description, honest 0–10 ratings, sights, best times to visit, transport notes, and related destinations.
Built in 2023 as a solo project. Live at triptldr.com.
- Nuxt 3 (Nitro server, SSR) with Vue 3 and Pinia for state
- MongoDB via Prisma — document model fits the nested, semi-structured destination data well (ratings, sights, best-times arrays, embedded types)
- OpenAI (
gpt-3.5-turbo/gpt-4) for all content generation - Tailwind CSS + daisyUI,
@nuxt/imagefor responsive image delivery - @nuxt/content for hand-written editorial pieces (monthly "best places" guides)
- @sidebase/nuxt-auth (NextAuth) for login, Stripe for subscriptions
- Leaflet maps, Plausible analytics, schema.org / sitemap / robots modules
- Deployed on Fly.io (Amsterdam) via a multi-stage Docker build
A destination starts life as a bare row with only a queryName. A Nitro plugin
(server/plugins/queue-worker.ts) runs a scheduler that picks up incomplete,
non-duplicate destinations and advances each one one step at a time.
The pipeline (server/helper/jobs/) is an ordered list of independent steps —
updateBasics, updateDescription, updateTags, updateRatings,
updateSimilarDestinations, updateSights, updateBestTimes, updateTransport,
updateThingsToDo, updateCoordinates. Each completed step name is pushed to a
jobsDone array on the document; the worker simply runs the first step not yet
present. Once every step is done the destination is marked complete and published.
Each AI step follows the same contract: build a prompt that asks GPT to return JSON against a documented schema, call OpenAI, then validate the response with Zod. If parsing or validation fails it retries up to 3 times; if it still fails the destination is deleted rather than published half-formed.
updateBasics also does deduplication — it normalises the place to an
English name + ISO country code and drops the record if a match already exists,
which keeps a free-text search box from spawning duplicate pages.
- Step-based enrichment over one big prompt. Splitting generation into resumable steps keeps each prompt focused and cheap, makes a crashed or rate-limited run recoverable from where it stopped, and lets a page publish incrementally instead of all-or-nothing.
jobsDoneas the source of truth. Progress lives on the document itself, so the worker is stateless and idempotent — no separate job queue to keep in sync. A cleanup task also un-sticks rows leftisUpdatingfor over a minute.- Schema-validated AI output. Every model response is JSON parsed and Zod-checked before it touches the database, so malformed generations fail loudly and are retried instead of corrupting content.
- MongoDB for a generative content model. The destination shape evolved
constantly during development (see
TODO.md); a document store absorbed new embedded fields without migrations.
Beyond the catalogue, tripBot takes a user's home base, travel months,
preferences, and trip distance, and asks GPT for tailored destination
recommendations — auto-queuing any suggested place not yet in the database.
npm install
npm run dev # http://localhost:3000Requires OPENAI_API_KEY and DATABASE_URL (MongoDB) in the environment;
Stripe and auth keys are needed for the paid/login flows.
A 2023 prototype, not actively maintained. Rough edges remain: the Stripe
free-user limit and auth guard in tripBot are commented out, get-image.ts
is an empty stub, and package.json still carries the nuxt-app scaffold name.