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 slugsKeep 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