# Phase 5: The "Create New Page" Feature

## Goal

Give the user the ability to dynamically generate brand-new, empty pages from the UI and automatically inject them into the site's navigation menus, without writing code.

\---

## How It Works (Actual Implementation)

> \\\[!IMPORTANT]
> The "Create New Page" feature is NOT a standalone button. It is an option \\\*\\\*inside the page switcher `<select>` dropdown\\\*\\\* in the editor header. When the user selects "➕ Create New Page...", it triggers the `CreatePageModal` and resets the select value.

### The Page Switcher Dropdown

The dropdown in the editor header lists pages in this order:

1. **Homepage** (`/`)
2. **About Us** (`/about`)
3. **Privacy Policy** (`/privacy-policy`)
4. Any custom pages fetched from the `custom\\\_pages` table (grouped under a divider `<optgroup>`)
5. A special **"➕ Create New Page..."** option at the bottom

When the user selects the `\\\_\\\_create\\\_\\\_` value, the `CreatePageModal` opens.

\---

## Requirements

### 1\. The `CreatePageModal` UI

The modal contains:

* **Page Name** — text input. As the user types, the URL slug is auto-generated (lowercase, hyphenated, special chars stripped).
* **URL Slug** — editable text input with a `/` prefix shown visually. Validated to be URL-safe, non-reserved.
* **SEO Settings (optional)** — collapsible section with `Meta Title` and `Meta Description` text inputs.
* **Navigation Settings** — checkboxes controlled by local site configuration:

  * ☐ Show in Top Header
  * ☐ Show in Blue Category Bar *(Conditionally rendered if `HAS\\\_BLUE\\\_CATEGORY\\\_BAR = true` in the codebase)*
  * ☐ Show in Footer
* **Parent Menu (optional)** — a `<select>` dropdown, grouped by location using `<optgroup>`.

  * **Top Header Optgroup:** Must contain BOTH the live site Categories (e.g., "グルメ") AND top-level custom pages.
  * **Footer Optgroup:** Contains only top-level footer custom pages.
  * Includes helper tip: *"⚠️ For nesting to work, pick a parent from the same location selected above."*
* **Cancel** and **Create Page** buttons.

> \\\[!WARNING]
> \\\*\\\*Reserved slugs must be blocked.\\\*\\\* The following slugs must be rejected client-side:
> `''`, `'about'`, `'privacy-policy'`, `'blogs'`, `'blog'`, `'api'`, `'category'`, `'author'`, `'page\\\_editor\\\_admin'`, `'\\\_next'`, `'robots.txt'`, `'sitemap.xml'`, `'icon.png'`, `'favicon.ico'`
> Keep this list in sync with `lib/custom-pages.ts` → `RESERVED\\\_SLUGS`.

### 2\. Database Logic — Step 1: The Directory (`custom\\\_pages`)

On form submit, call `POST /api/custom-pages` with:

```json
{
  "title": "Special Offers",
  "slug": "special-offers",
  "showInHeader": true,
  "showInFooter": false,
  "metaTitle": "...",
  "metaDescription": "..."
}
```

The API inserts into `custom\\\_pages`. The `indexable` column is always forced to `true` — never expose a toggle for this.

The API response returns the new page's `id`.

### 3\. Database Logic — Step 2: Navigation Items (`navigation\\\_items`)

After step 1, for each checked location, call `POST /api/navigation-items`:

```json
{
  "label": "Special Offers",
  "url": "/special-offers",
  "itemType": "custom\\\_page",
  "sourceId": "42",
  "menuLocation": "top\\\_header",
  "parentId": null,
  "sortOrder": 9999,
  "isActive": true,
  "openInNewTab": false
}
```

**Parent-child matching:** If the user selected a parent item from the dropdown, only apply that `parentId` if the parent item's `menuLocation` matches the current location. Otherwise, set `parentId: null`.

### 4\. Redirect After Creation

After all database writes succeed, call `onCreated(slug)` which pushes the router to `/page\\\_editor\\\_admin?path=/{slug}`. The editor loads a blank canvas for that new path.

### 5\. The Page Switcher Must Reload

After creation, the page switcher must include the new page in its list. This is handled by the server-side re-fetch on router navigation.

\---

## The Page Settings Modal (⚙️)

After a custom page is created, the user can click the **⚙️ Settings** button in the editor header to open `PageSettingsModal`. This allows editing:

* **Page Name** (title)
* **Meta Title**
* **Meta Description**

The `indexable` field is always saved as `true` and is NOT shown to the user.

\---

## Live Site Component Integration

> \\\[!CAUTION]
> \\\*\\\*Don't forget the Frontend!\\\*\\\*
> Saving the custom pages and navigation items into the database is only half the battle. The live site components must be updated to actually query the `navigation\\\_items` table.
>
> You must update:
> - `NavigationWrapper` (Desktop Top Header)
> - `MobileMenuWrapper` (Mobile Slide-out Menu)
> - `Footer.tsx` (Footer Links)
>
> They must fetch the items for their respective `menuLocation` and append them to the hardcoded/dynamic categories already present in the UI.

\---

## Common Gotchas \& Bug Fixes

1. **SQL `.limit()` Syntax:** When querying a single row by ID (e.g. for the Settings Modal), always make sure to use `.limit(1)` as a chained method on the query, not inside the `.where()` block. Incorrect placement (e.g., `where(eq(id, id)) { limit: 1 }`) will cause a 500 error when the user clicks the ⚙️ Settings button.
2. **UI Cursors:** Checkboxes and custom `<select>` dropdowns in the `CreatePageModal` should always have `cursor-pointer` explicitly added to their Tailwind classes so they feel interactive to the admin user.
3. **Multi-Level Nested Navigation:** The `navigation\\\_items` structure natively supports infinite depth. To support this on the frontend, `Navigation.tsx` uses a recursive tree component with hover-flyouts for desktop, and `MobileMenu.tsx` uses recursive slide-down accordions for mobile.
4. **Nesting Depth Restrictions:** To enforce a strict 3-level navigation hierarchy (Top-Level -> Child -> Subchild), the frontend `CreatePageModal` dropdown must be restricted. The easiest way to do this is to build the full tree on the server (`getNavigationTree`), and then use a utility like `flattenNavTree(tree, maxDepth = 1)` to filter out items at depth 2 (subchildren). Only depth 0 (root) and depth 1 (children) items are permitted as valid parents. Furthermore, the list should be sorted so children appear under their parents, and depth 1 items should be prefixed with `— ` so they are visually indented in the `<select>` dropdown. If you pass all custom pages directly without flattening the tree first, the dropdown will allow users to select an existing subchild as a parent, incorrectly creating a 4th tier of nesting.
5. **Dropdown Flex UI Bug:** When recursively rendering desktop dropdown links (`Navigation.tsx`), never apply a `block` class to the `Link` element. Keep it as a `flex` container so that the text and the dropdown arrow SVG remain vertically centered and horizontally aligned without stretching the button height.
6. **URL Slug Auto-generation State:** When auto-generating the URL slug from the Page Title input, you must track a boolean state (e.g., `slugManuallyEdited`). If you only check `if (!slug)`, the auto-generator will permanently disable itself the moment the user types the first character.
7. **URL Slug UI Layout:** When designing the `/` prefix box next to the slug input, always use a `flex` container (e.g., `flex w-full`) with the input set to `flex-1 block w-full min-w-0`. Otherwise, the prefix box will misalign vertically.
8. **Client-Side Reserved Slug Protection:** Ensure `handleSubmit` strictly validates against `RESERVED\_SLUGS` (like `about`, `api`, `\_next`) BEFORE firing the API request. Failure to do so will result in CMS pages overriding core Next.js routing infrastructure.
9. **Missing Parent Menu Categories:** When populating the "Parent Menu" `<optgroup>`, do not simply pass custom pages from the DB. You **must** also map and render the dynamic site categories (e.g., `categoryLinks`) into the dropdown so administrators can nest new pages under existing article categories.
10. **Delete Confirmation UI:** Never use the native browser `window.confirm()` for deleting custom pages. You must build and use a custom React Modal UI that includes an explicit `⚠️ Warning: SEO Impact` message to prevent accidental deletions of ranked pages.
11. **`customPages.id` Type — Integer, NOT UUID:** The `custom\_pages` table uses a standard PostgreSQL `serial` (integer) primary key, NOT a UUID. The `shared/schema.ts` definition **must** use `serial('id').primaryKey()`. If you accidentally declare it as `uuid('id').primaryKey().defaultRandom()`, Drizzle ORM will crash silently when trying to parse an integer ID as a UUID string, causing `getCustomPageBySlug()` to return `null` and Next.js to serve a **404** on every single custom page.
12. **Next.js 15+ Async `params`:** In Next.js 15 and above, the `params` object passed to `generateMetadata` and page components is now a **Promise** and must be awaited before accessing any property. Always type it as `params: Promise<{ slug: string }>` and destructure with `const { slug } = await params;`. Accessing `params.slug` directly (without `await`) causes a runtime crash and a 404.
13. **Editor Dropzone Blocked by Double SiteHeader/SiteFooter:** The Page Editor (`editor-client.tsx`) uses a `CanvasHeader` and `CanvasFooter` component rendered via Puck's `iframe` override to visually preview the site header and footer. Do NOT also inject `SiteHeader` / `SiteFooter` blocks into the Puck content data array. If both exist simultaneously, the header and footer blocks fill the entire Puck dropzone area, making it impossible for the admin to drag any component onto the canvas. Always filter them out of `puckData.content` before passing it to `<Puck>`, and also filter them out of the saved data before writing to the database.
14. **`overflow-hidden` Clips Flyout Sub-menus:** When building recursive desktop dropdown menus in `ResponsiveNav.tsx`, **never** apply `overflow-hidden` to any dropdown container `<div>`. Because sub-menus are absolutely positioned children that intentionally extend outside their parent's layout bounds (flying out to the side), `overflow-hidden` acts like scissors and makes them completely invisible. Always omit `overflow-hidden` from all dropdown and flyout wrappers. Also ensure the side-flyout has a higher `z-index` than its parent (e.g., `z-[60]` vs parent `z-50`) to prevent it from rendering behind other elements.
15. **`parentId` Mixed Types — Slug vs Integer String:** In the `navigation\_items` table, the `parent\_id` column stores different ID formats depending on the parent item type. Category parents use their **slug** string (e.g., `"ヴィーガンチョコ"`) while custom page parents use their **integer ID** as a string (e.g., `"198"`). The `Header.tsx` recursive tree builder must handle both formats — category links must use `cat.slug` as their tree node `id`, and custom page nav items must use `item.sourceId` (the integer ID as string) as their tree node `id`. If you use a single consistent format (e.g., only integer IDs), category-parented pages will be orphaned from their parent and lost from the navigation tree.
16. **Database Connection Import Path:** When importing the database connection instance (`db`) into new server-side files (like `lib/navigation.ts` or API routes), you must import it from `@/lib/db`. Do not mistake it for `@/lib/utils/db`, as that will cause a static build error: `Export db doesn't exist in target module`.
17. **Merging Categories into the Navigation Tree:** When building the navigation tree in `lib/navigation.ts`, you **must** pass the database categories (e.g. from `getAllCategories()`) into the builder and initialize them as root nodes in the tree map. If you fail to do this, any custom page that assigns a category as its parent will fail to find its parent node in the map, causing it to incorrectly render as a standalone top-level link instead of nesting properly in the category's dropdown.
18. **Flyout Sub-menu Width & Overflow:** When rendering multi-level flyout dropdown menus, use compact widths (e.g., `w-40` for the 1st dropdown, `w-36` for 2nd/3rd level flyouts) and tight margins (`ml-0.5`) between panels. If the flyouts are too wide (e.g., `w-48`), the combined width of stacked side-flyouts will exceed the viewport and cause horizontal scrolling on the page.

The modal calls `PUT /api/custom-pages` with `{ id, title, metaTitle, metaDescription, indexable: true }`.

\---

## Live Frontend: The Catch-All Route (`app/[slug]/page.tsx`)

To make the newly designed page visible to the public:

1. Query `custom\\\_pages` by slug — return 404 if not found.
2. Query `homepage\\\_content` using `${SITE\\\_ID}-${slug}` to fetch the Puck JSON canvas data.
3. Pass the data to the custom `<PageRenderer>` component.
4. Inject `<meta>` tags from `custom\\\_pages.meta\\\_title`, `meta\\\_description`, `canonical\\\_url`.
5. Inject structured `schema\\\_json` as `<script type="application/ld+json">` if the field is non-null.

> \\\[!IMPORTANT]
> \\\*\\\*We do NOT use Puck's `<Render>` component for the live frontend.\\\*\\\* Instead, we use our own custom `PageRenderer.tsx` (and a matching loop in `app/page.tsx`). This custom renderer manually maps the JSON data to React components. This solves two major issues:
> 1. It provides much better Server-Side Rendering (SSR) performance.
> 2. It avoids React hydration errors by safely separating `CLIENT\\\_ONLY\\\_COMPONENTS` (which get wrapped in a client-side `<PuckRenderer>`) from server-rendered components.

```tsx
// app/\\\[slug]/page.tsx
// IMPORTANT: In Next.js 15+, params is a Promise and must be awaited.
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
  const { slug } = await params;
  const page = await getCustomPageBySlug(slug);
  return {
    title: page?.metaTitle || page?.title,
    description: page?.metaDescription,
    alternates: { canonical: page?.canonicalUrl || `https://${DOMAIN}/${slug}` },
  };
}

export const dynamic = 'force-dynamic';
export const revalidate = 0;

export default async function CustomPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const page = await getCustomPageBySlug(slug);
  if (!page) notFound();
  const { puckData } = await getHomepageContent(`/${slug}`);
  return (
    <main className="bg-page-white min-h-screen">
      {page.schemaJson \\\&\\\& (
        <script
          type="application/ld+json"
          dangerouslySetInnerHTML={{
            \\\_\\\_html: typeof page.schemaJson === 'string'
              ? page.schemaJson
              : JSON.stringify(page.schemaJson)
          }}
        />
      )}
      
      <PageRenderer dataToRender={puckData} />
    </main>
  );
}
```

\---

## Safe Page Deletion Architecture

When allowing admins to permanently delete custom pages from the editor, you must strictly implement a cascading deletion flow to prevent database orphan records and UI crashes.

### 1\. Backend Deletion Logic (`DELETE /api/custom-pages`)

* **Enforce Referential Integrity:** Before deleting a row from `custom\\\_pages`, you **must** delete all associated navigation links from the `navigation\\\_items` table (where `sourceId === id`).
* **Delete Editor Canvas:** You **must** also delete the page's saved canvas from the `homepage\\\_content` table.
* **Consequence of failure:** If you fail to delete the navigation items, you will have ghost links pointing to 404s. If you fail to delete the canvas from `homepage\\\_content`, the database will throw a `Unique Constraint Error` if the user ever tries to create a new page with the exact same slug again (as the ghost canvas will still exist).

### 2\. Standardization \& Collision Safety (`POST /api/custom-pages`)

* **Strict `siteId` Formatting:** When creating or deleting a canvas in `homepage\\\_content`, you MUST use the exact same formatting utility used by the editor (e.g., `pathToSiteId(slug)`). Do not manually concatenate the ID (e.g. `${SITE\\\_ID}-/${slug}`) as it will lead to mismatched ghost records that the editor misses.
* **Auto-Overwrite Protection:** When initializing a new empty canvas in the database, always use an `.onConflictDoUpdate` database constraint to forcefully overwrite any potentially orphaned canvas data with an empty canvas. This ensures old ghost data is never accidentally revived.

### 3\. Frontend Deletion UI (`DeletePageModal`)

Deletion must use a **standalone `DeletePageModal` component** — completely separate from `PageSettingsModal`. Do NOT embed deletion inside the settings modal.

* **Dedicated Trash Icon:** The editor header must show a dedicated **🗑️ Trash** icon button (red) next to the ⚙️ Settings icon. This icon is only visible on custom pages (not on Homepage, About Us, or Privacy Policy).
* **Standalone Modal Design:** Clicking the trash icon opens a standalone `DeletePageModal` with this exact layout:
  1. **Header:** A 🗑️ trash icon + bold "Delete Page" title.
  2. **SEO Warning Box:** A red-bordered, light-red background box containing:
     - A bold `⚠ Warning: SEO Impact` heading in red.
     - Body text: *"If this page is already ranking on Google and you delete it, the page will be removed and may lose its Google ranking."*
  3. **Confirmation Text:** *"Are you sure you want to permanently delete **/slug**?"* followed by *"This will remove the page and all its content. **This cannot be undone.**"*
  4. **Action Buttons:** A centered row with a "Cancel" button (light gray) and a "Delete Page" button (solid red `#e02424`).
* **Post-Delete Routing:** After deletion, the editor client must actively redirect the user away from the deleted slug (e.g., redirect to the Homepage `/`). If the user remains on the deleted slug, the client will crash or throw 404 errors as the page data no longer exists.

\---

## SEO, AEO \& Indexing Management

### Forced Indexing

* The `indexable` column is always saved as `true`. Never let users toggle this.
* If a page is deleted, it will naturally 404 and drop from Google.

### Sitemap Automation

`app/sitemap.ts` must query `custom\\\_pages` where `indexable = true` and include all results as sitemap entries.

### Structured Data (AEO)

The `schema\\\_json` column in `custom\\\_pages` is injected as `<script type="application/ld+json">` — supports rich snippets and AI Overview ranking.

\---

## Dynamic Puck Component Architecture

When building new dynamic components for the Puck CMS Editor (e.g., components that fetch data from the Neon database like `SelectedBlogsEditor`), you must strictly adhere to the following Next.js Client/Server boundary rules:

### 1\. No Async Server Components in `config.tsx`

Puck's `EditorClient` runs entirely on the **Client-Side**. If you import an `async function` Server Component directly into `lib/puck/config.tsx` and map it to a component's `render` property, the editor will completely crash with the error: `<Component> is an async Client Component. Only Server Components can be async.`

### 2\. The Client-Side Fetching Pattern (For Canvas Previews)

If you want the component to render the exact, full live data *directly inside the editor canvas* (so the admin can preview exactly how it looks):

* The component assigned in `puckConfig` MUST be a Client Component (`'use client'`).
* It CANNOT use `await db.select()`.
* Instead, you must build a dedicated API route (e.g., `POST /api/editor/blogs/full`) that fetches the required database rows.
* The Client Component must use `useEffect` (or `SWR`) to hit that API, load the data, and render the actual UI components inside the canvas.

\---

## Testing Steps

1. Open `/page\_editor\_admin` and use the page switcher dropdown.
2. Select "➕ Create New Page...". Confirm the `CreatePageModal` opens.
3. Enter "Test Landing Page" — confirm slug becomes `test-landing-page`.
4. Check "Show in Top Header". Click "Create Page".
5. Confirm the editor redirects to `?path=/test-landing-page` with a blank canvas.
6. Drag a text block onto the canvas and hit Publish.
7. Go to the live website homepage — confirm "Test Landing Page" appears in the Top Header.
8. Click the link — confirm it loads `/test-landing-page` with the published content.
9. Check `app/sitemap.ts` output — confirm `/test-landing-page` appears.
10. Create a 2nd page "Sub Page" with "Test Landing Page" as its parent. Confirm it appears as a dropdown child under "Test Landing Page" on the live site.
11. Create a 3rd page "Deep Page" with "Sub Page" as its parent. Confirm it appears as a flyout sub-menu (with `>` arrow) when hovering over "Sub Page" in the dropdown.
12. Attempt to create a 4th-level page — confirm the "Parent Menu" dropdown does NOT show "Deep Page" as an option (depth restriction enforced).
13. Click the 🗑️ Trash icon on a custom page. Confirm the standalone `DeletePageModal` opens with the SEO warning.
14. Delete the page. Confirm redirect to Homepage and the page is removed from navigation.

---

## ? Phase 5 Completion Verification

Run this checklist to confirm Phase 5 is fully implemented:

- [ ] `npx tsc --noEmit` passes with 0 errors
- [ ] "Create New Page" option appears in the Page Switcher dropdown
- [ ] Creating a page with name + slug + menu checkboxes works without errors
- [ ] New page appears in the Page Switcher dropdown immediately after creation
- [ ] New page is accessible on the live site at `/slug`
- [ ] Navigation items are created for each checked menu location (`top_header`, `category_bar`, `footer`)
- [ ] Page Settings modal (??) opens and allows editing SEO fields (meta title, description, canonical URL)
- [ ] Parent menu nesting works � child pages appear as dropdown items on the live site
- [ ] 3-level depth limit is enforced (no 4th-level nesting allowed)
- [ ] Delete Page (???) opens confirmation modal and removes page + navigation items

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