Skip to content

refactor: remove ASCII/LQIP image-preview experiment, add spinner#443

Merged
mergify[bot] merged 2 commits into
mainfrom
refactor/441-remove-image-preview
Jun 1, 2026
Merged

refactor: remove ASCII/LQIP image-preview experiment, add spinner#443
mergify[bot] merged 2 commits into
mainfrom
refactor/441-remove-image-preview

Conversation

@julianken

@julianken julianken commented Jun 1, 2026

Copy link
Copy Markdown
Owner

Diagrams

N/A — pure removal + render-path simplification; no new architecture, data flow, or state machine to diagram. The before/after is "ASCII halftone + LQIP blur layered components" → "two stacked next/image variants with a CSS spinner behind them."

Summary

Removes the unfinished ASCII/LQIP image-preview experiment and replaces the loading state with a standard, zero-JS CSS spinner.

Why: Hero/post images carried a two-layer placeholder experiment — an ASCII-art halftone preview plus a legacy LQIP blur — wired through ThemeAwareHero → ImageWithAsciiPreview → AsciiPreviewImage with an OptimizedImage blur fallback. It was a lqip → preview cutover that never finished, so every Media upload still dual-wrote both fields on beforeChange. The only user-visible effect was a "weird ASCII thing." This deletes both sides instead of finishing the cutover (supersedes #246).

What changed:

  • Simplified (kept — core): ThemeAwareHero.tsx now renders the two theme variants directly via next/image (dark:hidden / hidden dark:block, fill, object-cover, focal-point style, relative + aspect-ratio wrapper preserved). Removed the unused ASCII_LENGTH import and the hasUsablePreview dual-read. Added a CSS-only centered spinner rendered behind the two images, so the opaque loaded image paints over it — the component stays a server component (no onLoad, no client boundary).
  • Deleted (experiment-only): OptimizedImage, ImageWithAsciiPreview, AsciiPreviewImage; lib/preview/encode, lib/lqip/encode, lib/hooks/generate-preview, lib/hooks/generate-lqip; the "ASCII PREVIEW" globals.css block (incl. --preview-color + reduced-motion sub-block); scripts/backfill-{lqip,previews}.ts + their package.json scripts; .github/workflows/backfill-previews.yml; the experiment unit/e2e tests.
  • Media model: dropped the beforeChange lqip+preview hooks, the lqip field, and the preview group from Media.ts; regenerated payload-types.ts (not hand-edited); updated CONTENT_MODEL.md.
  • DB: new forward migration 20260601_000000_drop_media_lqip_preview.ts runs DROP COLUMN IF EXISTS "lqip","preview_color","preview_ascii" on media (existing add-column migrations untouched; registered in migrations/index.ts). Completes chore(media): file lqip→preview cutover follow-up #246's "drop the lqip column."
  • Hero-glow decoupled: post-detail page now passes "var(--accent)" instead of preview?.color || "var(--accent)" (the .hero-filter / .hero-glow CSS is kept).
  • Kept: sharp (shared by payload.config.ts + convert-hero-webp.ts + generate-rss-icon.ts), PostCard, TextureOverlay/scanline/card-* chrome, theme-switch E2E.

Screenshots

Deferred to post-deploy verification (agreed with maintainer). This repo has no per-PR preview env (Cloud Run deploys on merge), and .env.local targets a remote DB the migration-bearing dev server must not run against (drizzle push would drop lqip/preview_* remotely pre-merge). The loaded UI is visually unchanged — this removes a transient ASCII/blur placeholder and adds a CSS spinner; spinner presence is asserted in ThemeAwareHero.test.tsx. Post-deploy, the orchestrator captures light+dark home+post against production to confirm no hero regression and correct spinner z-order in both themes.

Test plan

  • pnpm typecheck — PASS (clean, no errors).
  • pnpm lint (ESLint + lint:adp sketch/reference/affiliate/changelog checks) — PASS (0 errors; only pre-existing warnings).
  • pnpm test:unit — PASS (43 files, 662 tests). ThemeAwareHero.test.tsx updated: dropped the blurDataURL/lqip + ASCII dual-read assertions, kept variant / aspect-ratio / focal-point assertions, added a spinner-present assertion.
  • pnpm build — PASS (Compiled successfully, 67/67 pages, exit 0). DB-connection noise at static-export time is the sandbox having no Postgres, not a code defect.
  • pnpm test:e2e — DEFERRED-CI: requires a Postgres-seeded env (Playwright globalSetup seeds the DB; webServer boots the app). The 4 E2E shards run in CI + are enforced by Mergify before merge. The kept hero specs (theme-aware-hero.spec.ts, homepage-hero.spec.ts) assert theme switching / hero presence and are unaffected.
  • Acceptance grep returns nothing: grep -rn "ascii-preview\|AsciiPreview\|ImageWithAsciiPreview\|OptimizedImage\|generate-lqip\|generate-preview\|lib/lqip\|lib/preview\|data-loaded\|--preview-color\|blurDataURL\|hero-loading" src tests scripts .github — CLEAN (no matches).

Plan reference

Out of plan — remove failed image-preview experiment; replace with a standard spinner.


Closes #441
Closes #246

🤖 Generated with Claude Code

The hero/post images carried a two-layer loading-state experiment: an
ASCII-art halftone preview plus a legacy LQIP blur placeholder, wired
through ThemeAwareHero -> ImageWithAsciiPreview -> AsciiPreviewImage with
an OptimizedImage blur fallback. It was a lqip -> preview cutover that
never finished, so every Media upload still dual-wrote both fields on
beforeChange. The only user-visible result was a "weird ASCII thing" and
a meaningful pile of components, hooks, scripts, CSS, and tests to carry.

Rather than finish the cutover (#246), delete both sides of the
experiment and replace the loading state with a standard, zero-JS CSS
spinner rendered behind the two stacked theme variants — the opaque
object-cover image paints over it once the active variant loads. This
keeps ThemeAwareHero a server component (no onLoad, no client boundary)
while cutting the render path down to plain next/image.

The dark/light theme-aware hero, sharp, the hero-glow/hero-filter chrome,
and the theme-switch E2E coverage are all core infrastructure and are
kept; only the experiment is removed. A new forward migration drops the
now-orphaned lqip/preview_color/preview_ascii columns (existing add-column
migrations are left untouched), and payload-types is regenerated from the
trimmed Media config. This supersedes #246 (we delete both sides instead
of cutting over) and completes its "drop the lqip column" item.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@julianken julianken marked this pull request as ready for review June 1, 2026 17:43

@julianken-bot julianken-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE

Clean, complete removal of the ASCII/LQIP image-preview experiment, replaced by a zero-JS CSS spinner. The deletion is internally consistent across all eight surfaces it touches — component, CSS, collection schema, generated types, forward migration, content-model doc, workflow, and scripts — with no orphaned consumers. Every acceptance check I ran this turn is green. One minor design-tradeoff SUGGESTION; not a defect.

Verification ledger (commands I ran this turn — not trusting the PR body)

Check Result
Acceptance grep over src tests scripts .github exit 1, no matches
pnpm typecheck @ b1c2662 clean, exit 0
pnpm test:unit 43 files / 662 tests pass
pnpm lint + lint:adp 0 errors, 19 pre-existing warnings
Migration coherence drops exactly lqip/preview_color/preview_ascii; names match the add-column migrations; existing bodies untouched; timestamp strictly last
payload-types.ts lqip + preview removed from Media and MediaSelect — clean regen, not hand-edit
Orphan consumers none
Workflow/script coherence (backfill-*) none dangling
CSS contract (.hero-spinner, border-border, border-t-accent) all resolvable via @theme inline / :root / .dark
HEAD stable unchanged b1c2662

GitHub CI green on every verdict-relevant check (ESLint, TypeScript, Vitest, Next.js Build, Analyze Bundle, CodeQL).

Findings

1. [scope / R10] Large diff (122+/2055−, 27 files), but ~95% is mechanical deletion of one self-contained experiment; the load-bearing new code is a single component + one CSS rule + one migration. Reviewed in full; scope appropriate.

2. [SUGGESTION] src/components/ThemeAwareHero.tsx — the animate-spin spinner renders behind both <Image> variants and never unmounts, so the CSS animation runs perpetually on every hero and every PostCard thumbnail even after the opaque image fully covers it. Cost is small (GPU-composited transform), but it's an always-running animation on a permanently-invisible element across the listing grid. Pausing it post-load would require the onLoad/client boundary this PR deliberately removed — a conscious zero-JS tradeoff, not a bug. Noting for the next reader of the CSS.

Checked, NOT findings

  • z-order — correct. .hero-spinner { z-index: 0 } (DOM-first) vs next/image fill at z-index: auto (DOM-later): both stack level 0, painted in tree order → images paint over the spinner.
  • "Infinite spinner on image 404" — NOT a regression (R7). The old OptimizedImage path also had no onError handler. Pre-existing, excluded.
  • R16 screenshots — deferral accepted on merits. No per-PR preview env; the migration-bearing dev server would drizzle push against a remote DB and drop the columns pre-merge (a real hazard); loaded-UI delta is near-zero; spinner presence is unit-asserted.
  • R11 no injection; R15 no mermaid.
  • R13 (migrations/**, .github/workflows/**) and R14 (spinner className) shadow checks both PASS — repo uses Tailwind v4 @theme inline, not the grep's assumed paths; verified the substance (every new class resolves).

Bottom line

Complete, coherent deletion with green typecheck/lint/unit and a verified migration. Schema-drop, type regen, and content-model doc all consistent; spinner z-order behaves as documented. APPROVE.

@julianken-bot (opus, fresh context) · same-tier author caveat noted (R12)

@julianken

Copy link
Copy Markdown
Owner Author

@Mergifyio queue

@mergify

mergify Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Merge Queue Status

  • Entered queue2026-06-01 17:46 UTC · Rule: default
  • Checks passed · in-place
  • Merged2026-06-01 17:52 UTC · at 5c089c0b86c65ba33e552b0d97c36a3666b47502 · squash

This pull request spent 5 minutes 56 seconds in the queue, including 5 minutes 16 seconds running CI.

Required conditions to merge
  • #approved-reviews-by >= 1 [🛡 GitHub branch protection]
  • github-review-decision = APPROVED [🛡 GitHub branch protection]
  • any of [🛡 GitHub branch protection]:
    • check-success = ESLint
    • check-neutral = ESLint
    • check-skipped = ESLint
  • any of [🛡 GitHub branch protection]:
    • check-success = TypeScript
    • check-neutral = TypeScript
    • check-skipped = TypeScript
  • any of [🛡 GitHub branch protection]:
    • check-success = Vitest
    • check-neutral = Vitest
    • check-skipped = Vitest
  • any of [🛡 GitHub branch protection]:
    • check-success = Next.js Build
    • check-neutral = Next.js Build
    • check-skipped = Next.js Build
  • any of [🛡 GitHub branch protection]:
    • check-success = Analyze Bundle
    • check-neutral = Analyze Bundle
    • check-skipped = Analyze Bundle
  • any of [🛡 GitHub branch protection]:
    • check-success = CodeQL Analysis
    • check-neutral = CodeQL Analysis
    • check-skipped = CodeQL Analysis
  • any of [🛡 GitHub branch protection]:
    • check-success = E2E Shard 1/4
    • check-neutral = E2E Shard 1/4
    • check-skipped = E2E Shard 1/4
  • any of [🛡 GitHub branch protection]:
    • check-success = E2E Shard 2/4
    • check-neutral = E2E Shard 2/4
    • check-skipped = E2E Shard 2/4
  • any of [🛡 GitHub branch protection]:
    • check-success = E2E Shard 3/4
    • check-neutral = E2E Shard 3/4
    • check-skipped = E2E Shard 3/4
  • any of [🛡 GitHub branch protection]:
    • check-success = E2E Shard 4/4
    • check-neutral = E2E Shard 4/4
    • check-skipped = E2E Shard 4/4

@mergify mergify Bot added the queued label Jun 1, 2026
@mergify mergify Bot merged commit 61510a1 into main Jun 1, 2026
13 checks passed
@mergify mergify Bot deleted the refactor/441-remove-image-preview branch June 1, 2026 17:52
@mergify mergify Bot removed the queued label Jun 1, 2026
mergify Bot pushed a commit that referenced this pull request Jun 1, 2026
* feat: load-aware hero spinner + glitch-in reveal (#444)

The hero spinner introduced in #441 was a zero-JS, CSS-only element that
animated perpetually behind every thumbnail (raised in the #443 review),
and it gave no signal that loading had finished. This makes the hero
image-pair load-aware so the spinner reflects real load state and a
genuine load is rewarded with a quick glitch-in.

- Extract the image+spinner into a small `"use client"` child,
  `HeroImagePair`, rendered by `ThemeAwareHero`. The server component keeps
  ownership of the Media null-guard, `toRelativeSrc`, focal-point math, and
  the aspect-ratio wrapper, so the public props and both call sites
  (PostCard, posts/[slug]) are unchanged.
- Cached case: a `useLayoutEffect` probes `img.complete` on the visible
  variant synchronously before paint (mirroring FadeReveal), so a warm
  image appears instantly — no spinner flash, no glitch.
- Real-load case: the spinner shows while the visible variant streams in;
  its `onLoad` removes the spinner and applies the existing `.glitch-reveal`
  animation (reused, not reinvented) for the reveal. The hidden variant's
  onLoad is ignored so the visible variant drives the state.
- `prefers-reduced-motion: reduce` is honored by the global media query in
  globals.css, which collapses `.glitch-reveal` to ~0ms: the image just
  appears, spinner still allowed.
- The glitch class lives on a theme-agnostic `absolute inset-0` wrapper
  holding both variants — it reads correctly in light and dark, provides the
  positioning context the `fill` images need, and adds no layout shift. The
  variants stay free of opacity/transition classes so the View Transitions
  theme crossfade (#53) is not degraded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: cover hidden-variant onLoad guard + drop dead eslint-disable (#444)

The visible/hidden discrimination keys off getComputedStyle(img).display, but
jsdom never applies the Tailwind stylesheet so both variants reported
display:"inline" in tests — the handleLoad early-return guard was never
exercised and visibleImg() passed only by first-element fallthrough. Stub
getComputedStyle so the dark variant reports display:"none" and assert load
on the hidden variant is a no-op while load on the visible variant removes the
spinner + applies .glitch-reveal (verified load-bearing via mutation check).

Also drop the dead react-hooks/exhaustive-deps disable on the useLayoutEffect:
the effect already has an explicit [] dep array, so the directive suppressed
nothing and ESLint flagged it as an unused directive in a changed file. The
adjacent set-state-in-effect disable is kept (legitimate). No runtime change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor: remove ASCII/LQIP image-preview experiment, add spinner chore(media): file lqip→preview cutover follow-up

2 participants