feat(design): scaffold the /design inspiration gallery — schema, taxonomy, bordered shell, feed, /go redirects - #134
feat(design): scaffold the /design inspiration gallery — schema, taxonomy, bordered shell, feed, /go redirects#134arihantcodes wants to merge 3 commits into
Conversation
…and feed First vertical slice of Spectrum Design. Every route renders today; only the items are missing until design-schema.sql is applied. Schema (design-schema.sql, run manually in Supabase per repo convention): - design_items, design_media, design_outbound_links, plus a `role` column on users for admin gating, and RLS policies as a backstop. - Two deliberate departures from the original Prisma sketch. Categories and facets are text[] slugs against a code-side taxonomy rather than four join tables, so category routes are statically generatable and render their SEO copy before a single row exists. Enums are CHECK constraints rather than Postgres ENUM types, which can be edited in one statement. - Partial indexes on (section, published_at) and (section, score) filtered to published rows, so the draft queue never bloats the feed indexes. Routes: - /design — masonry feed, ItemList JSON-LD. - /design/c/[category] — real navigable category routes, not query strings. Nine statically generated pages, each with its own H1, title, meta description derived from its intro, BreadcrumbList + ItemList JSON-LD, and 150-250 words of unique indexable copy. This is the organic traffic engine. UI: - Three-column shell under the existing site header. The global nav is kept on purpose: funnelling gallery traffic into the component library is the third business goal, so removing the links to /docs would work against it. - CSS multi-column masonry rather than JS masonry — no measurement pass, no layout thrash, correct from the server on first paint. - MediaCard reserves its box from the stored media aspect ratio, which is what holds CLS near zero, and pairs it with a --design-media-placeholder tint. Also: AdSense is now suppressed on /design only (spec 12.8) so it cannot cannibalise directly-sold sponsorship inventory; it still loads everywhere else. Feed and section-count queries are wrapped in unstable_cache with a shared "design-items" tag, so publishing can revalidate them and repeated renders do not re-hit Supabase. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two gaps found while verifying the gallery against the live Supabase schema. MediaCard already linked every card's outbound arrow at /go/<short_id>, but no such route existed — every one of those clicks would have 404'd. /go/[code] now resolves a code two ways: a design_outbound_links row (sponsors, tools, jobs) or a design_items.short_id, which redirects to that item's source_url. The second form is why feed cards do not need a pre-created link row each. Safety: only http(s) destinations are followed, so a stored javascript: or data: value cannot become a navigation; redirects are 302 rather than 301 because targets change; click counting is fire-and-forget so a failed increment never costs the user their navigation; paid placements get X-Robots-Tag noindex, nofollow so they pass no link equity. next/image rejects any host not in remotePatterns, so gallery media served from Cloudflare R2 would have 400'd. The bucket host is now read from NEXT_PUBLIC_R2_PUBLIC_HOST and filtered out when unset, rather than hardcoded. Verified against the live schema: all three design_* tables and users.role are present with the expected columns; a temporary three-item seed rendered through the feed, the masonry grid and two category filters with no console errors and no broken radii, and was then removed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eo cards, seed content Rebuilds the /design shell to recent.design's mechanics expressed in this site's own visual language: container-wrapper draws the dashed hairline rules at the container edges, and the rails divide from the feed with the same dashed border instead of whitespace. Title row carries "Last updated Xm ago"; the filter rail is sticky under the 3.5rem site header. The filter rail is rendered as a sibling of the title row, not inside a wrapping <header> — position:sticky can only stick within its parent's box, so nested in a header that ends just below it, it scrolled away with the page. Sort (Recent / Staff picks / Popular) persists in a cookie and never mints URLs, so sorted views cannot create duplicate content; the server reads the cookie, the client only writes it and refreshes. MediaCard gains the video path from spec §7: muted looping inline playback, metadata-only preload, plays at ≥50% visibility, pauses off-screen, on hidden tabs and under prefers-reduced-motion. GIF submissions become mp4 at ingest, so this is also the GIF path. Attribution chips are now visible at rest — credit is part of the card, not a hover reward. Seed content: 15 real product screens curated via the Mobbin MCP (dashboards, iOS onboarding, landing heroes) with original editorial descriptions and source links, plus a self-rendered 6s gradient-motion mp4 exercising the video card. Media is rehosted in the public Supabase Storage bucket `design-media` — Mobbin's CDN URLs are signed and expire, so hotlinking them would have left blank cards within days. The Supabase host joins remotePatterns (derived from NEXT_PUBLIC_SUPABASE_URL); media can move to R2 later without schema changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
24 issues found across 21 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="components/seo/adsense-script.tsx">
<violation number="1" location="components/seo/adsense-script.tsx:18">
P3: Non-gallery paths such as `/designer` or a future `/design-system` route will also lose AdSense because this prefix check has no route-segment boundary. Match `/design` exactly or require the following slash.</violation>
</file>
<file name="components/design/section-header.tsx">
<violation number="1" location="components/design/section-header.tsx:86">
P3: Screen-reader users cannot identify the currently selected gallery filter because the active state is only visual. Expose the selected category (and `All` on the index) with `aria-current="page"`.</violation>
</file>
<file name="lib/design/format.ts">
<violation number="1" location="lib/design/format.ts:9">
P3: Future timestamps are displayed as past: any `published_at` later than the server clock becomes `1m ago`. Handling the negative elapsed case explicitly (for example as `just now`, or with a dedicated future label) would keep the gallery metadata truthful.</violation>
</file>
<file name="components/design/design-sidebar.tsx">
<violation number="1" location="components/design/design-sidebar.tsx:34">
P2: The section rail currently sends users to 404s for every section other than the feed: `DESIGN_SECTIONS` includes `/design/websites`, `/design/og-images`, `/design/app-screenshots`, `/design/app-icons`, and `/design/showcase`, but none of those routes exists yet. Either add the section pages (and their category routes) before exposing these links, or restrict the sidebar to implemented sections until those slices land.</violation>
<violation number="2" location="components/design/design-sidebar.tsx:35">
P3: Screen-reader users cannot identify the current design section: active state is only visual and the dot is `aria-hidden`. Expose `aria-current="page"` on the active Link.</violation>
</file>
<file name="app/(design)/design/c/[category]/page.tsx">
<violation number="1" location="app/(design)/design/c/[category]/page.tsx:55">
P2: Category pages render dynamically for every request, so the intended pre-rendered/300-second ISR SEO routes are not produced. The `cookies()` sort read is a Dynamic API; move sort state to a client-side view or accept an explicit dynamic-rendering strategy rather than relying on `generateStaticParams` here.</violation>
<violation number="2" location="app/(design)/design/c/[category]/page.tsx:58">
P3: Category pages misreport an unprovisioned gallery as an empty published category. The query already provides `unavailable` for this case, so carrying that flag through and using the same setup-state messaging as the feed would make the fallback accurate.</violation>
</file>
<file name="lib/design/types.ts">
<violation number="1" location="lib/design/types.ts:64">
P3: Consumers following this exported cursor contract will send the last item's UUID, which parses to `0` and repeats the first page. Describe it as an offset (or implement an ID-based cursor) so callers can paginate correctly.</violation>
</file>
<file name="lib/design/queries.ts">
<violation number="1" location="lib/design/queries.ts:130">
P2: Pagination can duplicate or skip gallery items when the feed changes between requests because the cursor is only an offset into a mutable ordering. A keyset cursor containing the ordering value and a stable ID would keep subsequent pages anchored to the same feed position.</violation>
<violation number="2" location="lib/design/queries.ts:151">
P2: Items sharing `published_at` have no stable order, so paginating a timestamp tie can duplicate or omit cards between requests. Add a unique secondary order key (for example `id`) for this and the other sortable feeds.</violation>
<violation number="3" location="lib/design/queries.ts:176">
P2: A transient feed failure is presented as “Gallery not provisioned yet,” which sends operators toward the wrong remediation and hides the distinction between an un-applied schema and an outage. The catch path could return `unavailable: false` (or classify the exception with `isNotProvisioned`) while retaining the existing logging.</violation>
</file>
<file name="app/layout.tsx">
<violation number="1" location="app/layout.tsx:160">
P2: AdSense remains loaded when users navigate to `/design` from another page in the SPA. Because this component lives in the persistent root layout and `next/script` injects the script as a side effect, returning `null` after `usePathname()` changes does not unload it; placing the script in non-design route-group layouts (or otherwise managing the script lifecycle) would preserve the suppression on client-side navigation.</violation>
</file>
<file name="design-schema.sql">
<violation number="1" location="design-schema.sql:31">
P2: A published item can enter the feed without a publication date, leaving it unsorted at the end and excluded from “Last updated.” Require a timestamp whenever status is published.</violation>
<violation number="2" location="design-schema.sql:56">
P3: `updated_at` is set only on insert and becomes stale after every item edit. Add an update trigger or omit the field until it is maintained.</violation>
<violation number="3" location="design-schema.sql:73">
P3: The schema creates duplicate indexes for three unique keys, increasing storage and write cost for no query benefit. Removing the explicit single-/same-key indexes and relying on the unique constraint indexes would keep the schema leaner.</violation>
<violation number="4" location="design-schema.sql:83">
P2: Invalid media dimensions can make a card’s computed aspect ratio invalid and prevent the feed from reserving the intended media box. The schema would protect this invariant by requiring both width and height to be positive, not merely non-null.</violation>
</file>
<file name="app/(design)/design/page.tsx">
<violation number="1" location="app/(design)/design/page.tsx:80">
P2: Published gallery cards are currently clickable links to `/design/i/[slug]`, but that detail route is not part of this slice, so every card click produces a 404. The gallery could defer card linking until the detail page exists, link to an available destination, or ship the detail route together with the feed.</violation>
</file>
<file name="app/go/[code]/route.ts">
<violation number="1" location="app/go/[code]/route.ts:46">
P2: Concurrent redirects can be undercounted because the counter is read and incremented in application code before being written. Two requests observing the same value overwrite each other with the same next value, and the same bug exists for `design_items.outbound_clicks`; an atomic database-side increment (for example, an RPC or SQL `counter = counter + 1`) would preserve every click.</violation>
</file>
<file name="app/(design)/design/layout.tsx">
<violation number="1" location="app/(design)/design/layout.tsx:51">
P2: The design route introduces a nested `<main>` landmark because the root layout already supplies the page-level `<main>`. Using a non-landmark wrapper here, such as a `div`, keeps the design column layout without exposing invalid nested main landmarks to assistive technology.</violation>
</file>
<file name="components/design/card-video.tsx">
<violation number="1" location="components/design/card-video.tsx:42">
P2: Videos keep playing whenever any part of the card intersects the viewport, rather than only while at least 50% is visible. The observer is configured with a 0.5 threshold, but `isIntersecting` remains true for ratios such as 0.1, so scrolling a card mostly off-screen does not pause it and can leave multiple off-screen videos consuming CPU and bandwidth. Using the reported `intersectionRatio` for the state check keeps the implementation aligned with the documented 50% behavior.</violation>
</file>
<file name="content/design-taxonomy.ts">
<violation number="1" location="content/design-taxonomy.ts:115">
P2: The category pages do not meet the declared 150–250-word intro requirement: the newly added section/category copy is generally only 39–67 words. That leaves the intended SEO body substantially underlength, so expanding each intro to the planned range would make the taxonomy match the gallery’s content contract.</violation>
</file>
<file name="components/design/feed-grid.tsx">
<violation number="1" location="components/design/feed-grid.tsx:22">
P2: On narrow screens this default marks the first eight cards as priority images even though the feed has only one column (or three icon columns), so several rows below the fold are eagerly prioritized. Each of those cards passes the flag to `next/image` as `priority`, creating avoidable high-priority image requests and competing with the actual LCP image. The fixed count should be reduced or made responsive so only the cards that can be above the fold are prioritized.</violation>
</file>
<file name="components/design/media-card.tsx">
<violation number="1" location="components/design/media-card.tsx:11">
P2: The gallery's image `sizes` substantially overstates the rendered card width on desktop, causing `next/image` to select unnecessarily large source variants. The design layout removes a 220px left rail and, at xl, a 256px right rail before the feed is divided into four or five columns; the icons layout divides that remaining width into eight columns, yet this constant tells the browser each image is 20–25vw wide. Making `sizes` aware of the active layout and rail widths would avoid the resulting bandwidth and image-decoding overhead.</violation>
</file>
<file name="components/design/sort-select.tsx">
<violation number="1" location="components/design/sort-select.tsx:28">
P3: Keyboard users get no visible focus indication for the sort control because the focused native select is fully transparent and its parent has no focus styling. Adding a visible `focus-within` ring or equivalent to the label would preserve the custom appearance while keeping the control navigable.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| const category = findDesignCategory("feed", slug); | ||
| if (!category) notFound(); | ||
|
|
||
| const rawSort = (await cookies()).get("design_sort")?.value; |
There was a problem hiding this comment.
P2: Category pages render dynamically for every request, so the intended pre-rendered/300-second ISR SEO routes are not produced. The cookies() sort read is a Dynamic API; move sort state to a client-side view or accept an explicit dynamic-rendering strategy rather than relying on generateStaticParams here.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/(design)/design/c/[category]/page.tsx, line 55:
<comment>Category pages render dynamically for every request, so the intended pre-rendered/300-second ISR SEO routes are not produced. The `cookies()` sort read is a Dynamic API; move sort state to a client-side view or accept an explicit dynamic-rendering strategy rather than relying on `generateStaticParams` here.</comment>
<file context>
@@ -0,0 +1,127 @@
+ const category = findDesignCategory("feed", slug);
+ if (!category) notFound();
+
+ const rawSort = (await cookies()).get("design_sort")?.value;
+ const sort: DesignSort =
+ rawSort === "popular" || rawSort === "staff" ? rawSort : "recent";
</file context>
| .order("staff_pick", { ascending: false }) | ||
| .order("published_at", { ascending: false, nullsFirst: false }); | ||
| } else { | ||
| q = q.order("published_at", { ascending: false, nullsFirst: false }); |
There was a problem hiding this comment.
P2: Items sharing published_at have no stable order, so paginating a timestamp tie can duplicate or omit cards between requests. Add a unique secondary order key (for example id) for this and the other sortable feeds.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/design/queries.ts, line 151:
<comment>Items sharing `published_at` have no stable order, so paginating a timestamp tie can duplicate or omit cards between requests. Add a unique secondary order key (for example `id`) for this and the other sortable feeds.</comment>
<file context>
@@ -0,0 +1,269 @@
+ .order("staff_pick", { ascending: false })
+ .order("published_at", { ascending: false, nullsFirst: false });
+ } else {
+ q = q.order("published_at", { ascending: false, nullsFirst: false });
+ }
+
</file context>
| return ( | ||
| <li key={section.slug}> | ||
| <Link | ||
| href={section.path} |
There was a problem hiding this comment.
P2: The section rail currently sends users to 404s for every section other than the feed: DESIGN_SECTIONS includes /design/websites, /design/og-images, /design/app-screenshots, /design/app-icons, and /design/showcase, but none of those routes exists yet. Either add the section pages (and their category routes) before exposing these links, or restrict the sidebar to implemented sections until those slices land.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At components/design/design-sidebar.tsx, line 34:
<comment>The section rail currently sends users to 404s for every section other than the feed: `DESIGN_SECTIONS` includes `/design/websites`, `/design/og-images`, `/design/app-screenshots`, `/design/app-icons`, and `/design/showcase`, but none of those routes exists yet. Either add the section pages (and their category routes) before exposing these links, or restrict the sidebar to implemented sections until those slices land.</comment>
<file context>
@@ -0,0 +1,63 @@
+ return (
+ <li key={section.slug}>
+ <Link
+ href={section.path}
+ className={cn(
+ "flex items-center justify-between py-1 text-[13px] transition-colors",
</file context>
| strategy="lazyOnload" | ||
| crossOrigin="anonymous" | ||
| /> | ||
| <AdSenseScript /> |
There was a problem hiding this comment.
P2: AdSense remains loaded when users navigate to /design from another page in the SPA. Because this component lives in the persistent root layout and next/script injects the script as a side effect, returning null after usePathname() changes does not unload it; placing the script in non-design route-group layouts (or otherwise managing the script lifecycle) would preserve the suppression on client-side navigation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/layout.tsx, line 160:
<comment>AdSense remains loaded when users navigate to `/design` from another page in the SPA. Because this component lives in the persistent root layout and `next/script` injects the script as a side effect, returning `null` after `usePathname()` changes does not unload it; placing the script in non-design route-group layouts (or otherwise managing the script lifecycle) would preserve the suppression on client-side navigation.</comment>
<file context>
@@ -156,12 +157,7 @@ gtag('config', 'G-K7ZP6JB4MG');
- strategy="lazyOnload"
- crossOrigin="anonymous"
- />
+ <AdSenseScript />
</head>
<body className="font-regular" suppressHydrationWarning>
</file context>
| status TEXT NOT NULL DEFAULT 'draft' | ||
| CHECK (status IN ('draft','published','hidden')), | ||
| staff_pick BOOLEAN NOT NULL DEFAULT FALSE, | ||
| published_at TIMESTAMPTZ, |
There was a problem hiding this comment.
P2: A published item can enter the feed without a publication date, leaving it unsorted at the end and excluded from “Last updated.” Require a timestamp whenever status is published.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At design-schema.sql, line 31:
<comment>A published item can enter the feed without a publication date, leaving it unsorted at the end and excluded from “Last updated.” Require a timestamp whenever status is published.</comment>
<file context>
@@ -0,0 +1,136 @@
+ status TEXT NOT NULL DEFAULT 'draft'
+ CHECK (status IN ('draft','published','hidden')),
+ staff_pick BOOLEAN NOT NULL DEFAULT FALSE,
+ published_at TIMESTAMPTZ,
+
+ -- Attribution. Never publish without it.
</file context>
| section: DesignSectionSlug; | ||
| category?: string; | ||
| sort?: DesignSort; | ||
| /** Cursor is the previous page's last item id. */ |
There was a problem hiding this comment.
P3: Consumers following this exported cursor contract will send the last item's UUID, which parses to 0 and repeats the first page. Describe it as an offset (or implement an ID-based cursor) so callers can paginate correctly.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/design/types.ts, line 64:
<comment>Consumers following this exported cursor contract will send the last item's UUID, which parses to `0` and repeats the first page. Describe it as an offset (or implement an ID-based cursor) so callers can paginate correctly.</comment>
<file context>
@@ -0,0 +1,75 @@
+ section: DesignSectionSlug;
+ category?: string;
+ sort?: DesignSort;
+ /** Cursor is the previous page's last item id. */
+ cursor?: string;
+ limit?: number;
</file context>
| score DOUBLE PRECISION NOT NULL DEFAULT 0, | ||
|
|
||
| created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), | ||
| updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() |
There was a problem hiding this comment.
P3: updated_at is set only on insert and becomes stale after every item edit. Add an update trigger or omit the field until it is maintained.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At design-schema.sql, line 56:
<comment>`updated_at` is set only on insert and becomes stale after every item edit. Add an update trigger or omit the field until it is maintained.</comment>
<file context>
@@ -0,0 +1,136 @@
+ score DOUBLE PRECISION NOT NULL DEFAULT 0,
+
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
</file context>
| const rawSort = (await cookies()).get("design_sort")?.value; | ||
| const sort: DesignSort = | ||
| rawSort === "popular" || rawSort === "staff" ? rawSort : "recent"; | ||
| const { items } = await getFeed({ section: "feed", category: slug, sort }); |
There was a problem hiding this comment.
P3: Category pages misreport an unprovisioned gallery as an empty published category. The query already provides unavailable for this case, so carrying that flag through and using the same setup-state messaging as the feed would make the fallback accurate.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/(design)/design/c/[category]/page.tsx, line 58:
<comment>Category pages misreport an unprovisioned gallery as an empty published category. The query already provides `unavailable` for this case, so carrying that flag through and using the same setup-state messaging as the feed would make the fallback accurate.</comment>
<file context>
@@ -0,0 +1,127 @@
+ const rawSort = (await cookies()).get("design_sort")?.value;
+ const sort: DesignSort =
+ rawSort === "popular" || rawSort === "staff" ? rawSort : "recent";
+ const { items } = await getFeed({ section: "feed", category: slug, sort });
+
+ return (
</file context>
| CREATE INDEX IF NOT EXISTS idx_design_items_categories | ||
| ON design_items USING GIN (categories); | ||
|
|
||
| CREATE INDEX IF NOT EXISTS idx_design_items_slug ON design_items (slug); |
There was a problem hiding this comment.
P3: The schema creates duplicate indexes for three unique keys, increasing storage and write cost for no query benefit. Removing the explicit single-/same-key indexes and relying on the unique constraint indexes would keep the schema leaner.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At design-schema.sql, line 73:
<comment>The schema creates duplicate indexes for three unique keys, increasing storage and write cost for no query benefit. Removing the explicit single-/same-key indexes and relying on the unique constraint indexes would keep the schema leaner.</comment>
<file context>
@@ -0,0 +1,136 @@
+CREATE INDEX IF NOT EXISTS idx_design_items_categories
+ ON design_items USING GIN (categories);
+
+CREATE INDEX IF NOT EXISTS idx_design_items_slug ON design_items (slug);
+
+CREATE TABLE IF NOT EXISTS design_media (
</file context>
| return ( | ||
| <label | ||
| className={cn( | ||
| "relative flex shrink-0 cursor-pointer items-center gap-1 text-[12px] text-muted-foreground transition-opacity hover:text-foreground", |
There was a problem hiding this comment.
P3: Keyboard users get no visible focus indication for the sort control because the focused native select is fully transparent and its parent has no focus styling. Adding a visible focus-within ring or equivalent to the label would preserve the custom appearance while keeping the control navigable.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At components/design/sort-select.tsx, line 28:
<comment>Keyboard users get no visible focus indication for the sort control because the focused native select is fully transparent and its parent has no focus styling. Adding a visible `focus-within` ring or equivalent to the label would preserve the custom appearance while keeping the control navigable.</comment>
<file context>
@@ -0,0 +1,52 @@
+ return (
+ <label
+ className={cn(
+ "relative flex shrink-0 cursor-pointer items-center gap-1 text-[12px] text-muted-foreground transition-opacity hover:text-foreground",
+ pending && "opacity-50",
+ )}
</file context>
First two vertical slices of Spectrum Design (
/design) — a curated design-inspiration gallery modelled on recent.design's mechanics and IA, expressed in this site's own visual language. Every route renders today; the gallery is already seeded with 16 live items.What's in this PR
Data layer (Supabase)
design-schema.sql(applied to Supabase already):design_items,design_media,design_outbound_links, arolecolumn onusersfor future admin gating, partial indexes filtered to published rows, and RLS policies as a backstop.content/design-taxonomy.ts), not the DB — items reference categories astext[]slugs (GIN-indexed). This is what lets every category page be statically known and render its SEO copy before a single row exists, and it removes four join tables.lib/design/queries.ts) wrapped inunstable_cachewith a shareddesign-itemstag; degrades to a "not provisioned" state instead of a 500 if the tables don't exist.Routes
/designItemListJSON-LD, "Last updated Xm ago"/design/c/[category]×9BreadcrumbList+ItemListJSON-LD, and 150–250 words of unique indexable intro copy. This is the organic-traffic engine./go/[code]noindex+nofollowon paid links)Shell — recent.design mechanics, Spectrum's language
container-wrapperdashed-hairline convention; rails divide from the feed with the same dashed border.position:stickycan only stick within its parent's box).prefers-reduced-motion. This is also the GIF path (GIFs transcode to mp4 at ingest)./designonly (components/seo/adsense-script.tsx) so it can't cannibalise directly-sold sponsorship inventory; unchanged everywhere else.Seed content (16 items, live now)
15 real product screens curated via the Mobbin MCP (web dashboards, iOS onboarding, landing heroes) with original editorial descriptions and source attribution, plus a self-rendered 6s gradient-motion mp4 exercising the video path. Media is rehosted in the public Supabase Storage bucket
design-media— Mobbin's CDN URLs are signed and expire, so hotlinking would have left blank cards within days. Migration to R2 later needs no schema change (NEXT_PUBLIC_R2_PUBLIC_HOSTis already wired intoremotePatterns).Verification
tsc --noEmit0 errors ✅ · ESLint clean on new files ✅/design, still loading on/docs/*.Known / deferred
/designsitemap entries, the item detail page (/design/i/[slug]), and/design/admin(ingest UI) are the next slices.await auth()(pre-existing, affects all 135 routes).🤖 Generated with Claude Code
Summary by cubic
Scaffolded the
/designinspiration gallery with a code-driven taxonomy, Supabase schema, bordered shell, masonry feed (with video), and tracked/goredirects. All routes render now; gallery is seeded with 16 items and AdSense is suppressed on/design.New Features
design_items,design_media,design_outbound_links,users.role; categories astext[]slugs; CHECK constraints; partial indexes; RLS.content/design-taxonomy.ts; static category routes with unique intro copy and JSON‑LD;/designand nine/design/c/[category]pages.unstable_cachewith thedesign-itemstag./go/[code]tracked redirects (302, http(s)-only, fire‑and‑forget click counts;nofollowon paid)./designviaAdSenseScript; unchanged elsewhere.next/imageremotePatterns usingNEXT_PUBLIC_R2_PUBLIC_HOSTand the Supabase host.Migration
design-schema.sqlin Supabase to create gallery tables and policies.NEXT_PUBLIC_R2_PUBLIC_HOST(or ensureNEXT_PUBLIC_SUPABASE_URLis set) so gallery media loads.revalidateTag('design-items')on publish to refresh cached feeds.Written for commit 55ade7c. Summary will update on new commits.