Keep a Mostly Static Site Fast by Routing Only Requests That Need a Function

A mostly static site should send only genuinely dynamic requests through a server-side function. Keep articles, images, stylesheets, scripts, downloads, and public directory pages on the static path; reserve Functions for narrow jobs such as accepting a report, validating form input, or returning authenticated user data. On Cloudflare Pages, a deliberate _routes.json configuration file makes that boundary visible, predictable, and testable.

Without an explicit routing boundary, adding a single Function can unintentionally broaden the dynamic surface area of the entire project. That expansion creates avoidable failure modes, makes edge request limits harder to estimate, and complicates debugging. The solution is not to avoid Functions altogether, but rather to give each Function a narrow route contract and allow everything else to remain an ordinary static asset served directly from the edge cache.

Overhead Slide Gate System (Gate and Operator System) | TYMETAL

Start with a route inventory

List the site’s public paths before writing server-side code. Classify each route by the exact behavior required when a visitor requests it.

Route group Example Preferred delivery Reason
Entry pages / and /about/ Static Delivers identical content to every visitor
Articles /guides/* Static Prebuilt, cacheable editorial content
Assets /assets/* Static Versioned files requiring no dynamic processing
Directory data /data/resources.json Static Public, reviewed structured dataset
Report submission /api/report Function Validates, sanitizes, and accepts user input
Personal view /api/account/* Function Requires authenticated user context

Do not route a file through a Function merely because it contains structured data. If the exact same JSON payload can be downloaded by every visitor, it belongs on the static delivery path. Dynamic execution is justified only when the response depends on unique request parameters, requires private secrets, mutates persistent state, or enforces runtime access rules.

Define the operational requirement for every dynamic route in a single sentence: "This Function exists because the response cannot be safely or correctly produced at build time." If the team cannot complete that sentence with a clear technical justification, keep the route static until a genuine requirement emerges.

Understand the two routing layers

Cloudflare Pages Functions use file-based routing by default. Placing a JavaScript or TypeScript file inside the project-level functions directory automatically provisions a matching route.

/
 functions/
 api/
 report.js
 account/
 [user].js
 public/
 index.html
 guides/
 assets/

In this layout, /functions/api/report.js maps directly to /api/report, while the bracketed [user].js file handles variable path parameters beneath /api/account/.

The functions directory must reside at the project root, not inside the static asset folder. Conversely, the _routes.json file belongs in the final build output directory alongside the published HTML files, because it instructs the Cloudflare edge runtime which requests should invoke Functions. This distinction is easy to overlook when a build framework compiles source files from src or public into a separate dist directory. Always inspect the generated deployment folder directly to ensure the configuration file survived the build process.

Prefer a narrow include list

When a site features only a small dynamic surface, configure _routes.json to include only that explicit prefix:

JSON
{
 "version": 1,
 "include": ["/api/*"],
 "exclude": []
}

This configuration ensures that requests beneath /api/ can invoke Functions, while requests for articles, images, stylesheets, and public data files remain on the static path.

If an individual path beneath a dynamic prefix is intentionally static, exclude it explicitly:

JSON
{
 "version": 1,
 "include": ["/api/*"],
 "exclude": ["/api/status.json"]
}

Exclusion rules take priority over inclusion rules. Use exclusions for genuine exceptions rather than patching an overly broad include pattern. A configuration file containing dozens of disparate exclusions usually indicates that the include pattern is too broad. Avoid starting with /* unless the entire site requires server-side execution. A static publication with a single contact endpoint does not need every page request crossing the compute layer.

Give every Function a method contract

Route scope defines where code executes, while method scope defines which HTTP methods the endpoint accepts. An input endpoint might accept POST requests while explicitly rejecting GET, PUT, and DELETE.

JavaScript
export async function onRequest(context) {
 if (context.request.method !== "POST") {
 return new Response("Method not allowed", {
 status: 405,
 headers: { Allow: "POST" }
 });
 }
 const type = context.request.headers.get("content-type") || "";
 if (!type.includes("application/json")) {
 return new Response("Expected JSON", { status: 415 });
 }
 const body = await context.request.json();
 if (typeof body.path !== "string" || body.path.length > 300) {
 return new Response("Invalid report", { status: 400 });
 }
 return new Response("Report accepted", { status: 202 });
}

This handler represents the initial boundary check of a workflow. A production implementation still requires durable storage integrations, rate limiting, retention rules, and structured error logging.

Perform validation as close to the route boundary as possible. Reject unsupported methods, invalid content types, oversized payloads, and unverified parameters before calling downstream storage or third-party APIs. Return concise, descriptive error responses that help the client correct the request without exposing stack traces, internal variables, or architectural details.

Keep public collections outside the Function

A curated resource directory is fundamentally a static publication, even when updated on a regular schedule. Compile approved records into static HTML or a public JSON file during the build, deploy the updated artifact, and let the CDN serve requests directly from the edge cache.

For a fixture that represents an external public collection, one Korean example such as 링크모음 주소업 can be included in the reviewed data. It should behave like any other static record. Do not create a Function that fetches the destination on every visit, copies its contents, or decides in real time whether it is safe.

If editors require a submission form to propose updates, make the submission endpoint dynamic while keeping the published directory completely static. A maintainer can review pending suggestions, update the source data files, and trigger a fresh deployment. This maintains a clean separation between untrusted public input and production output.

Plan failure behavior by route

Static and dynamic routes fail differently and require distinct recovery paths. A missing static asset returns a standard 404 response, whereas a failed form submission should preserve the user's input and provide clear guidance on whether to retry.

Failure Visitor result Operator action
Static article missing Site error page and navigation Repair the broken link or restore the source file
Function rejects input Field-level correction message Review validation rules only if reports are valid
Function dependency unavailable Temporary failure with retry guidance Check backend service health and runtime logs
Function allowance exhausted Defined open or closed behavior Review routing rules and evaluate quota usage
Public JSON missing HTML directory remains fully usable Restore or re-generate the build artifact

The choice between failing open or failing closed is an architectural decision. If a Function enforces access controls or authenticates requests, falling back to a static file may expose restricted content. If the Function merely provides a non-essential convenience, serving a static fallback is often appropriate. Document this failure behavior for every dynamic route before incidents occur.

Measure the dynamic surface

Audit the routes capable of executing code and track the specific user actions that trigger them. A concise review matrix helps keep this surface under control:

Metric Desired interpretation
Included route patterns Small and tied strictly to named features
Excluded patterns Few, with a documented reason for each
Functions per feature One clear owner and single purpose
Static requests invoking code Zero unless intentionally designed
Dynamic requests without method tests Zero

Do not evaluate the routing boundary solely by looking at the functions directory. Inspect the deployed _routes.json file, framework-generated routes, edge middleware, and custom worker scripts. Modern static site generators sometimes emit routing rules automatically, so compare build output against your route inventory after major framework updates.

Test on a Preview deployment

Use a Cloudflare Pages Preview deployment to verify both content delivery and routing rules. The test must confirm that dynamic routes invoke serverless code while static routes bypass the compute layer entirely.

Ensure the test plan covers the following checks:

Test against deployment-specific permalink URLs rather than mutable branch aliases, as branch aliases change whenever new commits are pushed. Record the specific deploy ID, routing configuration, and test outcomes in the release log. Note that Cloudflare Pages Functions require deployment via connected Git repositories or the Wrangler CLI; Direct Upload via the web dashboard only supports purely static deployments.

Promote and monitor deliberately

Promote only the exact build artifact whose output directory and routing rules were inspected in Preview. Following production deployment, repeat a brief signed-out verification on the live domain.

Monitor dynamic function errors separately from static 404 events. A missing article indicates a content maintenance task, whereas a spike in validation errors on /api/report points to a broken client integration or abusive traffic. Combining both signals into a single alert channel obscures real operational issues. When decommissioning a Function, remove its routing rule and frontend client calls within the same release to prevent orphan routes and unexpected edge behavior.

Release checklist

A static site becomes far easier to operate when its dynamic boundary is small enough to explain in a single sitting. Let the platform serve public assets directly, route only the requests that require server-side computation, and verify that separation on every release. This approach keeps baseline content fast and resilient while maintaining a secure environment for features that genuinely need a backend.