How the `sxo generate` Command Works: Static Site Generation and Idempotency Explained

sxo generate pre-renders static HTML for all non-dynamic routes after a build, writing files to dist/client and marking routes with generated: true in the manifest to ensure subsequent runs skip already-generated pages.

The sxo generate command is the static-generation engine of the SXO framework (gc-victor/sxo). After sxo build compiles your application, this command walks the route manifest, server-side renders each static page, and persists the HTML to disk. Understanding its idempotent design is crucial for CI/CD pipelines and incremental static regeneration workflows.

What sxo generate Does

The command performs static site generation (SSG) for routes that do not contain dynamic parameters. It bridges the gap between a fully server-rendered application and a static site by pre-computing HTML at build time rather than request time.

The process targets the dist/server/routes.json manifest produced by the build step. For each eligible route, the command imports the compiled SSR module from dist/server/, executes the page function to obtain a full HTML document, and writes that document to dist/client/<filename>.html.

How sxo generate Works Internally

The core logic resides in src/js/generate/generate.js. The implementation follows a deterministic pipeline that ensures consistency across runs.

Step 1: Loading and Filtering the Route Manifest

The generator first reads the existing route manifest using readRoutesManifest(). It iterates over the array of route objects, applying two critical filters:

  • Idempotency check: If route.generated === true, the route is skipped entirely.
  • Dynamic parameter detection: Routes containing dynamic segments (e.g., [id]) are bypassed because they require runtime data to render.
// src/js/generate/generate.js (conceptual flow)
const routes = await readRoutesManifest();

for (const route of routes) {
  if (route.generated) continue;          // Idempotent guard
  if (hasDynamicParams(route)) continue;  // Skip dynamic routes
  
  // ... rendering logic
}

Step 2: Server-Side Rendering Execution

For each qualifying route, the generator dynamically imports the compiled server bundle from dist/server/. It invokes the route's default export or named jsx export, which returns a complete HTML string representing the fully rendered page.

// Load the compiled SSR module for the route
const mod = await importSSRModule(route.jsx);

// Execute the page function to obtain HTML
const html = await (mod.default || mod.jsx)();

Step 3: Writing HTML and Updating the Manifest

The resulting HTML is written to the client output directory at dist/client/<route.filename>. Simultaneously, the route object in memory is updated with generated: true. After all routes are processed, the modified manifest is persisted back to dist/server/routes.json via writeRoutesManifest().

await writeFile(
  path.join(outClient, route.filename), 
  html
);

route.generated = true;
await writeRoutesManifest(routes);

Why sxo generate Is Idempotent

Idempotency means that running the command multiple times produces the same result without side effects. The sxo generate command achieves this through the generated flag in the route manifest.

When the command starts, it checks dist/server/routes.json. If a route entry already contains generated: true, the generator skips the rendering and file-writing steps for that route. This prevents overwriting existing static files, preserves any manual modifications made to the HTML, and avoids redundant computation.

This design is particularly valuable in CI/CD environments where build scripts might be triggered multiple times or where incremental builds are required. The command safely resumes partial generations if interrupted, only processing routes that have not yet been marked as complete.

Production Runtime Behavior

The production server adapter, located in src/js/server/prod/core-handler.js, consumes the generated flag to optimize request handling. When an incoming request matches a route with generated: true, the server bypasses the SSR runtime entirely.

Instead of executing the JavaScript bundle to render the page, it serves the pre-computed HTML file directly from dist/client/ with a cache-control header of public, max-age=300. This reduces server load, improves response times, and enables aggressive caching while still allowing the application to fall back to dynamic SSR for routes not marked as generated.

Practical Usage Examples

Basic Generation Workflow


# 1. Build the application (creates dist/server and dist/client)

sxo build

# 2. Generate static HTML for all eligible routes

sxo generate

# 3. Serve the production build

sxo serve

Programmatic Usage

// scripts/generate.js
import { generate } from 'sxo/generate';

async function buildStatic() {
  // This can be called multiple times safely
  await generate();
  console.log('Static generation complete');
}

buildStatic();

Verifying Idempotency


# First run - processes all routes

sxo generate

# Output: Generated 12 static pages

# Second run - skips already generated routes

sxo generate

# Output: Generated 0 static pages (all routes already generated)

Summary

  • sxo generate pre-renders static HTML after sxo build completes, targeting the dist/client directory.
  • The command processes routes listed in dist/server/routes.json, skipping any with dynamic parameters.
  • Idempotency is enforced by checking the generated: true flag in the manifest; already-generated routes are skipped on subsequent runs.
  • The production server in src/js/server/prod/core-handler.js uses this flag to serve static files directly with Cache-Control: public, max-age=300, bypassing SSR.
  • Core implementation files include src/js/generate/generate.js for the generation logic and src/js/cli/commands/generate.js for the CLI interface.

Frequently Asked Questions

What happens if I modify a generated HTML file and run sxo generate again?

The command will not overwrite your changes. Because the route is already marked with generated: true in dist/server/routes.json, the generator skips that route entirely. To force regeneration, you must manually remove the generated flag from the manifest or delete the HTML file.

Does sxo generate work with dynamic routes?

No. The command explicitly skips routes containing dynamic parameters (e.g., /users/[id]). These routes require runtime data to render and cannot be statically generated at build time. The production server will handle these routes using full server-side rendering instead of serving pre-built HTML.

How does the production server know which routes are static?

The production adapter checks the generated boolean in dist/server/routes.json. If route.generated === true, it serves the corresponding HTML file from dist/client/ directly with a cache-control header of public, max-age=300. If the flag is false or missing, it executes the SSR bundle to render the page dynamically.

Where is the idempotency logic implemented?

The idempotency guard is implemented in src/js/generate/generate.js. The function generate() reads the route manifest and checks if (route.generated) continue; before processing any route. This ensures that already-generated routes are excluded from the rendering pipeline, making the command safe to run multiple times without side effects.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →