Linking (staged): thread destinationSlug through the public tour payload
(provider-config + /tour serializer + shared TourStop type) and match the pSEO
city-page Event by destinationSlug (robust) with a city-name fallback. New
staged seed scripts/seed-nyc-tour-destinations.ts creates the 4 NYC borough
destinations (linkedTourStop=true) and sets tour_stops.destination_slug —
dry-run by default, --commit to apply, not run in CI. Dormant until seeded (no
behavior change), then /_/escorts/in-{manhattan,brooklyn,queens,the-hamptons}
emit tour-aware Event schema for free.
Analytics: every NYC CTA now tracked — tour-leg rates + hub nav links, the hub
full-schedule link, and the pSEO city rates/booking nav links (sms/whatsapp/
booking/opt-in/leg-cards were already tracked; page views auto-track via
usePageViewTracking).
Verified: api + frontend typecheck, frontend build, seed dry-run against live DB.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
150 lines
7.4 KiB
TypeScript
150 lines
7.4 KiB
TypeScript
#!/usr/bin/env bun
|
|
/**
|
|
* STAGED seed — NYC summer 2026 tour: borough destinations + tour-stop links.
|
|
*
|
|
* Creates the pSEO `destinations` rows for the NYC boroughs (linkedTourStop=true)
|
|
* and sets `tour_stops.destination_slug` so the /_/escorts/in-{slug} pages emit
|
|
* tour-aware schema.org/Event markup (matched via destinationSlug — see
|
|
* PseoCityPage). Idempotent: existing destinations are flipped to linkedTourStop,
|
|
* existing links are left alone.
|
|
*
|
|
* DRY-RUN by default — prints the plan and writes nothing. Pass --commit to apply.
|
|
* Targets the quinn DB (QUINN_DB_URL, default black:25435/quinn). NOT run in CI;
|
|
* review the planned output before committing changes to production data.
|
|
*
|
|
* bun run scripts/seed-nyc-tour-destinations.ts # dry-run
|
|
* QUINN_DB_URL=postgres://…@black.lan:25435/quinn \
|
|
* bun run scripts/seed-nyc-tour-destinations.ts --commit # apply
|
|
*/
|
|
import { openDb } from '@/shared/db';
|
|
import { listDestinations, createDestination, updateDestination } from '@/entities/destination/repo';
|
|
import type { DestinationDraft } from '@/entities/destination/types';
|
|
import { listTourStops, updateTourStop } from '@/entities/tour-stop';
|
|
|
|
const COMMIT = process.argv.includes('--commit');
|
|
const DB_URL = process.env['QUINN_DB_URL'] ?? 'postgres://quinn:quinn@localhost:25435/quinn';
|
|
const PROVIDER = 'quinn';
|
|
|
|
function log(line: string): void {
|
|
process.stdout.write(`${line}\n`);
|
|
}
|
|
|
|
/** Destination slug → exact tour_stops.city to link it to. */
|
|
const LINKS: ReadonlyArray<{ destSlug: string; tourCity: string }> = [
|
|
{ destSlug: 'manhattan', tourCity: 'New York (Manhattan)' },
|
|
{ destSlug: 'brooklyn', tourCity: 'Brooklyn' },
|
|
{ destSlug: 'queens', tourCity: 'Queens' },
|
|
{ destSlug: 'the-hamptons', tourCity: 'The Hamptons' },
|
|
];
|
|
|
|
const DESTINATIONS: readonly DestinationDraft[] = [
|
|
{
|
|
slug: 'manhattan',
|
|
city: 'Manhattan',
|
|
region: 'New York',
|
|
fmtyTier: 'domestic',
|
|
relationship: 'tour-confirmed',
|
|
affluenceTier: 'ultra',
|
|
neighborhoods: ['Midtown', 'Chelsea', 'Flatiron', 'Upper East Side', 'SoHo'],
|
|
metaTitle: 'Trans Escort Manhattan — Quinn | NYC Tour',
|
|
metaDescription:
|
|
'Quinn is an upscale independent trans companion visiting Manhattan. In-room appointments from a discreet Midtown base. Screening required; bookings by text.',
|
|
headline: 'Escort in Manhattan',
|
|
intro:
|
|
'Manhattan is the anchor of my summer in New York. I base out of a quiet Midtown hotel, so in-room appointments are easy — a short hop from Grand Central or Penn, and a sensible train from Westchester, Connecticut, or New Jersey.\n\nI keep the calendar unhurried and the screening straightforward. Read the rates page first, then text me with your dates and a little about yourself.',
|
|
experiences: ['Dinner dates', 'Overnights', 'Discreet in-room company'],
|
|
},
|
|
{
|
|
slug: 'brooklyn',
|
|
city: 'Brooklyn',
|
|
region: 'New York',
|
|
fmtyTier: 'domestic',
|
|
relationship: 'tour-aspirational',
|
|
affluenceTier: 'high',
|
|
neighborhoods: ['Williamsburg', 'DUMBO', 'Brooklyn Heights', 'Park Slope'],
|
|
metaTitle: 'Trans Escort Brooklyn — Quinn | NYC Tour',
|
|
metaDescription:
|
|
'Quinn, an upscale independent trans companion, visits Brooklyn on the NYC tour. Relaxed waterfront base around Williamsburg / DUMBO. Screening required; bookings by text.',
|
|
headline: 'Escort in Brooklyn',
|
|
intro:
|
|
'Brooklyn closes out my New York run, and it suits the unhurried, grown-up energy I like to end on. Expect a relaxed base on the waterfront side — Williamsburg and DUMBO — walkable and easy, and a welcome alternative to the Manhattan crush.\n\nDates here firm up as the earlier legs settle. Reach out early and read the rates page before you text.',
|
|
experiences: ['Dinner dates', 'Discreet in-room company'],
|
|
},
|
|
{
|
|
slug: 'queens',
|
|
city: 'Queens',
|
|
region: 'New York',
|
|
fmtyTier: 'domestic',
|
|
relationship: 'tour-aspirational',
|
|
affluenceTier: 'high',
|
|
neighborhoods: ['Long Island City', 'Astoria', 'Forest Hills', 'Flushing'],
|
|
metaTitle: 'Trans Escort Queens — Quinn | NYC Tour',
|
|
metaDescription:
|
|
'Quinn, an upscale independent trans companion, visits Queens on the NYC tour. Quieter than Midtown, minutes from LaGuardia. Screening required; bookings by text.',
|
|
headline: 'Escort in Queens',
|
|
intro:
|
|
'Queens is the calmer middle stretch of my New York run. Long Island City and Astoria are minutes from Midtown but a world quieter, and LaGuardia is right there if you are flying in.\n\nIf a less conspicuous corner of the city suits you better, this is the leg for it. Read the rates page first, then text me your dates.',
|
|
experiences: ['Dinner dates', 'Discreet in-room company'],
|
|
},
|
|
{
|
|
slug: 'the-hamptons',
|
|
city: 'The Hamptons',
|
|
region: 'New York',
|
|
fmtyTier: 'domestic',
|
|
relationship: 'tour-aspirational',
|
|
affluenceTier: 'ultra',
|
|
neighborhoods: ['East Hampton', 'Southampton', 'Sag Harbor', 'Montauk'],
|
|
metaTitle: 'Trans Escort The Hamptons — Quinn | NYC Tour',
|
|
metaDescription:
|
|
'Quinn, an upscale independent trans companion, visits the Hamptons on the NYC tour. Discreet East End company, outcall-friendly. Screening required; bookings by text.',
|
|
headline: 'Escort in The Hamptons',
|
|
intro:
|
|
'A short East End interlude — the kind of low-key, discreet few days the Hamptons are made for. Out here I lean toward outcall: your rental, your hotel, your terms, somewhere between Southampton and Montauk.\n\nDiscretion is the whole point. Read the rates page, then text me to arrange the details.',
|
|
experiences: ['Dinner dates', 'Overnights', 'Discreet outcall company'],
|
|
},
|
|
];
|
|
|
|
const db = openDb(DB_URL);
|
|
try {
|
|
log(COMMIT ? '[seed] COMMIT mode — writing changes' : '[seed] DRY-RUN — no writes (pass --commit to apply)');
|
|
log(`[seed] DB: ${DB_URL.replace(/:\/\/[^@]*@/, '://***@')}`);
|
|
|
|
const existing = await listDestinations(db, { providerSlug: PROVIDER });
|
|
const bySlug = new Map(existing.map((d) => [d.slug, d]));
|
|
|
|
for (const draft of DESTINATIONS) {
|
|
const found = bySlug.get(draft.slug);
|
|
if (found) {
|
|
if (found.linkedTourStop) {
|
|
log(`[dest] ${draft.slug}: exists (id ${found.id}), already linkedTourStop — no change`);
|
|
} else {
|
|
log(`[dest] ${draft.slug}: exists (id ${found.id}) — set linkedTourStop=true`);
|
|
if (COMMIT) await updateDestination(db, found.id, { linkedTourStop: true });
|
|
}
|
|
} else {
|
|
log(`[dest] ${draft.slug}: CREATE (linkedTourStop=true, ${draft.relationship})`);
|
|
if (COMMIT) {
|
|
await createDestination(db, { ...draft, linkedTourStop: true, visibility: 'public', providerSlug: PROVIDER });
|
|
}
|
|
}
|
|
}
|
|
|
|
const stops = await listTourStops(db, { providerSlug: PROVIDER });
|
|
for (const { destSlug, tourCity } of LINKS) {
|
|
const stop = stops.find((s) => s.city === tourCity);
|
|
if (!stop) {
|
|
log(`[link] tour stop "${tourCity}" NOT FOUND — skip (create the stop first)`);
|
|
continue;
|
|
}
|
|
if (stop.destinationSlug === destSlug) {
|
|
log(`[link] stop ${stop.id} (${tourCity}) already → ${destSlug} — no change`);
|
|
continue;
|
|
}
|
|
log(`[link] stop ${stop.id} (${tourCity}) → destination_slug=${destSlug}`);
|
|
if (COMMIT) await updateTourStop(db, stop.id, { destinationSlug: destSlug });
|
|
}
|
|
|
|
log(COMMIT ? '[seed] done — changes applied.' : '[seed] done — dry-run only, nothing written.');
|
|
} finally {
|
|
await db.end();
|
|
}
|