# Phase 1: Global Header and Footer in Puck

## Important Context: Database Schema
> [!CAUTION]
> **DO NOT modify the Neon database schema.** 
> All required tables are already created in the Neon database. We use a single Neon database across all our blog websites. Do not attempt to run migrations or create new tables for this feature. Assume the database is already fully prepared to handle the required data and relies heavily on `site_id` filtering.

## Goal
Document how to implement the real website Header and Footer inside `/page_editor_admin`.

## Requirements
* The editor should render the same real Header and Footer components that are used on the live website.
* Do not create a new Header or Footer just for the editor.
* **IMPORTANT — Canvas vs Live distinction:** The live `Header.tsx` and `Footer.tsx` are **async Server Components** (they call DB functions like `getCachedCategories`). They **cannot** be imported inside the Puck canvas because Puck runs in a client context. Instead, you must create two **standalone client-side replicas**: `CanvasHeader` and `CanvasFooter` — these live inside `editor-client.tsx` and use `usePuck()` to read props. They must visually match the live components exactly, but are separate implementations.
* Header and Footer should be global layout components, not draggable Puck components.
* Header and Footer should not be stored inside individual page content JSON.
* Header and Footer should wrap the page canvas like this:

```txt
Global Header
Puck Page Canvas
Global Footer
```

* Remove Header and Footer from the general draggable component list that the user can freely drag onto any page.
* **IMPORTANT:** `SiteHeader` and `SiteFooter` must still exist as registered Puck block types (inside `lib/puck/config.tsx`). This is what makes the logo, favicon, and footer text editable from the Puck properties panel. However, they must be **pre-placed once automatically** as part of the default page content — the user should not be able to add a second copy by dragging from the sidebar.
* Users should not be able to add duplicate Headers or Footers.
* There should be only one source of truth for Header and Footer across the live website and editor.
* Logo, favicon, and footer descriptions should come from the same global JSON source.
* **Footer Navigation Logic:** The Footer's right-side navigation columns must dynamically pull from the `navigation_items` database using `site_id` and location `'footer'`. This ensures the exact same Parent/Child category tree is rendered identically to the Header, entirely reusable across different sites.
* The footer description text must be editable from the global header/footer settings, so the user can update it without changing the code.
* Any Header/Footer update from the CMS should update the same Header/Footer used on all pages.

## How Logo, Favicon, and Footer Text Are Stored
> [!IMPORTANT]
> There is **no separate database table** for the logo, favicon, or footer text. These values are stored as **props inside the Puck JSON data** in the `homepage_content` table, under the `SiteHeader` and `SiteFooter` block entries.
>
> The `SiteHeader` Puck block has fields: `logo` (image URL) and `favicon` (image URL).
> The `SiteFooter` Puck block has fields: `leftDescription` and `copyrightText`. 
> *(Note: The legacy 'Main Design' and 'designType' dropdowns, as well as warning fields, were explicitly removed. The "Category Tab Design" is the sole global footer layout).*
>
> When the user edits these in the Puck right-side panel and clicks Publish, the new values are saved back into the `homepage_content` JSON. The live website reads these values from that same JSON on every page load.
> Do NOT create a new database table for these settings.

> [!CAUTION]
> **The doc statement above ("the live website reads these values") does NOT happen automatically.** You must explicitly wire the live `Header.tsx`, `Footer.tsx`, and `app/layout.tsx` to read from the DB. See Section 7 below. If you skip this, every edit in the CMS will save correctly to the DB but will NEVER appear on the live site.

## Implementation Pattern (Critical Details for AI Agents)
To achieve this without breaking the existing editor and avoiding Puck's internal quirks, you must follow these specific technical patterns:

### 1. Hide from Sidebar via Category config
Puck automatically places any component without a category into an "OTHER" sidebar drawer. To prevent users from dragging duplicate headers, you must explicitly hide them in `lib/puck/config.tsx`:
```tsx
categories: {
  _global: {
    title: "Global Layout",
    components: ["SiteHeader", "SiteFooter"],
    visible: false, // Critical: Hides from drag-and-drop sidebar
  },
  // ... other categories
}
```

### 2. Inject via `overrides.iframe`
Do **not** wrap the `<Puck>` component with your Header/Footer. If they are outside the Puck context, they cannot access the `usePuck()` hook. Instead, inject them into the canvas using Puck's `overrides.iframe`:
```tsx
<Puck
  config={puckConfig}
  data={mergedInitialData}
  overrides={{
    iframe: ({ children }) => (
      <div style={{ display: 'flex', flexDirection: 'column' }}>
        <CanvasHeader />
        {children}
        <CanvasFooter />
      </div>
    ),
  }}
/>
```

### 3. Selection Dispatch and Event Bubbling
To make the `CanvasHeader` and `CanvasFooter` clickable so they open their respective CMS properties panels, use `usePuck().dispatch`. You must account for two undocumented Puck behaviors:
1. **Event Bubbling:** You MUST call `e.stopPropagation()`. If you don't, the click bubbles to the canvas background, and Puck instantly deselects the item.
2. **Omitting the Zone:** When dispatching the selection for root-level items, do NOT specify the `zone` name (like `'default'` or `'root:default'`). Omitting the zone allows Puck to automatically detect the correct internal root zone ID.

```tsx
function CanvasHeader() {
  const { appState, dispatch } = usePuck();

  const handleClick = (e: React.MouseEvent) => {
    e.stopPropagation(); // 1. Prevent Puck from clearing selection

    const index = appState.data.content.findIndex(b => b.type === 'SiteHeader');
    if (index !== -1) {
      dispatch({
        type: 'setUi',
        ui: { itemSelector: { index } } as any // 2. Omit the 'zone' property
      });
    }
  };

  // IMPORTANT: Use a <div> instead of <header> or <footer>. 
  // The editor's layout (app/page_editor_admin/layout.tsx) contains global CSS
  // designed to hide the live site's header and footer. If you use semantic 
  // <header>/<footer> tags here, they will be accidentally hidden!
  return <div onClick={handleClick}>...</div>;
}
```

### 4. Handling Legacy Database Saves
Because older pages saved before this architecture will not have `SiteHeader` or `SiteFooter` in their JSON `content` array, `findIndex` will return `-1` and the properties panel will not open when clicked. You must explicitly inject them into `rawPuckData.content` before passing it to `<Puck>`:
```tsx
  if (rawPuckData.content) {
    if (!rawPuckData.content.some((b: any) => b.type === 'SiteHeader')) {
      // IMPORTANT: Include full default props — the live site reads these immediately after first publish.
      // Use your site's actual fallback values here.
      rawPuckData.content.unshift({
        type: 'SiteHeader',
        props: { id: 'SiteHeader-default', logo: '/logo.png', favicon: '/favicon.ico' },
      });
    }
    if (!rawPuckData.content.some((b: any) => b.type === 'SiteFooter')) {
      rawPuckData.content.push({
        type: 'SiteFooter',
        props: {
          id: 'SiteFooter-default',
          leftDescription: 'Your default footer description text.',
          copyrightText: '© 2026 Your Site. All rights reserved.',
        },
      });
    }
  }
```

### 5. Hiding Puck's Native Wrappers via CSS
Because `SiteHeader` and `SiteFooter` exist in the `content` array, Puck will try to render its native editor wrappers (with the blue hover box and copy/delete icons) inside the main canvas, creating duplicate "ghost" components.
To fix this, you must:
1. Make your config's render function return a uniquely classed hidden div: `render: () => <div className="hidden-global-block" style={{ display: 'none' }} />`
2. Inject global CSS inside the editor to hide Puck's wrapper completely:
```css
div[data-puck-node-type="SiteHeader"],
div[data-puck-node-type="SiteFooter"] {
  display: none !important;
}
div:has(> .hidden-global-block) {
  display: none !important;
}
```
3. Custom-style your `CanvasHeader` and `CanvasFooter` with CSS (e.g. Tailwind `ring-blue-500` and dark label tags) so they mimic Puck's selection UI natively without exposing the copy/delete tools.

### 6. Custom Field Labels
When defining `type: "custom"` in your `puckConfig` for the Logo and Favicon (e.g., using a `<MediaUploadField>`), Puck will **not** render the field's `label` automatically. You must manually render a `<label>` tag inside the custom `render` function to ensure the CMS properties panel is readable.

### 7. Wiring the Live Site to Read CMS Props (CRITICAL — Do Not Skip)

This is the most commonly missed step. The Puck JSON is saved correctly to the DB when you Publish, but the live `Header.tsx`, `Footer.tsx`, and browser favicon will NOT update unless you explicitly wire them to read from `getHomepageContent()`.

#### 7a. Update `Logo.tsx` — Add an optional `src` prop
The Logo component must accept an optional `src` prop so the live Header and Footer can pass the CMS-controlled URL into it. Default to your local logo file so the live site never breaks if nothing is saved yet:
```tsx
interface LogoProps {
  src?: string;
}
export function Logo({ src = '/logo.png' }: LogoProps) {
  return (
    <Link href="/" className="flex items-center">
      <Image src={src} alt="site logo" width={200} height={55} className="h-14 w-auto" />
    </Link>
  );
}
```

#### 7b. Update `Header.tsx` — Read logo from CMS
The live `Header.tsx` must call `getHomepageContent()`, find the `SiteHeader` block, and pass its `logo` prop to `<Logo>`:
```tsx
import { getHomepageContent } from '@/lib/homepage-content';

async function getSiteHeaderProps() {
  try {
    const data = await getHomepageContent();
    const content = data?.puckData?.content as any[] | undefined;
    const headerBlock = content?.find((b: any) => b.type === 'SiteHeader');
    return { logo: headerBlock?.props?.logo || null };
  } catch {
    return { logo: null };
  }
}

export async function Header() {
  const { logo } = await getSiteHeaderProps();
  return (
    <header className="sticky top-0 z-50 w-full border-b bg-white shadow-sm">
      <div className="container flex h-20 items-center justify-between">
        <Logo src={logo ?? '/logo.png'} />
        {/* NavigationWrapper, MobileMenuWrapper, etc. */}
      </div>
    </header>
  );
}
```

#### 7c. Update `Footer.tsx` — Read logo + description from CMS
> [!IMPORTANT]
> The **logo** in the footer comes from `SiteHeader.logo` — NOT from `SiteFooter`. There is only one logo for the whole site. `SiteHeader` is the single source of truth. `SiteFooter` only controls `leftDescription` and `copyrightText`.

```tsx
import { getHomepageContent } from '@/lib/homepage-content';

async function getGlobalLayoutProps() {
  try {
    const data = await getHomepageContent();
    const content = data?.puckData?.content as any[] | undefined;
    const headerBlock = content?.find((b: any) => b.type === 'SiteHeader');
    const footerBlock = content?.find((b: any) => b.type === 'SiteFooter');
    return {
      logo: headerBlock?.props?.logo || null,           // from SiteHeader
      leftDescription: footerBlock?.props?.leftDescription || null,  // from SiteFooter
      copyrightText: footerBlock?.props?.copyrightText || null,      // from SiteFooter
    };
  } catch {
    return { logo: null, leftDescription: null, copyrightText: null };
  }
}

export async function Footer() {
  const { logo, leftDescription, copyrightText } = await getGlobalLayoutProps();
  // Use your site's hardcoded strings as fallback defaults:
  return (
    <footer>
      <Logo src={logo ?? '/logo.png'} />
      <p>{leftDescription ?? 'Your default footer description text here.'}</p>
      <p>{copyrightText ?? '© 2026 Your Site. All rights reserved.'}</p>
    </footer>
  );
}
```

#### 7d. Update `CanvasFooter` — Also read logo from `SiteHeader` props
The `CanvasFooter` inside `editor-client.tsx` must also read the logo from the `SiteHeader` Puck block — not hardcode `/logo.png`. Both the canvas header and footer must stay in sync:
```tsx
function CanvasFooter({ categories }: { categories: string[] }) {
  const { appState, dispatch } = usePuck();

  const footerBlock = appState.data.content?.find((b: any) => b.type === 'SiteFooter');
  const headerBlock = appState.data.content?.find((b: any) => b.type === 'SiteHeader');

  // Logo comes from SiteHeader — single source of truth
  const footerLogoSrc = headerBlock?.props?.logo || '/logo.png';
  const leftDescription = footerBlock?.props?.leftDescription || 'Your default description.';
  const copyrightText = footerBlock?.props?.copyrightText || '© 2026 Your Site.';
  // ...dispatch and render logic
}
```

#### 7e. Update `app/layout.tsx` — Use `generateMetadata()` for dynamic favicon
> [!WARNING]
> **Do NOT use `export const metadata = { ... }` for the favicon.** A static metadata export is computed once at build time and never re-reads the DB. The favicon field from `SiteHeader.favicon` will be ignored forever.

You MUST convert it to an async function.
> [!CAUTION]
> Next.js will throw a build error if you export both `metadata` and `generateMetadata` from the same file. You must remove the `export` keyword from your existing static metadata and merge it inside the new function.

```tsx
// 1. Remove the 'export' keyword from your static metadata
const defaultMetadata: Metadata = {
  // title, description, openGraph, etc.
};

// 2. Export ONLY the dynamic generateMetadata function
export async function generateMetadata(): Promise<Metadata> {
  let faviconUrl = '/favicon.png'; // your default fallback
  try {
    const data = await getHomepageContent();
    const content = data?.puckData?.content as any[] | undefined;
    const headerBlock = content?.find((b: any) => b.type === 'SiteHeader');
    const cmsFavicon = headerBlock?.props?.favicon;
    if (cmsFavicon && typeof cmsFavicon === 'string' && cmsFavicon.trim()) {
      faviconUrl = cmsFavicon;
    }
  } catch { /* fall back to default */ }

  return {
    ...defaultMetadata, // Spread all existing metadata here
    icons: { icon: faviconUrl, shortcut: faviconUrl, apple: faviconUrl },
  };
}
```

#### 7f. Pass `categories` from Server to `EditorClient` (for `CanvasFooter`)
Because `CanvasFooter` is a client component (it uses `usePuck()`), it cannot call `getCachedCategories()` directly. Fetch the categories in the server component `page.tsx` and pass them as a prop:
```tsx
// app/page_editor_admin/page.tsx
export default async function EditorPage() {
  const homepageData = await getHomepageContent();
  const categoriesData = await getCachedCategories();
  const categories = categoriesData.map((cat) => cat.japanese); // or .name, etc.
  return <EditorClient initialData={homepageData} categories={categories} />;
}
```

## Testing Steps

### Editor Canvas Tests
1. Open `/page_editor_admin?path=/`.
2. Confirm the real Header and Footer are visible in the canvas.
3. Confirm there is no duplicate Header or Footer.
4. Confirm the sidebar does NOT have a freely draggable "SiteHeader" or "SiteFooter" block.
5. Click the canvas Header → confirm the right-side panel opens with **Logo** and **Favicon** upload fields.
6. Click the canvas Footer → confirm the right-side panel opens with **Footer Description** and **Copyright** fields.

### Live Site Integration Tests (CRITICAL — Test All of These)
7. In the CMS, upload a new logo image via the `SiteHeader` Logo field. Click **Publish**. Refresh the live homepage. Confirm the new logo appears in both the **header** and the **footer**.
8. Edit the footer description text. Click **Publish**. Refresh. Confirm the updated text appears in the live footer.
9. Edit the copyright text. Click **Publish**. Refresh. Confirm the updated copyright appears.
10. Upload a new favicon image via the `SiteHeader` Favicon field. Click **Publish**. Do a **hard refresh** (`Ctrl+Shift+R`) or open in a new incognito window. Confirm the browser tab icon has changed.
11. Confirm `app/layout.tsx` exports `generateMetadata()` (async function) — NOT `export const metadata` — otherwise the favicon test above will always fail.

---

## ? Phase 1 Completion Verification

Run this checklist to confirm Phase 1 is fully implemented:

- [ ] `npx tsc --noEmit` passes with 0 errors
- [ ] `/page_editor_admin` shows real Header and Footer wrapping the canvas
- [ ] Logo upload in CMS ? Publish ? logo updates on live site (both header and footer)
- [ ] Favicon upload in CMS ? Publish ? hard refresh ? browser tab icon updates
- [ ] Footer description and copyright editable and visible on live site after publish
- [ ] `app/layout.tsx` exports `generateMetadata()` (NOT `export const metadata`)
- [ ] No duplicate Header/Footer in the editor canvas

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