# Static Site Generation

APIs for prerendering React components to static HTML with support for postponed content, enabling hybrid static and dynamic rendering strategies.

## Runtime Selection

React DOM provides runtime-specific static generation implementations:

- **Node.js**: `react-dom/static` or `react-dom/static.node`
- **Browser**: `react-dom/static.browser`
- **Edge Runtimes**: `react-dom/static.edge`

## Capabilities

### prerender (Web Streams)

Prerenders React elements to a ReadableStream with optional postponed state for later completion.

```javascript { .api }
/**
 * Prerender to static HTML with postponed content support
 * @param children - React elements to prerender
 * @param options - Configuration options
 * @returns Promise with prelude stream and postponed state
 */
function prerender(
  children: ReactNode,
  options?: PrerenderOptions
): Promise<{
  prelude: ReadableStream;
  postponed: PostponedState | null;
}>;

interface PrerenderOptions {
  /** Prefix for IDs generated by useId */
  identifierPrefix?: string;
  /** Namespace URI for SVG or MathML content */
  namespaceURI?: string;
  /** Inline script content to inject before React code */
  bootstrapScriptContent?: string;
  /** External script URLs to load */
  bootstrapScripts?: Array<string | BootstrapScriptDescriptor>;
  /** ES module URLs to load */
  bootstrapModules?: Array<string | BootstrapScriptDescriptor>;
  /** Target chunk size for progressive streaming (bytes) */
  progressiveChunkSize?: number;
  /** AbortSignal to cancel prerendering */
  signal?: AbortSignal;
  /** Error handler called for all errors */
  onError?: (error: Error, errorInfo: ErrorInfo) => string | void;
  /** Called when content is postponed */
  onPostpone?: (reason: string, postponeInfo: PostponeInfo) => void;
  /** External runtime script for React */
  unstable_externalRuntimeSrc?: string | BootstrapScriptDescriptor;
  /** Import map for module resolution */
  importMap?: ImportMap;
  /** Called with headers that should be set */
  onHeaders?: (headers: Headers) => void;
  /** Maximum length for header values */
  maxHeadersLength?: number;
}

interface BootstrapScriptDescriptor {
  src: string;
  integrity?: string;
  crossOrigin?: 'anonymous' | 'use-credentials';
}

type PostponedState = OpaquePostponedState; // Opaque postponed rendering state

interface ErrorInfo {
  componentStack?: string;
}

interface PostponeInfo {
  componentStack?: string;
}
```

**Usage Examples:**

```javascript
import { prerender } from 'react-dom/static';

// Basic prerendering
const { prelude, postponed } = await prerender(<App />);

// Save to file
const file = await Deno.open('index.html', { write: true, create: true });
await prelude.pipeTo(file.writable);

// Check if anything was postponed
if (postponed) {
  console.log('Some content was postponed');
  // Save postponed state for later
  await Deno.writeTextFile('postponed.json', JSON.stringify(postponed));
}

// With configuration
const { prelude, postponed } = await prerender(<App />, {
  bootstrapModules: ['/app.js'],
  signal: AbortSignal.timeout(30000),
  onError(error, errorInfo) {
    console.error('Prerender error:', error);
    console.error('Component stack:', errorInfo.componentStack);
  },
  onPostpone(reason, postponeInfo) {
    console.log('Postponed:', reason);
    console.log('At:', postponeInfo.componentStack);
  }
});

// Edge function for static generation
export default {
  async scheduled(event, env) {
    const { prelude, postponed } = await prerender(<BlogIndex />, {
      bootstrapModules: ['/blog.js']
    });

    // Store static HTML
    await env.STORAGE.put('blog/index.html', prelude);

    // Store postponed state if needed
    if (postponed) {
      await env.STORAGE.put('blog/postponed.json', JSON.stringify(postponed));
    }
  }
};
```

**Key Features:**

- **Postponed Content**: Components can postpone rendering to request time
- **Static Shell**: Generate static HTML for SEO and performance
- **Hybrid Rendering**: Combine static and dynamic content
- **Zero Runtime**: Static HTML doesn't need React on the client (unless hydrating)

### prerenderToNodeStream (Node.js)

Node.js-specific prerendering to a Readable stream.

```javascript { .api }
/**
 * Prerender to Node.js Readable stream
 * @param children - React elements to prerender
 * @param options - Configuration options
 * @returns Promise with prelude stream and postponed state
 */
function prerenderToNodeStream(
  children: ReactNode,
  options?: PrerenderOptions
): Promise<{
  prelude: Readable;
  postponed: PostponedState | null;
}>;

type Readable = NodeJS.ReadableStream;
```

**Usage Examples:**

```javascript
import { prerenderToNodeStream } from 'react-dom/static';
import { createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';

// Prerender and save to file
const { prelude, postponed } = await prerenderToNodeStream(<App />);

await pipeline(
  prelude,
  createWriteStream('dist/index.html')
);

if (postponed) {
  await writeFile(
    'dist/postponed.json',
    JSON.stringify(postponed)
  );
}

// Build-time static generation
import { prerenderToNodeStream } from 'react-dom/static';
import { writeFile } from 'fs/promises';

async function buildStaticSite() {
  const pages = [
    { path: 'index.html', component: <HomePage /> },
    { path: 'about.html', component: <AboutPage /> },
    { path: 'blog/index.html', component: <BlogIndex /> }
  ];

  for (const page of pages) {
    const { prelude, postponed } = await prerenderToNodeStream(
      page.component,
      {
        bootstrapModules: ['/client.js'],
        identifierPrefix: page.path + '-'
      }
    );

    // Save HTML
    const chunks = [];
    for await (const chunk of prelude) {
      chunks.push(chunk);
    }
    await writeFile(`dist/${page.path}`, Buffer.concat(chunks));

    // Save postponed state
    if (postponed) {
      await writeFile(
        `dist/${page.path}.postponed`,
        JSON.stringify(postponed)
      );
    }
  }
}

buildStaticSite();
```

### resumeAndPrerender (Web Streams)

Resumes and prerenders from previously postponed state.

```javascript { .api }
/**
 * Resume and prerender from postponed state
 * @param children - React elements to render
 * @param postponedState - State from previous postponed render
 * @param options - Configuration options
 * @returns Promise with prelude stream and new postponed state
 */
function resumeAndPrerender(
  children: ReactNode,
  postponedState: PostponedState,
  options?: PrerenderOptions
): Promise<{
  prelude: ReadableStream;
  postponed: PostponedState | null;
}>;
```

**Usage Example:**

```javascript
import { resumeAndPrerender } from 'react-dom/static';

// Load postponed state from build time
const postponedState = await loadPostponedState();

export default {
  async fetch(request) {
    // Complete the postponed rendering at request time
    const { prelude, postponed } = await resumeAndPrerender(
      <App url={request.url} />,
      postponedState,
      {
        bootstrapModules: ['/app.js'],
        onError(error) {
          console.error('Resume error:', error);
        }
      }
    );

    // Should be no more postponed content
    if (postponed) {
      console.warn('Still have postponed content');
    }

    return new Response(prelude, {
      headers: { 'Content-Type': 'text/html' }
    });
  }
};
```

### resumeAndPrerenderToNodeStream (Node.js)

Node.js-specific resume and prerender from postponed state.

```javascript { .api }
/**
 * Resume and prerender to Node.js stream from postponed state
 * @param children - React elements to render
 * @param postponedState - State from previous postponed render
 * @param options - Configuration options
 * @returns Promise with prelude stream and new postponed state
 */
function resumeAndPrerenderToNodeStream(
  children: ReactNode,
  postponedState: PostponedState,
  options?: PrerenderOptions
): Promise<{
  prelude: Readable;
  postponed: PostponedState | null;
}>;
```

**Usage Example:**

```javascript
import { resumeAndPrerenderToNodeStream } from 'react-dom/static';
import { readFile } from 'fs/promises';

// Request handler completing postponed render
export async function handler(req, res) {
  // Load postponed state from build
  const postponedState = JSON.parse(
    await readFile('dist/postponed.json', 'utf-8')
  );

  const { prelude, postponed } = await resumeAndPrerenderToNodeStream(
    <App url={req.url} />,
    postponedState,
    {
      bootstrapModules: ['/app.js']
    }
  );

  res.setHeader('Content-Type', 'text/html');
  prelude.pipe(res);
}
```

## Postponing Content

### Using postpone()

```javascript
import { postpone } from 'react';

function DynamicContent({ userId }) {
  if (typeof window === 'undefined') {
    // Postpone during static generation
    postpone('User-specific content');
  }

  // This runs at request time or on client
  const user = useUser(userId);
  return <div>{user.name}</div>;
}

function BlogPost() {
  return (
    <article>
      <h1>Static Title</h1>
      <p>Static content that gets prerendered...</p>

      {/* This will be postponed */}
      <DynamicContent userId={currentUserId} />
    </article>
  );
}
```

**Workflow:**

1. **Build time**: Call `prerender()` - static content is rendered, dynamic content is postponed
2. **Save**: Store both the static HTML and postponed state
3. **Request time**: Call `resumeAndPrerender()` with the postponed state - dynamic content is rendered
4. **Serve**: Send complete HTML to client

### Suspense with Postpone

```javascript
import { Suspense, postpone } from 'react';

function UserProfile({ userId }) {
  if (typeof window === 'undefined') {
    postpone('User profile');
  }

  const user = useUser(userId);
  return <ProfileCard user={user} />;
}

function App() {
  return (
    <div>
      <Header /> {/* Static */}

      <Suspense fallback={<ProfileSkeleton />}>
        <UserProfile userId={currentUserId} />
      </Suspense>

      <Footer /> {/* Static */}
    </div>
  );
}

// Build time:
// - Header and Footer are in static HTML
// - UserProfile is in postponed state with Suspense fallback

// Request time:
// - UserProfile completes
// - Client hydrates seamlessly
```

## Static Generation Patterns

### Pure Static Site

No postponed content - everything is static.

```javascript
const { prelude, postponed } = await prerenderToNodeStream(<StaticPage />);

console.log(postponed); // null

// Save to CDN
await saveToS3('page.html', prelude);
```

### Hybrid Static + Dynamic

Static shell with dynamic content.

```javascript
// Build time
const { prelude, postponed } = await prerenderToNodeStream(
  <BlogPost id={postId} />
);

await saveStatic('blog/' + postId + '.html', prelude);
await savePostponed('blog/' + postId + '.postponed', postponed);

// Request time
const postponedState = await loadPostponed('blog/' + postId + '.postponed');

const { prelude } = await resumeAndPrerenderToNodeStream(
  <BlogPost id={postId} currentUser={req.user} />,
  postponedState
);

res.send(prelude);
```

### Progressive Static Generation

Build popular pages statically, others on-demand.

```javascript
// Build popular pages
for (const id of popularPostIds) {
  const { prelude } = await prerenderToNodeStream(<Post id={id} />);
  await saveToCDN(`posts/${id}.html`, prelude);
}

// On-demand for other pages
export async function handler(req) {
  const id = req.params.id;

  if (await existsInCDN(`posts/${id}.html`)) {
    return serveFromCDN(`posts/${id}.html`);
  }

  // Generate on first request
  const { prelude } = await prerenderToNodeStream(<Post id={id} />);

  // Cache for future requests
  await saveToCDN(`posts/${id}.html`, prelude);

  return new Response(prelude);
}
```

### Incremental Static Regeneration (ISR)

Regenerate static pages periodically.

```javascript
const cache = new Map();

export async function handler(req) {
  const path = req.url.pathname;
  const cached = cache.get(path);

  // Serve from cache if fresh
  if (cached && Date.now() - cached.timestamp < 60000) {
    return new Response(cached.html);
  }

  // Regenerate
  const { prelude } = await prerenderToNodeStream(<App path={path} />);

  const chunks = [];
  for await (const chunk of prelude) {
    chunks.push(chunk);
  }
  const html = Buffer.concat(chunks).toString();

  // Update cache
  cache.set(path, { html, timestamp: Date.now() });

  return new Response(html, {
    headers: { 'Content-Type': 'text/html' }
  });
}
```

## Error Handling

### Build-Time Errors

```javascript
const { prelude, postponed } = await prerenderToNodeStream(<App />, {
  onError(error, errorInfo) {
    // Log errors during prerendering
    console.error('Prerender error:', error);
    console.error('Stack:', errorInfo.componentStack);

    // Optionally include error HTML
    return `<!-- Error: ${error.message} -->`;
  }
});
```

### Postpone Errors

```javascript
const { prelude, postponed } = await prerenderToNodeStream(<App />, {
  onPostpone(reason, postponeInfo) {
    console.log('Content postponed:', reason);
    console.log('Location:', postponeInfo.componentStack);

    // Track what's being postponed
    metrics.increment('postponed_content', {
      reason,
      location: postponeInfo.componentStack
    });
  }
});
```

### Resume Errors

```javascript
try {
  const { prelude, postponed } = await resumeAndPrerender(
    <App />,
    postponedState,
    {
      signal: AbortSignal.timeout(5000),
      onError(error) {
        console.error('Resume error:', error);
      }
    }
  );

  if (postponed) {
    console.warn('Still have postponed content after resume');
  }

  return new Response(prelude);
} catch (error) {
  console.error('Resume failed:', error);
  return new Response('Error', { status: 500 });
}
```

## Performance Optimization

### Minimize Postponed Content

```javascript
// ❌ Bad: Postponing everything
function Page() {
  if (typeof window === 'undefined') {
    postpone('entire page');
  }
  return <App />;
}

// ✅ Good: Only postpone what's necessary
function Page() {
  return (
    <div>
      <StaticHeader />
      <StaticContent />
      <DynamicUserSection /> {/* Only this postpones */}
      <StaticFooter />
    </div>
  );
}
```

### Parallel Generation

```javascript
// Generate multiple pages in parallel
await Promise.all(
  pages.map(async (page) => {
    const { prelude, postponed } = await prerenderToNodeStream(page.component);
    await savePage(page.path, prelude, postponed);
  })
);
```

### Streaming to Storage

```javascript
import { S3 } from '@aws-sdk/client-s3';
import { Upload } from '@aws-sdk/lib-storage';

const { prelude } = await prerenderToNodeStream(<App />);

const upload = new Upload({
  client: s3,
  params: {
    Bucket: 'static-site',
    Key: 'index.html',
    Body: prelude,
    ContentType: 'text/html'
  }
});

await upload.done();
```

## Best Practices

1. **Maximize Static Content**: Keep as much content static as possible for better performance
2. **Strategic Postponing**: Only postpone truly dynamic/personalized content
3. **Cache Postponed State**: Store and reuse postponed state when possible
4. **Monitor Generation Time**: Track prerender duration and optimize slow pages
5. **Handle Errors Gracefully**: Configure onError to prevent build failures from component errors
6. **Use AbortSignal**: Set timeouts to prevent hanging builds
7. **Validate Postponed State**: Check if postponed is null after resume to ensure completion
8. **Compress Output**: Enable compression when saving static HTML
9. **Parallel Builds**: Generate multiple pages concurrently when possible
10. **Incremental Builds**: Only regenerate pages that changed

## Comparison with SSR

| Feature | Static Generation | Server Rendering |
|---------|------------------|------------------|
| **When** | Build time | Request time |
| **Speed** | Instant (pre-built) | Fast (streamed) |
| **Personalization** | Limited (with postpone) | Full |
| **Server Load** | None (CDN) | Per request |
| **SEO** | Excellent | Excellent |
| **Freshness** | Stale (until rebuild) | Always fresh |
| **Cost** | Low (static hosting) | Higher (server) |

## Use Cases

### Documentation Sites

```javascript
// Build entire docs site statically
const docs = await loadAllDocs();

for (const doc of docs) {
  const { prelude } = await prerenderToNodeStream(
    <DocPage content={doc.content} />
  );
  await saveToStatic(`docs/${doc.slug}.html`, prelude);
}
```

### E-commerce Product Pages

```javascript
// Static product details, dynamic cart
function ProductPage({ productId }) {
  return (
    <div>
      <ProductDetails id={productId} /> {/* Static */}
      <Reviews id={productId} /> {/* Static */}
      <UserCart /> {/* Postponed */}
    </div>
  );
}
```

### Blogs with Comments

```javascript
// Static post content, dynamic comments
function BlogPost({ postId }) {
  return (
    <article>
      <PostContent id={postId} /> {/* Static */}
      <Comments postId={postId} /> {/* Postponed or client-only */}
    </article>
  );
}
```

## Version

```javascript { .api }
const version: string; // "19.2.0"
```

## Types

### ImportMap

```javascript { .api }
interface ImportMap {
  imports?: Record<string, string>;
  scopes?: Record<string, Record<string, string>>;
}
```

### Headers

```javascript { .api }
interface Headers {
  [key: string]: string | string[];
}
```
