Web Device: Declare a Website, Then Pick the Right Path

Declaration first; path selection follows.
AFS (Agentic File System) is the unified directory structure ARC uses for data, apps, and configuration. In ARC, a "device" is a way of turning what's under an AFS directory into a surface for something else to see. Inconveniently, both roads to a webpage are called a device, which makes them easy to conflate, and I still catch myself doing it. One is /dev/ui/web: it treats the browser as a device you can operate live, rendering AUP (Agentic UI Protocol, ARC's declarative language for what an interface looks like) right there in the browser. Click it, drag it, it responds instantly. That's UI Device. The other is Web Device: declare a .web/ folder anywhere in an AFS directory, and the data and AUP underneath get rendered server-side into a website. Header, footer, navigation, SEO, all the site-wide plumbing just shows up.
Neither one outranks the other. They're closer to a terminal and a printer. UI Device is the terminal, it moves the moment you click. Web Device is the printer, you feed it .web/ and AUP, it spits out a whole page, once per feed, and it doesn't keep running on its own. This piece covers the whole thing: where it came from, what problem it solves, exactly how to declare one, what you get for free, and which situations call for which path.
Origin: declaration before live interaction
This wasn't designed top-down from theory. It started with a specific headache: the team was maintaining several domains by hand, running the old static-site-generator routine, write the HTML or Markdown, run a build script, deploy. Every content change meant a rebuild and a redeploy. Nothing about it was declarative. Add a new domain and the whole routine ran again, and none of the sites shared anything, each one kept its own copy of the header, footer, and SEO config, so changing a brand color meant changing it in every one of them.
By February 2026, the reasoning was straightforward: AFS already had AUP, which can render any directory into a UI. So why couldn't a website be an AFS device too? Web Device took shape from there, and the split from UI Device was baked in from day one: where the rendering happens, in the browser or on the server, decides what each one can do. UI Device can hold a live session; Web Device can't. What it gets in exchange is that the site-wide layer needs no code, and AUP renders exactly as expressive as it's built to, nothing lost in translation.
What it's for: data is the source of truth, declare it and it's a website
The problem this was actually built to solve isn't "how do we render a webpage dynamically," it's the reverse: how do you keep data that already exists in AFS from having to be hand-written into a webpage a second time. What a team is doing, a product's status, every article, all of that already exists in some structured form in AFS. Before Web Device, turning that data into a website still meant someone writing HTML separately, wiring up templates, plugging in a CMS. That's duplicated work, and it's the root of drift, too, the data changes, the webpage doesn't get the memo, and the two start telling different stories.
Web Device cuts that chain out. Data in AFS is the source of truth; declare a .web/ and the website re-renders itself whenever the data changes, no one has to manually keep the page in sync with the data. Internally we have a working principle called "Everything is Context": what's in AFS is the one fact, and a website, a doc, an interface, are all just presentations of that fact, none of them deserve a separately maintained copy. Web Device is that principle applied to one specific presentation, the website.
The benefits you can actually feel: bring up a new domain and you don't build a deploy pipeline from scratch, declare a handful of files under .web/ and the site is there. What used to be a separate copy per domain is now a shared declaration; change it once and every site using that .web/ picks it up. The rendering pipeline also does a few more things on its own, more on that below.
How to use it: what a .web folder actually looks like
Web Device assembles a site from two layers of declaration. .web/ covers the site-wide stuff, header, footer, navigation, SEO templates, and that just appears once you declare it, no code needed. A single page's content doesn't live in .web/, it goes under pages/<route>/ instead, written in AUP, a syntax rather than traditional rendering code, but a syntax you do have to learn.
The site you're reading this on is a live example. Here's what blocklets/arcblock/.web/ looks like in the arcblock-site repo, if you want to see the mechanics up close:
.web/
├── domain # arcblock.io
├── tone # editorial
├── palette # neutral
├── color-scheme # light
├── render-mode # static
├── locale / locales # default language, supported languages
├── tokens.json # design tokens
├── seo/
│ ├── title-template # "%s | ArcBlock"
│ ├── description
│ └── og-site-name
├── template/
│ ├── header-actions
│ ├── footer
│ ├── footer-links
│ ├── navigation
│ ├── docs-actions
│ └── logo.svg
├── components/
│ └── site-footer/ # manifest.json + render.js + style.css, a custom component
├── redirects / legacy-redirects.csv # redirect table
└── tag-slugs # tag display name to slug mapThese files split into two kinds. Most are one line of config: domain is one line, arcblock.io. tone is one line, editorial. render-mode is one line, static. A few, like redirects and tag-slugs, are tables, one record per line, still declarative rather than rendering logic. The one place code genuinely shows up is components/: skip it if the built-in components cover what you need, and only when you want a behavior none of them has do you write a render.js under components/<name>/ yourself. This site's site-footer is exactly that, a small amount of custom logic for redirecting domains during local development.
A single page's content doesn't live in .web/, it goes under pages/<route>/: one layout.aup for the page structure, one seo/ folder for that page's SEO title and description. layout.aup is written in AUP's declarative syntax, one field per line of copy per language. Here's the simplest possible example, roughly what a page looks like:
page index {
i18n {
title from "home.title" { en "ArcBlock" zh "ArcBlock" }
}
}Declaring it doesn't put it live immediately. Web Device takes the .web/ global declaration and each page's declaration and renders them into a preview area first; once it checks out, that becomes the live version. Change one field and Web Device only re-renders the pages that change actually touches, not the whole site every time. No template engine in between, and no one has to babysit a build pipeline.
What you get for free
Declaring .web/ and a few pages doesn't just get you a working website. Three more things ride along on the rendering pipeline itself, and the site's author doesn't configure any of them:
Every render gets an SEO check for free. Is the title the right length, is there a description written for search engines, do images have alt text (so screen readers and search engines can tell what they're showing), are the title and cover image that show up when someone shares the link on social media set. These are the kind of details that used to either get checked by hand or not at all, and now they get flagged the moment something's missing, instead of finding out after a search engine dings you for it or a shared link shows up with a blank cover image.
Internal links get verified automatically. Pages link to each other, and whether the target actually exists gets checked as part of rendering into preview. A broken link, the classic hand-built-website problem, becomes something the pipeline catches before it ever goes live, not something a person has to go hunting for after the fact.
Visitors get basic behavior tracking, and a rough sense of who's actually visiting. Dwell time, scroll depth, where they came from, the usual signals get collected. More useful is that it sorts visitors into rough buckets based on the clues left behind by the request itself: real people, search engine crawlers, AI agent crawlers, the bots that fetch a preview when something gets shared on social media, and a catch-all bucket for whatever doesn't match. It's not guaranteed to get every single one right, but it's enough to see the rough shape of the mix. With how much traffic these days is actually AI reading your site, that distinction is meaningful on its own, and it comes with declaring .web/, no third-party analytics tool required.
None of this is a planned feature. It's code already running in the rendering pipeline, turned on the moment you declare .web/.
Two paths, which one
There are two situations where Web Device isn't the answer, and /dev/ui/web is.
The first is anything that needs to stay "alive." A form that validates your input as you type, a list that reorders the moment you drag an item, several people editing the same document and seeing each other's cursors move. All of that needs an open, ongoing session, and Web Device doesn't keep one.
The second is when the thing operating the page isn't a person, it's an agent. Say an agent needs to carry a user through a multi-step setup flow, clicking through one screen after another. UI Device has an event channel an agent can click, navigate, and drive through like a person would. Web Device only outputs an HTML string, with no channel to drive; an agent can read it, not operate it.
There's a more extreme case too: if what you need depends heavily on browser-only capabilities, real-time audio and video, a complex animation engine, neither path is the answer, and that's a different conversation entirely.
Outside those scenarios, laying the two paths side by side makes the difference clearer:
| Dimension | UI Device (/dev/ui/web) | Web Device (.web/) |
|---|---|---|
| Interaction | Live: clicks, drags, form validation all respond instantly | None: you get a page that's already rendered, a new request only happens when you click a link |
| Render timing | The moment you connect and act, continuously | Once, when data changes or a page gets requested; the rendered page stays servable until the next data change |
| SEO | Not the point, content lives inside a session search engines can't see | The core requirement, checks included |
| Output | A live surface you can operate, kept in sync over an open connection | A fixed HTML page, you get it the moment you open the link, no extra connection needed |
| Typical use | Forms, drag-to-reorder, collaborative editing, agent-driven interaction | Content pages, articles, product pages, a company homepage |
"Render timing" is the row most likely to trip you up: Web Device isn't sitting there waiting for you to show up. It's more like a printer spitting out a page, once it's out it just sits there, and whoever looks at it sees the same page, until the data changes and a new one gets printed to replace it.
Two paths, not one
Back to where this started: /dev/ui/web and Web Device aren't in a replace-each-other relationship. One lets people and agents operate something live; the other takes what's already a fact inside AFS and declares it straight into a proper website, with SEO checks, broken-link detection, and visitor analytics riding along for free. If you want to see it in the flesh, the .web/ folder in the arcblock-site repo is right there. That layer you can reuse by copying files. A single page's structure and copy, you still have to write in AUP yourself.
Both being called a device isn't a coincidence, either, they grew out of the same place. Both are two ways of outputting the same AUP semantics, one to a browser for live use, one into a page for publishing. Once that's straight, the rest is just putting files in the right place in the directory tree, and picking the right path.
Referenced here
Products
-
ARC
active
The runtime for Blocklets. It gives a developer a place to run an application described as a Blocklet, together with the resources that Blocklet declares it needs.
Terms
-
AFS
AFS (Agentic File System) turns the files, services, and active work relevant to a task into an inspectable resource view. Instead of handing an agent an undifferentiated machine or a pile of APIs, it gives the task a world with names and boundaries.
-
ARC
ARC is the runtime for Blocklets. It gives a developer a place to run an application described as a Blocklet, instead of making the runtime, the application, and its resource connections one unexplained deployment.
-
AUP
AUP (Agentic UI Protocol) lets an application express what is shown and what actions are available. A runtime then renders the parts that fit its device capabilities, instead of treating one screen's pixels as the application itself.
-
Device
In ARC, a device is a way of turning what is under an AFS directory into a surface for something else to see.