# Resolving the "Disappearing Components" Bug in Next.js Page Builders

This guide explains the root causes and solutions for a critical bug where UI components added to a drag-and-drop page builder (like Puck) disappear after 1-2 hours of inactivity, or fail to render on the live production site.

## The Problem

When a user adds components to the editor (`/page_editor_admin`), everything looks correct in the browser. However, after leaving the page idle for a few hours and returning (or refreshing), the components completely vanish from both the editor and the live site. Additionally, when clicking "Publish", the live site sometimes fails to show the updated data.

## Root Causes

This catastrophic data-loss loop is caused by three overlapping architectural issues:

### 1. Database Cold Starts & Error Swallowing
When using a serverless database (like Neon Postgres or Supabase), the database goes to "sleep" after a period of inactivity. When the Next.js server attempts to fetch the initial editor state, the asleep database causes a connection timeout. If the backend data-fetching function catches this error and returns **empty default data** instead of throwing a hard error, the editor loads an empty canvas.

### 2. The Auto-Save Death Loop
Because the editor is configured to automatically save changes, as soon as it loads the (incorrectly) empty canvas caused by the database timeout, it immediately fires an "Auto-save" request. This overwrites the actual database records with the empty default template, permanently destroying the user's previously saved components.

### 3. Hardcoded Frontend Rendering Overrides
Even when the database correctly saves the component data, the live Next.js landing page (`app/page.tsx`) may be configured to ignore the database properties. If the server-side rendering configuration hardcodes the React components without passing down the dynamically injected props, the live site will always render default fallback text instead of the user's edits.

---

## The Solution & Implementation Steps

To permanently fix this, you must separate draft states from published states, implement database retries, and properly propagate React props.

### Step 1: Database Schema Upgrade (Draft vs Publish)
Add a dedicated `draft_content` column to your database schema so that auto-saves never overwrite the live production data.

```typescript
// schema.ts
export const homepageContent = pgTable('homepage_content', {
  id: uuid('id').defaultRandom().primaryKey(),
  siteId: varchar('site_id', { length: 255 }).notNull().unique(),
  content: jsonb('content').notNull(),         // LIVE Production Data
  draftContent: jsonb('draft_content'),        // WIP Draft Data
  updatedBy: varchar('updated_by', { length: 255 }),
});
```

### Step 2: Implement Database Retries
Update your backend fetch function to try multiple times before failing. If the database is asleep, this gives it time to wake up instead of returning empty data.

```typescript
// lib/homepage-content.ts
export async function getHomepageContent() {
  let retries = 3;
  while (retries > 0) {
    try {
      const rows = await db.select().from(homepageContent).limit(1);
      
      const dbContent = rows[0]?.content;
      const dbDraftContent = rows[0]?.draftContent;

      return { 
        puckData: dbContent?.puckData || DEFAULT_DATA,
        draftData: dbDraftContent?.puckData || dbContent?.puckData || DEFAULT_DATA 
      };
    } catch (error) {
      retries -= 1;
      if (retries === 0) {
        // IMPORTANT: Throw a hard error. Do NOT return default data here.
        throw new Error("Database failed to respond after 3 attempts."); 
      }
      await new Promise(resolve => setTimeout(resolve, 1500)); // Wait 1.5s
    }
  }
}
```

### Step 3: Secure the Save API Route
Modify the `POST` endpoint to accept a `mode` parameter. Auto-saves should only touch the `draft_content` column. Next.js cache invalidation (`revalidatePath('/')`) should only run when explicitly publishing.

```typescript
// app/api/homepage-content/route.ts
export async function POST(request: NextRequest) {
  const body = await request.json();
  const mode = body.mode || 'publish'; // 'publish', 'draft', or 'auto'
  
  // Custom save function that updates content vs draftContent based on mode
  const result = await saveHomepageContent({ puckData: body.content }, mode);

  if (result.success) {
    if (mode === 'publish') {
      revalidatePath('/'); // Only clear live site cache on Publish
    }
    revalidatePath('/page_editor_admin'); // Always update editor cache
    return NextResponse.json({ success: true });
  }
}
```

### Step 4: Fix Frontend Component Overrides
Ensure that the live site rendering config in `app/page.tsx` actually passes the `props` coming from the database into the React components.

**Incorrect (Ignores Database):**
```typescript
SiteHeroSection: {
  ...puckConfig.components.SiteHeroSection,
  render: () => <HeroSection />, // ❌ Fails to pass props!
}
```

**Correct (Propagates Database Edits):**
```typescript
SiteHeroSection: {
  ...puckConfig.components.SiteHeroSection,
  render: (props: any) => <HeroSection {...stripPuck(props)} />, // ✅ Works
}
```
*(Note: `stripPuck` is a helper function that removes internal editor props before passing them to the live component).*

> [!TIP]
> After implementing these changes, make sure the editor initializes by fetching `draftData` instead of `puckData`. This ensures that when a user leaves the editor and comes back tomorrow, their unsaved work is exactly where they left it!
