Skip to content

Repository files navigation

Art Archive

Personal art collection site — simplified Pixiv-like experience. Built with Astro 7 hybrid mode on Cloudflare Pages. Markdown content, D1-backed likes, Pagefind search, S3 image pipeline.

Project Structure

├── public/                  # Static assets (favicon, fonts)
├── src/
│   ├── assets/              # Image manifest, logo
│   ├── components/          # Astro components (reusable UI)
│   │   ├── ArtworkCard.astro
│   │   ├── ArtworkGallery.astro
│   │   ├── Header.astro / Footer.astro / SEO.astro
│   │   ├── LikeButton.astro / LikeButton.client.js
│   │   ├── PrevNextNav.astro / TagList.astro
│   │   ├── SearchUI.astro / SectionDivider.astro
│   ├── content/
│   │   └── artworks/        # .mdx artwork files (content collection)
│   ├── layouts/
│   │   └── Layout.astro     # HTML shell (head, header, footer)
│   ├── lib/
│   │   ├── artworks.ts      # getCollection, paginate, allTags, adjacent
│   │   ├── image-url.ts     # getImageUrl, getImageManifest
│   │   └── ip.ts            # getClientIP, hashIp (HMAC-SHA-256)
│   ├── pages/
│   │   ├── index.astro      # Home page (intro + latest 12)
│   │   ├── gallery/[...page].astro
│   │   ├── tag/[tag].astro  # Tag filter + tag/index.astro listing
│   │   ├── artwork/[slug].astro
│   │   ├── search.astro
│   │   ├── rss.xml.ts       # RSS 2.0 feed
│   │   └── api/like.ts      # POST /api/like
│   ├── styles/
│   │   └── global.css       # Tailwind v4 @theme tokens, prose styles
│   └── content.config.ts    # Zod schema for artworks collection
├── d1/migrations/           # D1 SQL migrations
├── scripts/
│   ├── upload.mjs           # Image pipeline (sharp + S3)
│   └── setup-hooks.sh       # Git hooks setup
├── .githooks/pre-push       # Warns about un-uploaded images
├── wrangler.toml            # Cloudflare Pages + D1 config
├── astro.config.mjs
└── package.json

Quick Start

npm install
npm run dev           # http://localhost:4321

Creating Artwork

Drop a .mdx file in src/content/artworks/:

---
title: "Sunset Over Harbor"
date: 2025-12-20
tags: [landscape, digital, warm]
image:
  path: ./sunset.png          # local file before upload, S3 key after
  alt: "Warm sunset over a quiet harbor"
---

Description text (markdown supported).

The dev server picks up new .mdx files automatically. No restart needed.

A single artwork can have one image or multiple images (but not both, and not neither):

images:
  - path: artworks/page1.abc12345.webp
    alt: "First page"
  - path: artworks/page2.def67890.webp
    alt: "Second page"

Tags are normalized: trim().toLowerCase() applied automatically by the Zod schema.

Image Pipeline

Images are stored on S3-compatible object storage and served via a separate domain (e.g. img.carbonc.cc). The pipeline processes local images, uploads to S3, and records metadata.

Creating an Artwork (full workflow)

1. Write the .mdx file in src/content/artworks/, referencing local images:

---
title: "Sunset Over Harbor"
date: 2025-12-20
tags: [landscape, digital]
image:
  path: ./sunset.png          # local file next to the .mdx
  alt: "Warm sunset over a quiet harbor"
---

2. Upload images to S3 with the pipeline script:

# Single artwork
node scripts/upload.mjs --artwork src/content/artworks/sunset.mdx

# All artworks (batch)
node scripts/upload.mjs --all

The script will:

  • Read the local image file from disk
  • Convert to lossless WebP (full-size) + 800px lossy thumbnail
  • Generate a content-hash filename: artworks/{12-char-hex}.webp
  • Upload both to S3
  • Rewrite the path field in the .mdx frontmatter to the S3 key
  • Record dimensions & size in src/assets/image-manifest.json

After upload, your frontmatter becomes:

image:
  path: artworks/abc123def456.webp   # rewritten by the script
  alt: "Warm sunset over a quiet harbor"

3. Commit and push. Dev server picks up new .mdx automatically; production needs a rebuild + deploy.

Controlling thumbnails

By default, every image gets an 800px-wide thumbnail for gallery cards. You can disable this per image:

image:
  path: ./icon.png
  alt: "A small icon"
  thumb: false    # skip thumbnail — this image is too small

Thumbnails are also auto-skipped for images ≤ 800px wide (upload script detects this).

Script reference

Flag Purpose
--artwork <path> Process a single .mdx file
--all Process all .mdx files in src/content/artworks/
--force Re-process images that are already S3 keys (use after changing pipeline settings)

Skip logic: Images whose path already points to artworks/{12-hex}.webp are skipped (already uploaded). Images whose local file doesn't exist on disk are skipped (already processed, file removed). Use --force to override.

Required env vars

Set in .env (or export in shell):

PUBLIC_IMAGE_BASE_URL=https://img.example.com
S3_ENDPOINT=https://s3.example.com
S3_BUCKET=art
S3_REGION=auto
S3_ACCESS_KEY=xxx
S3_SECRET_KEY=xxx

Likes

Likes use Cloudflare D1 with HMAC-SHA-256 IP hashing.

Local development

Local dev server renders like buttons as static (0 likes) — no D1 needed. The button is visible but non-functional.

Testing likes end-to-end

Requires wrangler + D1:

wrangler d1 create art-archive-db
# Copy database_id into wrangler.toml
wrangler d1 execute art-archive-db --local --file=d1/migrations/0001_create_likes.sql
wrangler dev

API

POST /api/like
Content-Type: application/json
{"slug": "sunset"}

→ 200 { "count": 1, "userLiked": true }
→ 409 { "error": "Already liked" }
→ 503 { "error": "Service unavailable" }   (D1 not configured)

Like deduplication: one like per artwork per IP hash. IP read from CF-Connecting-IP header (Cloudflare proxy).

Search

Pagefind indexes the static build output. Search is not functional in dev mode — it shows a friendly message instead.

npm run build       # runs: astro build && npx pagefind --site dist/client
npm run preview     # serve dist and test search locally

RSS

/rss.xml — RSS 2.0 with Atom self-link. Enclosures included when image manifest has metadata for the first image.

Design Tokens

All colors are CSS custom properties defined in src/styles/global.css under @theme:

Role Value
Page bg #f8fafc
Card bg #ffffff
Chip bg #f1f5f9
Border #e2e8f0
Text primary #596683
Text secondary #7188a7
Text muted #94a3b8
Accent purple #c4b5fd
Accent blue #bfdafc
Accent pink #f6c0d7
Accent teal #c3ecf0

Font: 寒蝉全圆体 (ChillRoundF) via chinese-font CDN, falling back to system-ui.

Routes

Route Page
/ Home (latest 12)
/gallery/, /gallery/2 Paginated gallery
/artwork/[slug]/ Artwork detail
/tag/ All tags listing
/tag/[tag]/ Tagged artworks
/search/ Pagefind search
/rss.xml RSS 2.0 feed
POST /api/like Like action

Deployment

Prerequisites

  1. Cloudflare account with Pages and D1 access
  2. Wrangler CLI: npm install -g wrangler (or use npx wrangler)
  3. S3-compatible object storage (self-hosted or cloud)
  4. Node.js >= 22.12.0

Setup

# 1. Login to Cloudflare
npx wrangler login

# 2. Create D1 database
npx wrangler d1 create art-archive-db
# → Copy the database_id into wrangler.toml

# 3. Configure wrangler.toml
#    Fill in database_id, then add public env vars to [vars] section:
#      PUBLIC_SITE_URL, PUBLIC_IMAGE_BASE_URL,
#      S3_ENDPOINT, S3_BUCKET, S3_REGION

# 4. Configure .env (build-time vars)
#    Copy .env.example and fill in real values.
#    PUBLIC_SITE_URL and PUBLIC_IMAGE_BASE_URL are read
#    by astro.config.mjs at build time via loadEnv.

# 5. Run local migration (test locally)
npx wrangler d1 execute art-archive-db --file=d1/migrations/0001_create_likes.sql

# 6. Build and deploy as Cloudflare Worker
npm run build
npx wrangler deploy --config dist/server/wrangler.json

# 7. Set secrets for production
npx wrangler secret put SALT
npx wrangler secret put S3_ACCESS_KEY
npx wrangler secret put S3_SECRET_KEY

# 8. Run remote migration (production database)
npx wrangler d1 execute art-archive-db --file=d1/migrations/0001_create_likes.sql --remote

Git hooks (optional)

bash scripts/setup-hooks.sh

The pre-push hook scans staged .mdx files for local image paths and warns if any haven't been uploaded yet. It does not block the push.

Tech Stack

  • Astro 7 + Tailwind CSS v4 (Vite plugin)
  • @astrojs/cloudflare (output: static, deployed as Cloudflare Worker)
  • @astrojs/mdx (content collections)
  • Pagefind (post-build search index)
  • Cloudflare D1 (likes)
  • sharp + gray-matter + @aws-sdk/client-s3 (image pipeline)

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages