# Blog Prismic-style Layout Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add a sticky secondary blog nav (category tabs + search), a featured + thematic `/blog` index, and richer cards (author + placeholder avatar) — Prismic-style — keeping `/blog` static.

**Architecture:** A client `BlogSubNav` (tabs as next-intl Links + a search input) rendered sticky on `/blog` and article pages. A client `BlogIndexView` shows featured + per-category sections by default and a Fuse filtered grid when `?q`/`?category` is set (search-param logic in client islands under `<Suspense>` to keep `/blog` SSG). Author avatar path is centralized in `site.ts`.

**Tech Stack:** Next 16 App Router (RSC + client islands), next-intl, fuse.js, Tailwind tokens.

**Spec:** `docs/superpowers/specs/2026-06-05-blog-prismic-layout-design.md`. Branch `feat/blog-prismic-layout` (created from `dev`).

**Verification model:** No unit-test framework. Verify with `bun run lint` + `bun run build` (route list must show `/[locale]/blog` as SSG `●`) + browser checks via next-devtools on **port 3001** when available (else inspect prerendered HTML under `.next/server/app/<locale>/blog.html`). Format only touched files (`bunx oxfmt <files>`). Commit per task.

> **Pre-req:** per `AGENTS.md`, verify installed Next/next-intl APIs before writing code. Reuse existing patterns: `src/components/blog/PostCard.tsx`, `BlogSearch.tsx` (Fuse logic to fold in), `Breadcrumbs.tsx`, `src/lib/blog/index.ts` (`getAllPosts`, `categorySlug`, `categoryIdFromSlug`, `localePrefix`, `articlePath`), `src/config/blog/{categories,authors}.ts`, `src/config/site.ts`, `messages/{en,fr}.json` (`BlogPage`).

---

## File Structure

**Create:**
- `public/blog/authors/placeholder.webp` — placeholder author avatar.
- `src/components/blog/BlogSubNav.tsx` — sticky secondary nav (client).
- `src/components/blog/BlogIndexView.tsx` — default (featured + thematic) + filtered views (client).
- `src/components/blog/FeaturedPosts.tsx` — featured zone (presentational).

**Modify:**
- `src/config/site.ts` — add `blog.authorAvatar`.
- `src/config/blog/authors.ts` — add `avatar` (from `site.ts`).
- `src/lib/blog/schema.ts` — add `featured?` to frontmatter + `PostMeta`.
- `src/lib/blog/index.ts` — add `getFeatured`.
- `src/components/blog/PostCard.tsx` — add date + author (name + avatar).
- `src/components/blog/PostLayout.tsx` — avatar in byline + `BlogSubNav` at top.
- `src/app/[locale]/blog/page.tsx` — render `BlogSubNav` + `BlogIndexView`.
- `src/app/[locale]/[category]/[slug]/page.tsx` — (only if BlogSubNav not placed via PostLayout) — placed via PostLayout, so no change needed beyond passing nothing.
- `messages/en.json`, `messages/fr.json` — `BlogPage` strings.

**Delete:** `src/components/blog/BlogSearch.tsx` (logic folded into `BlogIndexView`).

---

## Task 1: Avatar config + placeholder asset

**Files:** Modify `src/config/site.ts`, `src/config/blog/authors.ts`; Create `public/blog/authors/placeholder.webp`

- [ ] **Step 1: Add the avatar config to site.ts**

In `src/config/site.ts`, add a `blog` key inside the `site` object (before the closing `} as const;`, after `contact`):
```ts
  blog: {
    // TODO: replace with a real author avatar
    authorAvatar: "/blog/authors/placeholder.webp",
  },
```

- [ ] **Step 2: Create the placeholder avatar asset**

Generate a neutral 160×160 brand-tinted placeholder webp (no network needed) with sharp:
```bash
mkdir -p public/blog/authors
cat > /tmp/avatar.cjs <<'JS'
const sharp = require("sharp");
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160">
  <rect width="160" height="160" fill="#e5e2e1"/>
  <circle cx="80" cy="62" r="30" fill="#8e8e93"/>
  <rect x="34" y="100" width="92" height="54" rx="0" fill="#8e8e93"/>
</svg>`;
sharp(Buffer.from(svg)).webp({ quality: 82 }).toFile("public/blog/authors/placeholder.webp")
  .then(() => console.log("ok"));
JS
NODE_PATH="$(pwd)/node_modules" node /tmp/avatar.cjs
```
Expected: `ok`; file exists at `public/blog/authors/placeholder.webp`.

- [ ] **Step 3: Wire avatar into authors config**

In `src/config/blog/authors.ts`, import `site` and add `avatar` to the `advance` author:
```ts
import { site } from "@/config/site";

export const BLOG_AUTHORS = {
  advance: {
    name: "Advance Manufacturers",
    avatar: site.blog.authorAvatar,
  },
} as const;
```
(Keep the existing `AuthorId`/`isAuthorId` exports.)

- [ ] **Step 4: Build + commit**

Run: `bun run build` → green.
```bash
bunx oxfmt src/config/site.ts src/config/blog/authors.ts
git add src/config/site.ts src/config/blog/authors.ts public/blog/authors/placeholder.webp
git commit -m "feat(blog): author avatar config (placeholder in site.ts)"
```

---

## Task 2: `featured` frontmatter + `getFeatured`

**Files:** Modify `src/lib/blog/schema.ts`, `src/lib/blog/index.ts`

- [ ] **Step 1: Add `featured` to the schema**

In `src/lib/blog/schema.ts`, add to `frontmatterSchema` (after `draft`):
```ts
  featured: z.boolean().optional().default(false),
```
`PostMeta`/`Post` derive from `Frontmatter`, so `featured` flows through automatically.

- [ ] **Step 2: Add `getFeatured` to the data layer**

In `src/lib/blog/index.ts`, add (after `getPostsByCategory`):
```ts
export function getFeatured(locale: Locale, n = 3): PostMeta[] {
  const all = getAllPosts(locale); // already date-desc, drafts excluded
  const flagged = all.filter((p) => p.featured);
  return (flagged.length > 0 ? flagged : all).slice(0, n);
}
```

- [ ] **Step 3: Mark the seed article featured (so the zone is non-empty)**

In `content/blog/jewellery-sourcing-thailand/en.mdx` and `.../fr.mdx`, add to the frontmatter:
```yaml
featured: true
```

- [ ] **Step 4: Build + smoke + commit**

Run: `bun run build` → green (schema accepts `featured`).
```bash
bunx oxfmt src/lib/blog/schema.ts src/lib/blog/index.ts
git add src/lib/blog/schema.ts src/lib/blog/index.ts content/blog/jewellery-sourcing-thailand
git commit -m "feat(blog): featured frontmatter + getFeatured"
```

---

## Task 3: i18n strings

**Files:** Modify `messages/en.json`, `messages/fr.json`

- [ ] **Step 1: Add to `BlogPage` (en.json)**

Merge into the existing `BlogPage` object:
```json
"allCategories": "All",
"featured": "Featured",
"viewAll": "View all {category}"
```
(`search.allCategories` already exists for the chip wording; `allCategories` here is the sub-nav "All" tab. If `search.allCategories` is identical, reuse it instead and skip adding a duplicate — pick one key and use it consistently.)

- [ ] **Step 2: Add to `BlogPage` (fr.json)**

```json
"allCategories": "Tous",
"featured": "À la une",
"viewAll": "Voir tout : {category}"
```

- [ ] **Step 3: Build + commit**

Run: `bun run build` (regenerates typed messages) → green.
```bash
bunx oxfmt messages/en.json messages/fr.json
git add messages/en.json messages/fr.json
git commit -m "feat(blog): i18n for sub-nav tabs + featured/view-all"
```

---

## Task 4: `BlogSubNav` (sticky secondary nav)

**Files:** Create `src/components/blog/BlogSubNav.tsx`

> The site header is `fixed top-0` (see `src/components/Header.tsx`, `p-3` ≈ 4rem tall). Make the sub-nav `sticky top-16` so it sticks just below the header.

- [ ] **Step 1: Write the component**

```tsx
"use client";

import { type Locale, useLocale, useTranslations } from "next-intl";
import { useRouter, useSearchParams } from "next/navigation";
import { type FormEvent, useState } from "react";
import { Link, useRouter as useIntlRouter } from "@/i18n/navigation";
import { BLOG_CATEGORIES, type CategoryId, categoryIdFromSlug } from "@/config/blog/categories";
import { cn } from "@/lib/utils";

function tabClass(active: boolean) {
  return cn(
    "whitespace-nowrap py-4 text-xs font-semibold uppercase tracking-[0.15em] transition-colors",
    active ? "text-tertiary" : "text-foreground hover:text-tertiary",
  );
}

export default function BlogSubNav({ activeCategory }: { activeCategory?: CategoryId }) {
  const t = useTranslations("BlogPage");
  const locale = useLocale() as Locale;
  const params = useSearchParams();
  const intlRouter = useIntlRouter();
  const [query, setQuery] = useState("");

  // Active tab: explicit prop (article pages) wins; else derive from ?category (/blog).
  const fromParam = params.get("category");
  const active: CategoryId | "all" =
    activeCategory ?? (fromParam ? (categoryIdFromSlug(fromParam, locale) ?? "all") : "all");

  function onSearch(e: FormEvent) {
    e.preventDefault();
    const q = query.trim();
    intlRouter.push({ pathname: "/blog", query: q ? { q } : {} });
  }

  return (
    <nav
      aria-label={t("indexTitle")}
      className="sticky top-16 z-40 border-b border-outline-soft bg-background/95 backdrop-blur"
    >
      <div className="mx-auto flex max-w-7xl items-center gap-6 px-6 lg:px-8">
        <ul className="flex flex-1 items-center gap-6 overflow-x-auto">
          <li>
            <Link href="/blog" className={tabClass(active === "all")}>
              {t("allCategories")}
            </Link>
          </li>
          {BLOG_CATEGORIES.map((c) => (
            <li key={c.id}>
              <Link
                href={{ pathname: "/blog", query: { category: c.slug[locale as "en" | "fr"] } }}
                className={tabClass(active === c.id)}
              >
                {t(`categories.${c.id}`)}
              </Link>
            </li>
          ))}
        </ul>
        <form onSubmit={onSearch} className="hidden shrink-0 sm:block">
          <input
            type="search"
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            placeholder={t("search.placeholder")}
            aria-label={t("search.placeholder")}
            className="w-44 rounded-none border-0 border-b border-brushed-steel bg-transparent py-2 text-sm text-foreground placeholder:text-brushed-steel focus:border-foreground focus:outline-none"
          />
        </form>
      </div>
    </nav>
  );
}
```

> Note: `useRouter`/`useSearchParams` from `next/navigation` import is unused if `useIntlRouter` covers navigation — remove whichever you don't use to keep lint clean. The next-intl `useRouter` (`@/i18n/navigation`) handles localized push. Verify `intlRouter.push({ pathname, query })` is supported by the installed next-intl; if not, fall back to `next/navigation` `useRouter().push(\`${prefix}/blog?q=...\`)` using `localePrefix`.

- [ ] **Step 2: Build + lint + commit**

Run: `bun run build` then `bun run lint` (component clean; it's consumed under Suspense in later tasks).
```bash
bunx oxfmt src/components/blog/BlogSubNav.tsx
git add src/components/blog/BlogSubNav.tsx
git commit -m "feat(blog): sticky BlogSubNav (category tabs + search)"
```

---

## Task 5: Enrich `PostCard` (date + author + avatar)

**Files:** Modify `src/components/blog/PostCard.tsx`

- [ ] **Step 1: Add the byline row**

Add imports at top:
```tsx
import { BLOG_AUTHORS } from "@/config/blog/authors";
```
Inside the card body, after the reading-time line, add a byline (author avatar + name + date). Use `Intl.DateTimeFormat(post.locale, { dateStyle: "medium" })`. Example block to insert before the closing of the text container:
```tsx
      <div className="mt-4 flex items-center gap-3">
        <Image
          src={BLOG_AUTHORS[post.author as keyof typeof BLOG_AUTHORS].avatar}
          alt=""
          width={28}
          height={28}
          className="size-7 rounded-full object-cover"
        />
        <span className="text-xs text-on-surface-variant">
          {BLOG_AUTHORS[post.author as keyof typeof BLOG_AUTHORS].name} ·{" "}
          {new Intl.DateTimeFormat(post.locale, { dateStyle: "medium" }).format(new Date(post.date))}
        </span>
      </div>
```
(`Image` is already imported in PostCard. Avatar `alt=""` because the adjacent name is the text alternative. `rounded-full` is allowed — avatars are photographic/organic, an accepted exception to the sharp-corner rule, like jewellery imagery.)

- [ ] **Step 2: Build + commit**

Run: `bun run build` → green; `bun run lint` clean.
```bash
bunx oxfmt src/components/blog/PostCard.tsx
git add src/components/blog/PostCard.tsx
git commit -m "feat(blog): PostCard author + avatar + date byline"
```

---

## Task 6: `FeaturedPosts` + `BlogIndexView`

**Files:** Create `src/components/blog/FeaturedPosts.tsx`, `src/components/blog/BlogIndexView.tsx`; Delete `src/components/blog/BlogSearch.tsx`

- [ ] **Step 1: `FeaturedPosts.tsx` (presentational)**

```tsx
import { useTranslations } from "next-intl";
import PostCard from "@/components/blog/PostCard";
import type { PostMeta } from "@/lib/blog/schema";

export default function FeaturedPosts({ posts }: { posts: PostMeta[] }) {
  const t = useTranslations("BlogPage");
  if (posts.length === 0) return null;
  return (
    <section>
      <h2 className="text-sm font-semibold uppercase tracking-[0.2em] text-tertiary">
        {t("featured")}
      </h2>
      <div className="mt-6 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
        {posts.map((p) => (
          <PostCard key={p.postId} post={p} />
        ))}
      </div>
    </section>
  );
}
```

- [ ] **Step 2: `BlogIndexView.tsx` (client; default vs filtered)**

```tsx
"use client";

import Fuse from "fuse.js";
import { type Locale, useLocale, useTranslations } from "next-intl";
import { useSearchParams } from "next/navigation";
import { useMemo } from "react";
import FeaturedPosts from "@/components/blog/FeaturedPosts";
import PostCard from "@/components/blog/PostCard";
import { Link } from "@/i18n/navigation";
import { BLOG_CATEGORIES, type CategoryId, categoryIdFromSlug } from "@/config/blog/categories";
import type { PostMeta } from "@/lib/blog/schema";

const THEMATIC_LIMIT = 3;

export default function BlogIndexView({ posts }: { posts: PostMeta[] }) {
  const t = useTranslations("BlogPage");
  const locale = useLocale() as Locale;
  const params = useSearchParams();
  const query = params.get("q")?.trim() ?? "";
  const categoryParam = params.get("category");
  const activeCategory = categoryParam ? categoryIdFromSlug(categoryParam, locale) : null;

  const fuse = useMemo(
    () => new Fuse(posts, { keys: ["title", "description"], threshold: 0.3, ignoreLocation: true }),
    [posts],
  );

  const isFiltered = query.length > 0 || activeCategory !== null;

  const filtered = useMemo(() => {
    if (!isFiltered) return [];
    const base = query ? fuse.search(query).map((r) => r.item) : posts;
    return activeCategory ? base.filter((p) => p.category === activeCategory) : base;
  }, [isFiltered, query, activeCategory, fuse, posts]);

  if (isFiltered) {
    return filtered.length === 0 ? (
      <p className="text-on-surface-variant">{t("search.noResults")}</p>
    ) : (
      <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
        {filtered.map((p) => (
          <PostCard key={p.postId} post={p} />
        ))}
      </div>
    );
  }

  // Default browse view: featured + per-category thematic sections.
  const featured = (() => {
    const flagged = posts.filter((p) => p.featured);
    return (flagged.length > 0 ? flagged : posts).slice(0, 3);
  })();

  return (
    <div className="space-y-20">
      <FeaturedPosts posts={featured} />
      {BLOG_CATEGORIES.map((c) => {
        const inCat = posts.filter((p) => p.category === c.id);
        if (inCat.length === 0) return null;
        return (
          <section key={c.id}>
            <div className="flex items-baseline justify-between gap-4">
              <h2 className="font-semibold text-2xl tracking-tight text-foreground sm:text-3xl">
                {t(`categories.${c.id}`)}
              </h2>
              <Link
                href={{ pathname: "/blog", query: { category: c.slug[locale as "en" | "fr"] } }}
                className="shrink-0 text-xs font-semibold uppercase tracking-[0.12em] text-foreground transition-colors hover:text-tertiary"
              >
                {t("viewAll", { category: t(`categories.${c.id}`) })} →
              </Link>
            </div>
            <div className="mt-6 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
              {inCat.slice(0, THEMATIC_LIMIT).map((p) => (
                <PostCard key={p.postId} post={p} />
              ))}
            </div>
          </section>
        );
      })}
    </div>
  );
}
```

- [ ] **Step 3: Delete the superseded BlogSearch**

```bash
grep -rn "BlogSearch" src || echo "no refs"
```
After confirming `/blog/page.tsx` is updated in Task 7 to use `BlogIndexView` (do Task 7 Step 1 first if grep still shows the page importing BlogSearch), then:
```bash
git rm src/components/blog/BlogSearch.tsx
```

- [ ] **Step 4: Build + lint + commit**

```bash
bunx oxfmt src/components/blog/FeaturedPosts.tsx src/components/blog/BlogIndexView.tsx
git add -A
git commit -m "feat(blog): FeaturedPosts + BlogIndexView (featured/thematic + filtered)"
```

---

## Task 7: Wire `/blog` page + remove old chips

**Files:** Modify `src/app/[locale]/blog/page.tsx`

- [ ] **Step 1: Replace BlogSearch usage with BlogSubNav + BlogIndexView**

In `src/app/[locale]/blog/page.tsx`:
- Change the import `import BlogSearch from "@/components/blog/BlogSearch";` to:
```tsx
import BlogIndexView from "@/components/blog/BlogIndexView";
import BlogSubNav from "@/components/blog/BlogSubNav";
```
- Render `BlogSubNav` (sticky, full-bleed) ABOVE the `max-w-7xl` content container, and replace `<BlogSearch posts={posts} />` with `<BlogIndexView posts={posts} />` (both inside `<Suspense>`). The section's top padding should sit under the header; keep `pt-32`. Structure:
```tsx
  return (
    <>
      {/* JSON-LD scripts stay as-is */}
      <Suspense>
        <BlogSubNav />
      </Suspense>
      <section className="overflow-hidden bg-background pb-24 pt-8 sm:pb-32">
        <div className="mx-auto max-w-7xl px-6 lg:px-8">
          <Breadcrumbs items={[{ label: t("breadcrumbHome"), href: "/" }, { label: t("indexTitle") }]} />
          <h1 className="mt-8 font-semibold text-4xl tracking-tight text-foreground sm:text-5xl">
            {t("indexTitle")}
          </h1>
          <p className="mt-6 max-w-2xl text-lg leading-relaxed text-on-surface-variant">
            {t("indexIntro")}
          </p>
          <div className="mt-12">
            <Suspense>
              <BlogIndexView posts={posts} />
            </Suspense>
          </div>
        </div>
      </section>
    </>
  );
```
> The page is currently a single `<section>`; wrap in a fragment so `BlogSubNav` (sticky, full-width) is a sibling above it. Keep the two JSON-LD `<script>` blocks. Reduce the section top padding to `pt-8` since the sub-nav now provides the header offset (the sub-nav itself is `sticky top-16`; the page still scrolls under the fixed header — keep enough top space so the H1 isn't hidden: if the H1 hides under the header on first paint, restore `pt-32`).

- [ ] **Step 2: Build + verify SSG + browser**

Run: `bun run build`
Expected: green; `/[locale]/blog` still `●` (SSG). Browser (port 3001 if up): `/blog` shows sticky sub-nav (tabs + search), default featured + thematic sections; clicking a tab → `/blog?category=` shows the filtered grid + that tab active; typing in sub-nav search + Enter → `/blog?q=` filtered; scrolling keeps the sub-nav stuck under the header. If port 3001 is down, inspect `.next/server/app/en/blog.html`: featured + thematic sections + sub-nav links present in prerendered HTML.

- [ ] **Step 3: Format + commit**

```bash
bunx oxfmt "src/app/[locale]/blog/page.tsx"
git add "src/app/[locale]/blog/page.tsx"
git commit -m "feat(blog): /blog uses BlogSubNav + BlogIndexView"
```

---

## Task 8: Article page — sub-nav + avatar byline

**Files:** Modify `src/components/blog/PostLayout.tsx`

- [ ] **Step 1: Add BlogSubNav + avatar to PostLayout**

In `src/components/blog/PostLayout.tsx`:
- Add imports:
```tsx
import Image from "next/image"; // if not already imported
import BlogSubNav from "@/components/blog/BlogSubNav";
import { BLOG_AUTHORS } from "@/config/blog/authors";
```
- Render `BlogSubNav` with the article's category active, as the first element (above the `<article>`), wrapped in `<Suspense>`:
```tsx
  return (
    <>
      <Suspense>
        <BlogSubNav activeCategory={post.category} />
      </Suspense>
      <article ...>
        ...
```
  (Add `import { Suspense } from "react";`.) `BlogSubNav` is sticky; PostLayout's existing top padding stays.
- In the byline row, add the author avatar before the author name:
```tsx
        <span className="inline-flex items-center gap-2">
          <Image
            src={BLOG_AUTHORS[post.author as keyof typeof BLOG_AUTHORS].avatar}
            alt=""
            width={24}
            height={24}
            className="size-6 rounded-full object-cover"
          />
          {t("byAuthor", { author: authorName })}
        </span>
```
  (Replace the existing `<span>{t("byAuthor", ...)}</span>` with the version above; `authorName` is already computed in PostLayout.)

- [ ] **Step 2: Build + verify + commit**

Run: `bun run build` → green; articles SSG. Browser: open an article — sticky sub-nav with the article's category highlighted; clicking another tab → `/blog?category=`; avatar shows in the byline.
```bash
bunx oxfmt src/components/blog/PostLayout.tsx
git add src/components/blog/PostLayout.tsx
git commit -m "feat(blog): article sub-nav + avatar byline"
```

---

## Task 9: Full verification

- [ ] **Step 1: Lint** — `bun run lint` → clean for `src/components/blog/*`, `src/app/[locale]/blog/**`, `src/lib/blog/*`, `src/config/*`. Fix any unused imports (e.g. leftover `next/navigation` in BlogSubNav) or `key`/dynamic-key issues.

- [ ] **Step 2: Build** — `bun run build` → green; `/[locale]/blog` SSG (`●`), articles SSG, feeds + sitemap static. No `BlogSearch` references remain (`grep -rn BlogSearch src` → none).

- [ ] **Step 3: Browser (port 3001, next-devtools if up):**
  - `/blog`: sticky sub-nav (tabs + search) stays under header on scroll; default = featured + thematic; cards show avatar + author + date.
  - `/blog?category=sourcing`: filtered grid, Sourcing tab active.
  - sub-nav search "thai" + Enter → `/blog?q=thai` filtered.
  - article: sticky sub-nav with its category active; avatar in byline; "view all" + tab links work.
  - `/fr/blog`: localized tabs + labels.
  - Confirm `/blog` HTML (prerendered) contains featured + thematic links (SEO).

- [ ] **Step 4: Commit fixes**
```bash
git add -A && git commit -m "test(blog): verification fixes"
```

---

## Self-Review

**Spec coverage:** sub-nav sticky tabs + search (Task 4, 7, 8) ✓ · featured + thematic index (Task 6) ✓ · filtered view via `?q`/`?category` keeping `/blog` static (Task 6 client + Suspense) ✓ · enriched cards author+avatar+date (Task 5) ✓ · article avatar + sub-nav (Task 8) ✓ · `featured` frontmatter + `getFeatured` (Task 2) ✓ · avatar placeholder centralized in `site.ts` (Task 1) ✓ · i18n (Task 3) ✓ · removed duplicate chips / BlogSearch (Task 6–7) ✓.

**Risks flagged inline:** next-intl `useRouter().push({pathname, query})` support (Task 4 — fallback to `next/navigation` + `localePrefix`); sticky `top-16` vs real header height + H1 hiding under fixed header (Task 7 — adjust padding); `BlogSearch` deletion ordering vs `/blog` page import (Task 6 Step 3 / Task 7 Step 1).

**Type consistency:** `PostMeta` (incl. new `featured`) used across `getFeatured`/`FeaturedPosts`/`BlogIndexView`/`PostCard`; `categoryIdFromSlug`/`BLOG_CATEGORIES`/`categorySlug` from `@/config/blog/categories`; `BLOG_AUTHORS[id].avatar` typed via the `as const` authors map.

**YAGNI:** newsletter deferred; no "popular"; no pagination; no URL writeback beyond the sub-nav's `?q`/`?category`.
