Click Below to Get the Code

Browse, clone, and build from real-world templates powered by Harper.
Blog
GitHub Logo

The Resource API in Harper v5: HTTP Done Right

Harper v5's Resource API maps JavaScript class methods directly to HTTP verbs, eliminating routing and translation layers. Tables extend the same Resource class, unifying HTTP handling and data access into one interface. Key v5 additions include pre-parsed RequestTarget objects, Response-aware source caching with stale-while-revalidate support, and async context tracking via getContext().
Product Update
Blog
Product Update

The Resource API in Harper v5: HTTP Done Right

Kris Zyp
SVP of Engineering
at Harper
April 14, 2026
Kris Zyp
SVP of Engineering
at Harper
April 14, 2026
Kris Zyp
SVP of Engineering
at Harper
April 14, 2026
April 14, 2026
Harper v5's Resource API maps JavaScript class methods directly to HTTP verbs, eliminating routing and translation layers. Tables extend the same Resource class, unifying HTTP handling and data access into one interface. Key v5 additions include pre-parsed RequestTarget objects, Response-aware source caching with stale-while-revalidate support, and async context tracking via getContext().
Kris Zyp
SVP of Engineering

There is a gap that shows up consistently in application frameworks between what HTTP actually specifies and what the framework exposes to developers. HTTP's uniform interface — GET, PUT, PATCH, POST, DELETE — is precise and well-reasoned. The semantics are defined. The contract is clear. But most frameworks layer their own routing and handler abstractions on top, and the correspondence between what HTTP says and what your code does becomes indirect. You end up mapping between two systems rather than working in one.

Harper's Resource API closes that gap. In v5, it does so more directly than any previous version.

The Uniform Interface, Directly Expressed

The Resource API is a JavaScript class interface where static methods map one-to-one to HTTP verbs. A Resource class represents a collection; a Resource instance represents a single record. To define how your application responds to HTTP requests, you extend the base Resource class and override the methods that correspond to the HTTP verbs you care about.

class Product extends Resource {
    static async get(target) {
        // called for HTTP GET /Product/:id
        return super.get(target);
    }

    static async put(target, data) {
        // called for HTTP PUT /Product/:id
        return super.put(target, { ...(await data), status: 'active' });
    }

    static async delete(target) {
        // called for HTTP DELETE /Product/:id
        return super.delete(target);
    }
}

This is not a coincidence of naming. The static methods are the HTTP handlers. Harper routes incoming requests directly to these methods. There is no separate routing layer, no middleware registration, no controller-to-method mapping to maintain. The class structure encodes the URL hierarchy; the method names encode the HTTP verbs.

v5 formalizes this further by explicitly encouraging use of static methods as the primary interaction pattern, and by pre-parsing the RequestTarget before it reaches any static method. That second point matters more than it might seem.

The RequestTarget as Parsed URL

In v5, when a request arrives, Harper parses the URL into a RequestTarget object before calling your static method. RequestTarget is a subclass of URLSearchParams, so it carries the full query string as a native iterable interface, plus additional properties that Harper derives from the URL structure:

  • target.id — the primary key extracted from the path
  • target.pathname — the path portion relative to the resource
  • target.isCollectiontrue when the request targets a collection (e.g. /Product/ with query conditions) rather than a single record
  • target.conditions, target.limit, target.offset, target.sort, target.select — query parameters parsed from Harper's extended URL query syntax

The consequence is that your get method does not need to parse anything. The URL has already been translated into a structured object that reflects the semantics of the request.

static get(target) {
    const id = target.id;
    const filter = target.get('status');  // standard URLSearchParams access
    const limit = target.limit;           // parsed by Harper from URL syntax
    return super.get(target);
}

Compare this to the alternative: receiving a raw URL string, manually extracting path segments, manually parsing query parameters, and then constructing some internal query object from what you found. The RequestTarget design eliminates that translation layer. What HTTP says about the request is what your method receives.

Data Modeling Through the Resource Interface

The Resource API is also the primary modeling interface for data. Tables in Harper extend Resource, which means the same methods that handle HTTP requests are the methods you use to interact with data from application code. There is one interface, not two.

Static methods handle reads and writes at the collection level:

// retrieve a single record
const product = await Product.get(34);

// partial update
await Product.patch(34, { description: 'Updated description' });

// full replacement
await Product.put({ id: 34, name: 'New Product Name', price: 49.99 });

// create with auto-generated key
const created = await Product.create({ name: 'New Product', price: 29.99 });

// delete
await Product.delete(34);

Instance methods, retrieved via update(), allow mutable access to a single record within a transaction:

const product = await Product.update(32);
product.status = 'active';
product.subtractFrom('quantity', 1);  // CRDT-safe decrement
product.save();                        // explicit save within transaction

The save() method is new in v5. Previously, pending changes were committed only when the transaction completed. save() makes those changes visible to subsequent reads and queries within the same transaction — useful when you need to write a record and then query against the updated state before the transaction closes.

The get() method in v5 returns a RecordObject: a frozen plain object containing the record's properties plus getUpdatedTime() and getExpiresAt(). It is immutable. This is intentional. The returned object represents the committed state of the record in the database; mutation belongs to the update() flow. If you need a modified version of the record for a response, the spread operator is the explicit path:

static async get(target) {
    const record = await super.get(target);
    return { ...record, computedField: derive(record) };
}

The consistency here — a frozen read result, a distinct mutable update path — reflects the underlying transaction model. Reads are snapshots. Writes are transactions. The API makes that distinction explicit.

Caching and Sourcing: Where the Model Gets Interesting

The Resource API's treatment of caching is where the data modeling story becomes most useful. Harper supports the concept of a source resource: a resource that a caching table delegates to when a record is not found locally. This is configured with sourcedFrom():

ProductCache.sourcedFrom(ExternalProductAPI, {
    expiration: 300,   // TTL in seconds
    eviction: 3600     // eviction time in seconds
});

Once configured, reads against ProductCache transparently check local storage first, fall back to ExternalProductAPI if there is a miss, cache the result, and serve subsequent requests from local storage until expiration. Writes are delegated upstream.

The v5 changes to how source resources work make this pattern significantly more practical.

Returning a Response Object from Source

A source resource's get method can now return a standard Response object — the same type you get from a fetch() call. Harper will stream the response body and save the headers into the cached record automatically.

class ExternalProductAPI extends Resource {
    static async get(target) {
        const response = await fetch(`https://api.example.com/products/${target.id}`);
        return response;  // Harper handles streaming and header caching
    }
}

This matters because most upstream data sources are HTTP APIs. Previously, you had to extract the body, parse it, and return a plain object — discarding headers that might carry semantic information (caching directives, content type, custom metadata). Now the full response is preserved. If the upstream API returns Cache-Control or ETag headers, those are stored and can inform downstream caching behavior.

The getResponse() function, available as a named export from the harper module, provides access to the response object from within a resource method when you need it. You can use wasLoadedFromSource() on the instance to determine whether the current request was a cache miss — useful when you want to behave differently on cold cache versus warm cache.

Stale-While-Revalidate

The allowStaleWhileRevalidate() method gives you control over cache freshness policy at the record level:

static allowStaleWhileRevalidate(entry, id) {
    // serve the stale entry immediately while revalidation runs in the background
    return (Date.now() - entry.localTime) < 60_000;  // stale for less than 60s
}

When this returns true, Harper serves the cached value immediately and triggers a background refresh. When it returns false, the request waits for the fresh value. The entry object includes version, localTime, expiresAt, and value, giving you the information needed to make this decision per-record rather than globally.

The combination of source delegation, Response object handling, and stale-while-revalidate control gives you a caching layer that is not bolted on but is expressed through the same method interface as everything else in the API.

Context Without Threading

One of the practical complications in multi-request server environments is getting access to the current request from code that is not the immediate handler. If you have a utility function five calls deep in a call chain, and you need the request's authentication token or user context, the common solution is to pass the request object as a parameter through every intermediate function. This works but it is noise — most functions do not need the request, they just relay it to the next level.

v5 solves this with asynchronous context tracking. getContext(), available as a named export from the harper module or as a global, returns the current Request object — the one associated with whatever async execution context is currently running. No parameter passing required.

import { getContext } from 'harper';

function someUtilityDeepInTheCallChain() {
    const context = getContext();
    const userRole = context.user.role;
    // act on user authorization without having received request as a parameter
}

When triggered by HTTP, the context is the Request object with properties for method, headers, responseHeaders, url, ip, host, body, and data. You can set response headers through context.responseHeaders.set() from anywhere in the async context — another case where the alternative is passing state through intermediate layers.

This is the Node.js AsyncLocalStorage API applied consistently throughout Harper's request lifecycle. The mechanism is not novel, but the fact that Harper exposes it through the standard getContext() export means you do not have to set it up yourself.

The Benefits, in Summary

The Resource API's design choices compound. Because static methods map directly to HTTP verbs, there is no translation layer between what the HTTP client requests and what your code handles. Because RequestTarget arrives pre-parsed, URL interpretation is not your responsibility. Because the same methods work for both HTTP handling and programmatic data access, there is one mental model for both concerns. Because source resources can return Response objects, the caching layer preserves upstream HTTP semantics rather than discarding them. Because getContext() uses async context tracking, request state is accessible without explicit parameter threading.

None of these are individually unprecedented. What the Resource API does is combine them into a consistent interface where each decision reinforces the others. The HTTP uniform interface — which is precise and well-specified — becomes the application's data interface as well. That alignment reduces the surface area for inconsistency, and it reduces the amount of glue code that does nothing but translate between layers.

I am most interested to see how teams use the source resource pattern as systems scale. The caching and sourcing model is expressive enough to handle a range of architectures — local caching of remote APIs, tiered storage, conditional revalidation based on record-level logic — and I think the combinations people reach for in practice will be more varied than what we have anticipated.

The v5 Resource API documentation covers the full method signatures and options.

There is a gap that shows up consistently in application frameworks between what HTTP actually specifies and what the framework exposes to developers. HTTP's uniform interface — GET, PUT, PATCH, POST, DELETE — is precise and well-reasoned. The semantics are defined. The contract is clear. But most frameworks layer their own routing and handler abstractions on top, and the correspondence between what HTTP says and what your code does becomes indirect. You end up mapping between two systems rather than working in one.

Harper's Resource API closes that gap. In v5, it does so more directly than any previous version.

The Uniform Interface, Directly Expressed

The Resource API is a JavaScript class interface where static methods map one-to-one to HTTP verbs. A Resource class represents a collection; a Resource instance represents a single record. To define how your application responds to HTTP requests, you extend the base Resource class and override the methods that correspond to the HTTP verbs you care about.

class Product extends Resource {
    static async get(target) {
        // called for HTTP GET /Product/:id
        return super.get(target);
    }

    static async put(target, data) {
        // called for HTTP PUT /Product/:id
        return super.put(target, { ...(await data), status: 'active' });
    }

    static async delete(target) {
        // called for HTTP DELETE /Product/:id
        return super.delete(target);
    }
}

This is not a coincidence of naming. The static methods are the HTTP handlers. Harper routes incoming requests directly to these methods. There is no separate routing layer, no middleware registration, no controller-to-method mapping to maintain. The class structure encodes the URL hierarchy; the method names encode the HTTP verbs.

v5 formalizes this further by explicitly encouraging use of static methods as the primary interaction pattern, and by pre-parsing the RequestTarget before it reaches any static method. That second point matters more than it might seem.

The RequestTarget as Parsed URL

In v5, when a request arrives, Harper parses the URL into a RequestTarget object before calling your static method. RequestTarget is a subclass of URLSearchParams, so it carries the full query string as a native iterable interface, plus additional properties that Harper derives from the URL structure:

  • target.id — the primary key extracted from the path
  • target.pathname — the path portion relative to the resource
  • target.isCollectiontrue when the request targets a collection (e.g. /Product/ with query conditions) rather than a single record
  • target.conditions, target.limit, target.offset, target.sort, target.select — query parameters parsed from Harper's extended URL query syntax

The consequence is that your get method does not need to parse anything. The URL has already been translated into a structured object that reflects the semantics of the request.

static get(target) {
    const id = target.id;
    const filter = target.get('status');  // standard URLSearchParams access
    const limit = target.limit;           // parsed by Harper from URL syntax
    return super.get(target);
}

Compare this to the alternative: receiving a raw URL string, manually extracting path segments, manually parsing query parameters, and then constructing some internal query object from what you found. The RequestTarget design eliminates that translation layer. What HTTP says about the request is what your method receives.

Data Modeling Through the Resource Interface

The Resource API is also the primary modeling interface for data. Tables in Harper extend Resource, which means the same methods that handle HTTP requests are the methods you use to interact with data from application code. There is one interface, not two.

Static methods handle reads and writes at the collection level:

// retrieve a single record
const product = await Product.get(34);

// partial update
await Product.patch(34, { description: 'Updated description' });

// full replacement
await Product.put({ id: 34, name: 'New Product Name', price: 49.99 });

// create with auto-generated key
const created = await Product.create({ name: 'New Product', price: 29.99 });

// delete
await Product.delete(34);

Instance methods, retrieved via update(), allow mutable access to a single record within a transaction:

const product = await Product.update(32);
product.status = 'active';
product.subtractFrom('quantity', 1);  // CRDT-safe decrement
product.save();                        // explicit save within transaction

The save() method is new in v5. Previously, pending changes were committed only when the transaction completed. save() makes those changes visible to subsequent reads and queries within the same transaction — useful when you need to write a record and then query against the updated state before the transaction closes.

The get() method in v5 returns a RecordObject: a frozen plain object containing the record's properties plus getUpdatedTime() and getExpiresAt(). It is immutable. This is intentional. The returned object represents the committed state of the record in the database; mutation belongs to the update() flow. If you need a modified version of the record for a response, the spread operator is the explicit path:

static async get(target) {
    const record = await super.get(target);
    return { ...record, computedField: derive(record) };
}

The consistency here — a frozen read result, a distinct mutable update path — reflects the underlying transaction model. Reads are snapshots. Writes are transactions. The API makes that distinction explicit.

Caching and Sourcing: Where the Model Gets Interesting

The Resource API's treatment of caching is where the data modeling story becomes most useful. Harper supports the concept of a source resource: a resource that a caching table delegates to when a record is not found locally. This is configured with sourcedFrom():

ProductCache.sourcedFrom(ExternalProductAPI, {
    expiration: 300,   // TTL in seconds
    eviction: 3600     // eviction time in seconds
});

Once configured, reads against ProductCache transparently check local storage first, fall back to ExternalProductAPI if there is a miss, cache the result, and serve subsequent requests from local storage until expiration. Writes are delegated upstream.

The v5 changes to how source resources work make this pattern significantly more practical.

Returning a Response Object from Source

A source resource's get method can now return a standard Response object — the same type you get from a fetch() call. Harper will stream the response body and save the headers into the cached record automatically.

class ExternalProductAPI extends Resource {
    static async get(target) {
        const response = await fetch(`https://api.example.com/products/${target.id}`);
        return response;  // Harper handles streaming and header caching
    }
}

This matters because most upstream data sources are HTTP APIs. Previously, you had to extract the body, parse it, and return a plain object — discarding headers that might carry semantic information (caching directives, content type, custom metadata). Now the full response is preserved. If the upstream API returns Cache-Control or ETag headers, those are stored and can inform downstream caching behavior.

The getResponse() function, available as a named export from the harper module, provides access to the response object from within a resource method when you need it. You can use wasLoadedFromSource() on the instance to determine whether the current request was a cache miss — useful when you want to behave differently on cold cache versus warm cache.

Stale-While-Revalidate

The allowStaleWhileRevalidate() method gives you control over cache freshness policy at the record level:

static allowStaleWhileRevalidate(entry, id) {
    // serve the stale entry immediately while revalidation runs in the background
    return (Date.now() - entry.localTime) < 60_000;  // stale for less than 60s
}

When this returns true, Harper serves the cached value immediately and triggers a background refresh. When it returns false, the request waits for the fresh value. The entry object includes version, localTime, expiresAt, and value, giving you the information needed to make this decision per-record rather than globally.

The combination of source delegation, Response object handling, and stale-while-revalidate control gives you a caching layer that is not bolted on but is expressed through the same method interface as everything else in the API.

Context Without Threading

One of the practical complications in multi-request server environments is getting access to the current request from code that is not the immediate handler. If you have a utility function five calls deep in a call chain, and you need the request's authentication token or user context, the common solution is to pass the request object as a parameter through every intermediate function. This works but it is noise — most functions do not need the request, they just relay it to the next level.

v5 solves this with asynchronous context tracking. getContext(), available as a named export from the harper module or as a global, returns the current Request object — the one associated with whatever async execution context is currently running. No parameter passing required.

import { getContext } from 'harper';

function someUtilityDeepInTheCallChain() {
    const context = getContext();
    const userRole = context.user.role;
    // act on user authorization without having received request as a parameter
}

When triggered by HTTP, the context is the Request object with properties for method, headers, responseHeaders, url, ip, host, body, and data. You can set response headers through context.responseHeaders.set() from anywhere in the async context — another case where the alternative is passing state through intermediate layers.

This is the Node.js AsyncLocalStorage API applied consistently throughout Harper's request lifecycle. The mechanism is not novel, but the fact that Harper exposes it through the standard getContext() export means you do not have to set it up yourself.

The Benefits, in Summary

The Resource API's design choices compound. Because static methods map directly to HTTP verbs, there is no translation layer between what the HTTP client requests and what your code handles. Because RequestTarget arrives pre-parsed, URL interpretation is not your responsibility. Because the same methods work for both HTTP handling and programmatic data access, there is one mental model for both concerns. Because source resources can return Response objects, the caching layer preserves upstream HTTP semantics rather than discarding them. Because getContext() uses async context tracking, request state is accessible without explicit parameter threading.

None of these are individually unprecedented. What the Resource API does is combine them into a consistent interface where each decision reinforces the others. The HTTP uniform interface — which is precise and well-specified — becomes the application's data interface as well. That alignment reduces the surface area for inconsistency, and it reduces the amount of glue code that does nothing but translate between layers.

I am most interested to see how teams use the source resource pattern as systems scale. The caching and sourcing model is expressive enough to handle a range of architectures — local caching of remote APIs, tiered storage, conditional revalidation based on record-level logic — and I think the combinations people reach for in practice will be more varied than what we have anticipated.

The v5 Resource API documentation covers the full method signatures and options.

Harper v5's Resource API maps JavaScript class methods directly to HTTP verbs, eliminating routing and translation layers. Tables extend the same Resource class, unifying HTTP handling and data access into one interface. Key v5 additions include pre-parsed RequestTarget objects, Response-aware source caching with stale-while-revalidate support, and async context tracking via getContext().

Download

White arrow pointing right
Harper v5's Resource API maps JavaScript class methods directly to HTTP verbs, eliminating routing and translation layers. Tables extend the same Resource class, unifying HTTP handling and data access into one interface. Key v5 additions include pre-parsed RequestTarget objects, Response-aware source caching with stale-while-revalidate support, and async context tracking via getContext().

Download

White arrow pointing right
Harper v5's Resource API maps JavaScript class methods directly to HTTP verbs, eliminating routing and translation layers. Tables extend the same Resource class, unifying HTTP handling and data access into one interface. Key v5 additions include pre-parsed RequestTarget objects, Response-aware source caching with stale-while-revalidate support, and async context tracking via getContext().

Download

White arrow pointing right

Explore Recent Resources

Blog
GitHub Logo

5 Architectures for Web Personalization

Personalization is a data-delivery problem. Every architectural choice reduces to two distances: compute to user, and compute to fresh data. This piece maps five real architectures against both axes, scored on a concrete retailer workload where stale or slow data breaks the business.
Blog
Personalization is a data-delivery problem. Every architectural choice reduces to two distances: compute to user, and compute to fresh data. This piece maps five real architectures against both axes, scored on a concrete retailer workload where stale or slow data breaks the business.
Person with short dark hair and moustache, wearing a colorful plaid shirt, smiling outdoors in a forested mountain landscape.
Aleks Haugom
Senior Manager of GTM
Blog

5 Architectures for Web Personalization

Personalization is a data-delivery problem. Every architectural choice reduces to two distances: compute to user, and compute to fresh data. This piece maps five real architectures against both axes, scored on a concrete retailer workload where stale or slow data breaks the business.
Aleks Haugom
Jul 2026
Blog

5 Architectures for Web Personalization

Personalization is a data-delivery problem. Every architectural choice reduces to two distances: compute to user, and compute to fresh data. This piece maps five real architectures against both axes, scored on a concrete retailer workload where stale or slow data breaks the business.
Aleks Haugom
Blog

5 Architectures for Web Personalization

Personalization is a data-delivery problem. Every architectural choice reduces to two distances: compute to user, and compute to fresh data. This piece maps five real architectures against both axes, scored on a concrete retailer workload where stale or slow data breaks the business.
Aleks Haugom
Blog
GitHub Logo

Agentic Engineering Needs an Opinion: Why Scale Starts with Architecture

AI coding works in a sandbox because the environment is trivially narrow. Real systems have history, constraints, and blast radius. Coding agents make sound decisions only when the architecture is explicit and shared. Opinion isn't a constraint on agentic engineering, it's what makes it possible at scale.
Select*
Blog
AI coding works in a sandbox because the environment is trivially narrow. Real systems have history, constraints, and blast radius. Coding agents make sound decisions only when the architecture is explicit and shared. Opinion isn't a constraint on agentic engineering, it's what makes it possible at scale.
A smiling man with a beard and salt-and-pepper hair stands outdoors with arms crossed, wearing a white button-down shirt.
Stephen Goldberg
CEO & Co-Founder
Blog

Agentic Engineering Needs an Opinion: Why Scale Starts with Architecture

AI coding works in a sandbox because the environment is trivially narrow. Real systems have history, constraints, and blast radius. Coding agents make sound decisions only when the architecture is explicit and shared. Opinion isn't a constraint on agentic engineering, it's what makes it possible at scale.
Stephen Goldberg
Jun 2026
Blog

Agentic Engineering Needs an Opinion: Why Scale Starts with Architecture

AI coding works in a sandbox because the environment is trivially narrow. Real systems have history, constraints, and blast radius. Coding agents make sound decisions only when the architecture is explicit and shared. Opinion isn't a constraint on agentic engineering, it's what makes it possible at scale.
Stephen Goldberg
Blog

Agentic Engineering Needs an Opinion: Why Scale Starts with Architecture

AI coding works in a sandbox because the environment is trivially narrow. Real systems have history, constraints, and blast radius. Coding agents make sound decisions only when the architecture is explicit and shared. Opinion isn't a constraint on agentic engineering, it's what makes it possible at scale.
Stephen Goldberg
Blog
GitHub Logo

Building a Cozy Sandbox Game on Harper

A nature-restoration game with six biomes, 150 animals, and a real food web — built with a single Harper component as the entire backend. One YAML file wires the database, API, content seeder, and static host. The same binary ships offline on itch.io.
Shell
Blog
A nature-restoration game with six biomes, 150 animals, and a real food web — built with a single Harper component as the entire backend. One YAML file wires the database, API, content seeder, and static host. The same binary ships offline on itch.io.
Person with long wavy brown hair wearing a bright pink shirt with a teal trim, smiling outdoors in soft sunlight with blurred trees in the background.
Bailey Dunning
Forward Deployed Engineer
Blog

Building a Cozy Sandbox Game on Harper

A nature-restoration game with six biomes, 150 animals, and a real food web — built with a single Harper component as the entire backend. One YAML file wires the database, API, content seeder, and static host. The same binary ships offline on itch.io.
Bailey Dunning
Jun 2026
Blog

Building a Cozy Sandbox Game on Harper

A nature-restoration game with six biomes, 150 animals, and a real food web — built with a single Harper component as the entire backend. One YAML file wires the database, API, content seeder, and static host. The same binary ships offline on itch.io.
Bailey Dunning
Blog

Building a Cozy Sandbox Game on Harper

A nature-restoration game with six biomes, 150 animals, and a real food web — built with a single Harper component as the entire backend. One YAML file wires the database, API, content seeder, and static host. The same binary ships offline on itch.io.
Bailey Dunning
Blog
GitHub Logo

Your Website was Built for Humans. AI Needs Something Cleaner.

The web spent a decade optimizing for browsers. JavaScript-heavy rendering, dynamic CMS templates, and client-side hydration made pages beautiful and machines blind. AI answer engines retrieve, parse, and cite content directly. If your best content is trapped behind a render cycle, a cleaner source wins.
A.I.
Blog
The web spent a decade optimizing for browsers. JavaScript-heavy rendering, dynamic CMS templates, and client-side hydration made pages beautiful and machines blind. AI answer engines retrieve, parse, and cite content directly. If your best content is trapped behind a render cycle, a cleaner source wins.
Person with short dark hair and moustache, wearing a colorful plaid shirt, smiling outdoors in a forested mountain landscape.
Aleks Haugom
Senior Manager of GTM
Blog

Your Website was Built for Humans. AI Needs Something Cleaner.

The web spent a decade optimizing for browsers. JavaScript-heavy rendering, dynamic CMS templates, and client-side hydration made pages beautiful and machines blind. AI answer engines retrieve, parse, and cite content directly. If your best content is trapped behind a render cycle, a cleaner source wins.
Aleks Haugom
Jun 2026
Blog

Your Website was Built for Humans. AI Needs Something Cleaner.

The web spent a decade optimizing for browsers. JavaScript-heavy rendering, dynamic CMS templates, and client-side hydration made pages beautiful and machines blind. AI answer engines retrieve, parse, and cite content directly. If your best content is trapped behind a render cycle, a cleaner source wins.
Aleks Haugom
Blog

Your Website was Built for Humans. AI Needs Something Cleaner.

The web spent a decade optimizing for browsers. JavaScript-heavy rendering, dynamic CMS templates, and client-side hydration made pages beautiful and machines blind. AI answer engines retrieve, parse, and cite content directly. If your best content is trapped behind a render cycle, a cleaner source wins.
Aleks Haugom