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 generatepre-renders static HTML aftersxo buildcompletes, targeting thedist/clientdirectory.- The command processes routes listed in
dist/server/routes.json, skipping any with dynamic parameters. - Idempotency is enforced by checking the
generated: trueflag in the manifest; already-generated routes are skipped on subsequent runs. - The production server in
src/js/server/prod/core-handler.jsuses this flag to serve static files directly withCache-Control: public, max-age=300, bypassing SSR. - Core implementation files include
src/js/generate/generate.jsfor the generation logic andsrc/js/cli/commands/generate.jsfor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →