# Phase 11: Embeddable Blog Post Component

This phase details how to create a highly flexible "Blog Post" component for the Puck CMS. This component allows editors to select a specific blog post from the database and embed the full article (header, hero image, rich text, author box, related articles) seamlessly into any page (Homepage, About Us, Custom Pages).

**Universal Architecture Note:** 
To ensure this works instantly when deployed on new websites, the API route dynamically reads `SITE_ID` from the environment variables rather than hardcoding a domain name.

---

## 1. Universal Editor API Route

We need a backend endpoint that the Puck editor can call to get a list of available blog posts for the current site.

**Create `app/api/editor/blog-posts/route.ts`:**
```typescript
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { blogPosts } from '@/shared/schema';
import { eq, and, desc } from 'drizzle-orm';

// Universal Site ID resolution
const SITE_ID = (process.env.RANKTRACTION_SITE_ID || process.env.SITE_ID || 'example.com').replace(/"/g, '');

export const dynamic = 'force-dynamic';

export async function GET() {
  try {
    const rows = await db
      .select({
        title: blogPosts.title,
        slug: blogPosts.slug,
      })
      .from(blogPosts)
      .where(
        and(
          eq(blogPosts.siteId, SITE_ID),
          eq(blogPosts.isActive, true)
        )
      )
      .orderBy(desc(blogPosts.publicationDate));

    return NextResponse.json({ posts: rows });
  } catch (error) {
    console.error('Failed to fetch blog posts:', error);
    return NextResponse.json({ error: 'Failed to fetch blog posts' }, { status: 500 });
  }
}
```

---

## 2. Custom Editor Selector Field

Puck CMS allows us to build custom React fields for the right-side properties panel. We will build a dropdown that consumes our new API.

**Create `components/editor/BlogPostSelector.tsx`:**
```tsx
"use client";

import React, { useEffect, useState } from 'react';

interface BlogPostSelectorProps {
  value: string;
  onChange: (value: string) => void;
}

export function BlogPostSelector({ value, onChange }: BlogPostSelectorProps) {
  const [posts, setPosts] = useState<{ title: string; slug: string }[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchPosts() {
      try {
        const res = await fetch('/api/editor/blog-posts');
        if (res.ok) {
          const data = await res.json();
          setPosts(data.posts || []);
        }
      } catch (err) {
        console.error('Failed to fetch blog posts', err);
      } finally {
        setLoading(false);
      }
    }
    fetchPosts();
  }, []);

  if (loading) {
    return <div className="text-sm text-gray-500">Loading blog posts...</div>;
  }

  return (
    <div className="flex flex-col gap-2">
      <select
        className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
        value={value || ''}
        onChange={(e) => onChange(e.target.value)}
      >
        <option value="" disabled>Select a blog post...</option>
        {posts.map((post) => (
          <option key={post.slug} value={post.slug}>
            {post.title}
          </option>
        ))}
      </select>
    </div>
  );
}
```

---

## 3. Editor Preview Placeholder

Because the Puck canvas is rendered purely on the client-side, fetching full database articles in real-time inside the editor can cause performance and hydration issues. Instead, we use a lightweight preview box for the editor canvas.

**Create `components/sections/BlogPostEditor.tsx`:**
```tsx
import React from 'react';
import { FileText } from 'lucide-react';

interface BlogPostEditorProps {
  slug?: string;
  anchorId?: string;
}

export function BlogPostEditor({ slug, anchorId }: BlogPostEditorProps) {
  return (
    <section className="w-full py-8 bg-gray-50/50 border-y border-dashed border-gray-200" id={anchorId || undefined}>
      <div className="container max-w-4xl mx-auto px-4">
        <div className="bg-white rounded-xl shadow-sm border border-gray-200 p-8 text-center flex flex-col items-center justify-center min-h-[300px]">
          <div className="w-16 h-16 bg-blue-50 text-blue-500 rounded-full flex items-center justify-center mb-4">
            <FileText size={32} />
          </div>
          <h2 className="text-xl font-bold text-gray-900 mb-2">
            Blog Post Embedded
          </h2>
          {slug ? (
            <p className="text-gray-500 bg-gray-100 px-4 py-1 rounded-full text-sm font-mono mt-2">
              slug: {slug}
            </p>
          ) : (
            <p className="text-red-500 bg-red-50 px-4 py-1 rounded-full text-sm font-medium mt-2">
              Please select a blog post in the right panel.
            </p>
          )}
          <p className="text-xs text-gray-400 mt-6 max-w-sm">
            This is a placeholder for the editor. The full blog post will be rendered seamlessly into this page on the live site.
          </p>
        </div>
      </div>
    </section>
  );
}
```

---

## 4. Live Server Component

This is the component that actually renders on the live frontend. It runs on the server, fetches data via Drizzle ORM, and embeds the article.

**Create `components/sections/BlogPost.tsx`:**
```tsx
import React from 'react';
import { getArticleBySlug, getRelatedArticles } from '@/lib/utils/db';
import { ArticleHeader } from '@/components/article/ArticleHeader';
import { HeroImage } from '@/components/article/HeroImage';
import { ArticleContent } from '@/components/article/ArticleContent';
import { AuthorBox } from '@/components/article/AuthorBox';
import { RelatedArticles } from '@/components/article/RelatedArticles';
import { addHeadingIds } from '@/lib/utils/content';

interface BlogPostProps {
  slug?: string;
  anchorId?: string;
}

export async function BlogPost({ slug, anchorId }: BlogPostProps) {
  if (!slug) {
    return (
      <section className="w-full py-8 text-center text-gray-500" id={anchorId || undefined}>
        Please select a blog post.
      </section>
    );
  }

  const decodedSlug = decodeURIComponent(slug);
  const article = await getArticleBySlug(decodedSlug);

  if (!article) {
    return (
      <section className="w-full py-8 text-center text-red-500 font-medium" id={anchorId || undefined}>
        Blog post not found: {decodedSlug}
      </section>
    );
  }

  const relatedArticles = await getRelatedArticles(article.category, article.id);
  const contentWithIds = addHeadingIds(article.content);

  return (
    <section className="w-full py-8 bg-white" id={anchorId || undefined}>
      <div className="container mx-auto px-4 pb-8">
        <ArticleHeader
          title={article.title}
          author={article.author || '編集部'}
          publicationDate={article.publicationDate || article.createdAt}
          readingTime={article.readingTime}
          category={article.category}
        />

        {article.heroImage && (
          <HeroImage src={article.heroImage} alt={article.title} />
        )}

        <div className="max-w-4xl mx-auto">
          <article>
            <ArticleContent content={contentWithIds} />
            <AuthorBox
              author={article.author || '編集部'}
              authorBio={article.authorBio}
              authorProfileUrl={article.authorProfileUrl}
            />
          </article>
        </div>

        {relatedArticles && relatedArticles.length > 0 && (
          <RelatedArticles articles={relatedArticles} />
        )}
      </div>
    </section>
  );
}
```

---

## 5. CMS Registration

Finally, we map the components together in the Puck configuration and the live site renderers.

### A. Register in `lib/puck/config.tsx`
Update your Puck config to include the new component, binding the visual editor to `BlogPostEditor`.

```tsx
import { BlogPostEditor } from "@/components/sections/BlogPostEditor";
import { BlogPostSelector } from "@/components/editor/BlogPostSelector";

export const puckConfig: Config<any, any> = {
  categories: {
    content: {
      title: "Content",
      components: [
        // ... other components
        "BlogPost",
      ],
    },
  },
  components: {
    // ...
    BlogPost: {
      label: "Blog Post",
      fields: {
        anchorId: { type: "text", label: "Anchor ID (for navigation links)" },
        slug: {
          type: "custom",
          label: "Select Blog Post",
          render: ({ value, onChange }: any) => (
            <BlogPostSelector value={value || ""} onChange={onChange} />
          ),
        },
      },
      defaultProps: { anchorId: "", slug: "" },
      render: (props: any) => <BlogPostEditor {...stripPuck(props)} />,
    },
  },
};
```

### B. Register in Live Renderers (`serverConfig` overrides)

> [!WARNING]
> This is a critical step. If you do not map the component in the live renderers, the site will fallback to rendering the `BlogPostEditor` placeholder on the frontend!

Add the `BlogPost` server component override inside the `serverConfig.components` object in **both** `app/page.tsx` (Homepage) and `app/[slug]/page.tsx` (Custom Pages):

```tsx
import { BlogPost } from '@/components/sections/BlogPost';

// Inside serverConfig.components:
BlogPost: { ...puckConfig.components?.BlogPost, render: (props: any) => <BlogPost {...stripPuck(props)} /> },
```

> [!NOTE]
> This project uses the `serverConfig` override pattern with `<Render config={serverConfig} data={puckData} />`. There is no `PageRenderer.tsx`, `COMPONENT_MAP`, or `SERVER_OVERRIDES` — all live rendering overrides go inside `serverConfig.components`.

---

## ? Phase 11 Completion Verification

Run this checklist to confirm Phase 11 is fully implemented:

- [ ] `npx tsc --noEmit` passes with 0 errors
- [ ] `/api/editor/blog-posts` API returns list of blog posts for the selector dropdown
- [ ] `BlogPostSelector.tsx` created in `components/editor/`
- [ ] `BlogPostEditor.tsx` created in `components/sections/` (static editor preview card)
- [ ] `BlogPost.tsx` created in `components/sections/` (async RSC server component)
- [ ] "Blog Post" appears in the "Content" category in Puck sidebar
- [ ] Drag onto canvas ? `BlogPostSelector` dropdown shows available posts
- [ ] Select a post ? `BlogPostEditor` shows a static preview card in the canvas
- [ ] `BlogPost` server override added to `serverConfig.components` in BOTH `app/page.tsx` AND `app/[slug]/page.tsx`
- [ ] Publish ? live site ? full blog post renders (header, hero image, content, author box)
- [ ] `ArticleContent` receives ONLY the `content` prop (no `aioBox`, `faqItems`, `videos`)
- [ ] Date uses `article.publicationDate || article.createdAt` (NOT `ensureDate`)

> **Phase 11 is complete when ALL boxes above are checked.**
