← Corpus / astro-knots / spec
Image Catalog and ImageKit Hosting for Arthouse
A catalog-and-resolver content model that keeps the arthouse-site code repo public while client photography lives on ImageKit — including a privacy tier that lets NSFW originals exist in the catalog (for lineage) without ever appearing in rendered output.
- Path
- specs/Image-Catalog-and-ImageKit-Hosting-for-Arthouse.md
- Authors
- Michael Staton
- Augmented with
- Claude Code (Opus 4.7)
- Tags
- Arthouse-Site · Content-Model · ImageKit · Image-Privacy · Astro-Content-Collections · Sveltia-CMS
Image Catalog and ImageKit Hosting for Arthouse
Status: Draft (v0.0.0.1)
Codename: arthouse-site
Parent: [[Polish-Pass-for-Arthouse-Site]] · [[Maintain-an-Image-Heavy-Portfolio-Site]]
Related: [[AI-Photo-to-Illustration-Transform-for-Arthouse]]
1. Why this spec exists
The arthouse-site code repo is public. The client’s photography includes work that is sensitive, sometimes NSFW. Holding image bytes in the repo is therefore not an option — even an old commit on main would leak.
ImageKit (already provisioned for the team) becomes the asset host. The repo holds metadata only: a catalog of records describing every image, with the resolver layer assembling URLs at render time. Galleries reference records by slug; if a file gets renamed or moved on ImageKit, only the record’s imagekit_path changes — every gallery pointing at it keeps working.
Privacy is a property of the record, not of a directory. A record can describe an NSFW original (for the artist’s own catalog completeness and for tracking which images have been transformed by [[AI-Photo-to-Illustration-Transform-for-Arthouse]]) while being unreachable from any public route.
2. The three layers
2.1 Image records
Path: src/content/images/<slug>.md
One markdown file per image. Body is optional (long-form caption / story).
---
title: "Kiara · Audrey Hepburn"
slug: studio-kiara-audrey-hepburn # explicit; matches filename
kind: photograph # photograph | artwork | ai-rendered
privacy: public # public | gated | private
nsfw: false
imagekit_path: /portfolio/studio/kiara-audrey-hepburn.jpg
aspect_ratio: "2:3" # for layout without CLS
alt: "Portrait styled as Audrey Hepburn — black gown, pearl strand, soft studio light"
caption: "Studio session. Paris, October 2025."
palette_hint: gothic # dark | bright | gothic | girly
tags:
- Portraits
- Studio
- Styled
series: studio-sessions
shoot_location: Paris
date_taken: 2025-10-15
pricing_tier: standard
source_image: ~ # for ai-rendered only
ai_model: ~ # for ai-rendered only
---
For AI-rendered safe outputs, kind: ai-rendered and source_image: points to the slug of the private source record — preserving lineage without exposing the source.
For private NSFW originals (catalog presence only, never rendered):
---
title: "Private · 2025-10-15 frame 007"
slug: private-2025-10-15-frame-007
kind: photograph
privacy: private
nsfw: true
imagekit_path: /private/2025-10-15/frame-007.jpg
alt: "(private)"
date_taken: 2025-10-15
---
2.2 Galleries reference by slug
src/content/galleries/portrait/studio-sessions.md:
---
title: Studio Sessions
description: Portrait photography — styled sessions with creative direction
cover_image: studio-kiara-audrey-hepburn
images:
- studio-kiara-audrey-hepburn
- studio-kiara-steampunk
- studio-kiara-glitterbug
featured: true
sort_order: 1
tags: [Portraits, Studio, Styled]
date_created: 2026-04-21
---
No paths. Rename an ImageKit asset → update the record’s imagekit_path → every gallery follows.
2.3 Resolver library + component
src/lib/imagekit.ts — pure functions:
getImageRecord(slug)— fetches the content collection entryimagekitUrl(record, transformations)— builds${IMAGEKIT_URL_ENDPOINT}${imagekit_path}?tr=w-800,q-80,f-webpimageSrcSet(record, widths)— produces a srcset string for responsive<img>filterPublic(records)— stripsprivacy !== "public"(the load-bearing privacy gate)
src/components/basics/CatalogImage.astro — takes a slug prop, resolves the record, renders a responsive <img> with srcset, sized via aspect_ratio to prevent layout shift. If the record’s privacy doesn’t match the render context, renders nothing (silently in production, with a comment in dev).
3. Privacy rules (the single load-bearing contract)
privacy value | Renders in public routes? | Renders in gated routes? | Use case |
|---|---|---|---|
public | ✅ | ✅ | Default for everything safe-for-web |
gated | ❌ | ✅ | Behind a PUBLIC_DECK_CODE-style gate; future phase |
private | ❌ | ❌ | Originals that exist only for catalog/lineage |
nsfw is orthogonal — a marker for the CMS UI to warn before showing a thumbnail. It does not change render behavior. A public + nsfw: false AI-rendered output is rendered freely; the private + nsfw: true source it descended from is not.
Build-time enforcement: every component that pulls images must go through filterPublic() (or the future filterForContext(ctx)). Direct reads of getCollection('images') without filtering are an anti-pattern — surface in code review.
4. ImageKit folder convention
ik.imagekit.io/<account>/
portfolio/
studio/ # public
landscape/
portrait/
ai-rendered/ # public — outputs from the AI-transform pipeline
private/ # signed-URL-only, never linked from public site
2025-10-15/
2025-11-02/
Records under privacy: private MUST have imagekit_path starting /private/. A build-time invariant check is cheap to add and prevents misclassification.
Signing for the private/ folder is out of scope for Phase 1. We’ll implement signed-URL access if/when we build a gated review surface for the artist herself.
5. Sveltia CMS integration
public/admin/config.yml collection update:
imagescollection fields swapsrc(path) forimagekit_path(relative path under ImageKit endpoint)- Add:
kind(select),privacy(select, defaultpublic),nsfw(boolean),aspect_ratio(string),palette_hint(select),series(string),source_image(relation widget pointing back atimagescollection) - The CMS no longer uploads image bytes to
public/images/— uploads go directly to ImageKit through their dashboard or via a future custom widget. Phase 1: she uploads to ImageKit, copies the path, pastes into the CMS form.
The galleries collection field for images becomes a relation to image-record slugs rather than a free-form file picker.
6. Environment variables
.env.example adds:
IMAGEKIT_URL_ENDPOINT=https://ik.imagekit.io/your-account-id
# Server-side only, for future signed-URL work on the /private/ folder:
# IMAGEKIT_PRIVATE_KEY=...
The endpoint URL is non-secret (it ends up in client-side <img> src attributes anyway). The private key is server-only and Phase 2.
7. Migration of existing content
One gallery exists today: src/content/galleries/portrait/studio-sessions.md. It references three local JPEG paths. Phase 1 deliverable:
- Create three image records in
src/content/images/for the existing assets (uses placeholder ImageKit paths — client uploads later) - Rewrite the gallery’s
images:list to reference slugs - Keep the local
public/images/portfolio/*.jpegfiles in place during transition (so the dev server still has something to render until the ImageKit account holds the real assets) - The
CatalogImagecomponent supports alocal_srcfallback for the transition period — flag emitted in console when the fallback fires
8. What this spec does NOT do
- Signed URLs for
/private/(Phase 2) - Per-image color extraction (the
palette_hintis a manual hint, not derived) - AI-transform automation (separate exploration)
- Image upload UI (uses ImageKit dashboard for now)
9. References
- [[Polish-Pass-for-Arthouse-Site]] — parent
- [[Maintain-an-Image-Heavy-Portfolio-Site]] — grandparent (original site spec)
- [[AI-Photo-to-Illustration-Transform-for-Arthouse]] — produces the
kind: ai-renderedrecords this catalog tracks