Catalogue.gallery is a discovery platform designed around the idea that artists should own their digital environments. Instead of hosting artwork, the platform acts as a navigation layer that lets viewers explore each artist's personal website directly through an embedded browsing interface. This creates a fluid discovery experience while preserving the uniqueness of each artist's creative world. The result is a system where curation and independence coexist, turning the platform into a portal for navigating distributed digital art ecosystems.
- Frontend: React + TypeScript + Vite
- Content + review backend: Sanity Studio (
/studio) - Public API endpoints: Cloudflare Pages Functions (
/functions/api) - Email delivery: Resend
- Inbox/reply workflow: ProtonMail (via
reply_to) - Editorial/blog content: Sanity
postdocuments (served at/blog/:slug)
- Architecture:
docs/ARCHITECTURE.md - API contract:
docs/API.md - Quality and verification policy:
docs/QUALITY.md - Deployment runbook:
docs/DEPLOYMENT.md - Product roadmap notes:
docs/ROADMAP.md
- Security policy and reporting process:
SECURITY.md - Please report vulnerabilities privately through GitHub private vulnerability reporting when possible.
- Public bug reports: GitHub Issues
- New issue form: Report a bug or request a feature
- Private vulnerability reporting: GitHub Security Advisories
- Contribution guide:
CONTRIBUTING.md
Repository documentation, issues, and pull request discussions are maintained in English so bug reports, review notes, and operational guidance stay searchable and reusable.
- Public releases are published on GitHub Releases.
- Starting with
v0.1.0, the project uses Semantic Versioning for repository releases. - Human-readable release notes are tracked in
CHANGELOG.md.
- Install dependencies:
npm installcd studio && npm install
- Create local env file:
cp .env.example .env.local
- Run app:
npm run dev
- Run Sanity Studio:
cd studio && npm run dev
Use .env.local for local secrets. Never commit .env or .env.local.
Required app/server env vars:
VITE_SANITY_PROJECT_IDVITE_SANITY_DATASETSANITY_WRITE_TOKEN(server-side write token for submit endpoint)RESEND_API_KEYRESEND_FROM_EMAIL(example:CATALOGUE <apply@catalogue.gallery>)RESEND_REPLY_TO(set to your ProtonMail address)PUBLIC_BASE_URL(example:https://catalogue.gallery)VITE_CF_WEB_ANALYTICS_TOKEN(optional; only needed for manual beacon mode)WEBHOOK_SHARED_SECRET(required; webhook requests are rejected without it)EMAIL_ENCRYPTION_KEY(32-byte base64 key used to encrypt contact emails before storing in Sanity)SANITY_PROJECT_ID(optional server override)SANITY_DATASET(optional server override)
Cloudflare binding (not an env var):
CONTACTS_DB(D1 binding used for private contact storage)
- User submits via
/submit. POST /api/submitcreates a Sanityartistorgallerydocument with:status: "pending"- stores email in private D1 and writes
contactIdto Sanity
- You review in Sanity Studio:
- pending list:
In Review (New) - for fast workflow use document actions:
Approve & NotifyDecline & Notify
- for declines choose
rejectionReasonCodeand optionally addrejectionReasondetails - for approvals optionally add
approvalMessage
- pending list:
- Sanity webhook calls
POST /api/webhook. /api/webhooksends approval/decline email through Resend.
- Recommended mode: keep applicant emails in private Cloudflare D1 (
submission_contacts) and only storecontactIdin Sanity. - Submit endpoint now requires
CONTACTS_DB; new submissions fail closed if D1 binding is missing. - Webhooks decrypt server-side only (requires
EMAIL_ENCRYPTION_KEY).
- Create database:
npx wrangler d1 create catalogue-private-contacts
- Add binding in
wrangler.toml(replacedatabase_id):[[d1_databases]]binding = "CONTACTS_DB"database_name = "catalogue-private-contacts"database_id = "<your-database-id>"
- Apply schema:
npx wrangler d1 execute catalogue-private-contacts --remote --file=./migrations/001_submission_contacts.sql
- Deploy app again so Functions can use the binding.
- Migrate old encrypted emails from Sanity into D1 and remove legacy
emailfields:cd studio && npm run migrate:contacts:d1
- Blog/interview/article entries live in Sanity as
postdocuments. - Keep slugs stable to preserve URLs:
/blog/:slug. npm run buildrunspostbuild, which generates static meta pages indist/blog/<slug>/index.html.- Social OG/Twitter images are sourced from Sanity
post.thumbnail(with legacy fallback). - In-article entity linking still auto-links published names to profile routes.
Run once (idempotent by createOrReplace on post-<slug> ids):
npm run migrate:articlesThis imports current local articles using the same slug ids, preserving existing /blog/... links.
/content-lab is the password-protected editorial workspace for draft generation, review, and publishing.
Flow:
- Select an artist from the directory (or generate for a random one)
- Optionally hit Research to scrape the artist's website and cache a factual bio in Sanity (
contentBiofield) - Hit Generate:
Server keyuses the deployment'sGROK_API_KEYBring your own keykeeps an xAI key in browser session storage only and calls xAI directly from the browser
- Review the draft, edit inline, then publish directly to Sanity
Content types:
| Type | Voice | Length |
|---|---|---|
| Article | Cultural criticism, third person, structured sections | 700–950 words |
| Blog | Short editorial, first person, one strong observation | 250–380 words |
| Wildcard | Deep-dive on a collection, concept, or moment | 450–700 words |
API endpoints (functions/api/):
| Route | Method | Purpose |
|---|---|---|
/api/artists |
GET |
Public, read-only directory feed for published profiles |
/api/client-errors |
POST |
Collects browser runtime errors into Cloudflare logs |
/api/content-generate |
POST |
Grok-4 with live web + X search → drafts → D1 |
/api/content-scrape |
POST |
Scrapes artist website → Claude Haiku summarizes → saves to Sanity contentBio |
/api/content-publish |
POST |
Publishes approved draft from D1 → Sanity post |
/api/content-drafts |
GET |
Lists drafts stored in D1 |
/api/content-drafts |
POST |
Creates or updates draft records in D1 |
/api/content-drafts |
DELETE |
Deletes a draft |
/api/content-artists |
GET |
Lists published artists/galleries for Content Lab picker |
/api/content-upload-image |
POST |
Uploads image assets to Sanity for draft publishing |
Full endpoint details: docs/API.md
Additional env vars required:
GROK_API_KEY— xAI API key (Grok-4, primary generation model)CLAUDE_API_KEY— Anthropic API key (Claude Haiku, used for website summarization only)CONTENT_LAB_PASSWORD— auth password for the private/content-labroute
Use Resend to send mail and ProtonMail to receive replies:
- In Resend:
- verify
catalogue.gallerydomain with DNS records - set sender as
CATALOGUE <apply@catalogue.gallery>
- verify
- In Cloudflare Pages env vars:
RESEND_API_KEYRESEND_FROM_EMAIL=CATALOGUE <apply@catalogue.gallery>RESEND_REPLY_TO=yourname@proton.me
- In Sanity webhook settings:
- URL: your deployed webhook endpoint
- Trigger: document create/update for
artistandgallery - Configure the shared secret header using
WEBHOOK_SHARED_SECRET
This keeps deliverability high (Resend) while all replies route back to ProtonMail.
npm test # Run once
npm test -- --run # Run once (CI)
npm run test:watch # Watch mode
npm run verify # Lint + typecheck + tests19 unit tests across 3 suites:
linkUtils.ts— entity linking, double-link prevention, sort-by-length correctnessuseArtistshook — loading state, Sanity mapping, fetch error pathsuseArticleshook — post mapping, legacy fallback, thumbnail normalization
Verification policy, static analysis, and the new-functionality test policy are documented in docs/QUALITY.md.
- Build app:
npm run build - Migrate local article archive to Sanity posts:
npm run migrate:articles - Migrate old artists:
npx -y tsx scripts/migrate-artists.ts - Purge artists:
npx -y tsx scripts/purge-artists.ts - Move legacy encrypted contacts into D1:
cd studio && npm run migrate:contacts:d1
- CI runs lint, type check, tests, and build on pull requests to
main. - Branch protection is a GitHub repository setting (manual), not a code-delivered feature.
- Manual checklist is documented in
docs/DEPLOYMENT.md.
- Contribution guide:
CONTRIBUTING.md - License:
LICENSE(MIT)