agentsclimarketplace

Build artifact app

Skill jpvarbed/artifact-studio-tools/skills/build-artifact-app

Use when building a real interactive web app to host on Artifact Studio (apps go live at <slug>.jasonv.app), not just publishing a file you already made. Covers designing it, building it as a multi-file React app from esm.sh with no build step, adding the optional KV backend, and deploying + redeploying with the artifact CLI. Use whenever the task is "build and ship an app/tool/page" and the output should be a live URL.From its SKILL.md

Install
npx -y skills add jpvarbed/artifact-studio-tools --skill build-artifact-app

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

11.1 KB, ~2.9k tokens by cl100k_base, as published. Nobody here has run it

Build an Artifact Studio app

Design, build, and ship a real app to a live URL at <slug>.jasonv.app. Apps are multi-file static sites served full-page on their own origin with network access, so they load dependencies straight from a CDN. There is no build step. For publishing a one-off file you already made (an SVG, a Markdown page), use the share-artifact skill instead.

Default to a multi-file React app from esm.sh. A single self-contained HTML file is the exception, for something trivial. Real interactivity or more than ~100 lines means React + esm.sh.

1. Design first

Don't jump to code. Spend a moment on what you're building and how it should feel.

  • If the ask is open-ended, sketch the idea before coding (a brainstorming skill helps if you have one).
  • Make it feel built, not templated. A clear visual direction plus the small details that read as care: concentric border radii, tabular-nums on changing numbers, a subtle scale-on-press, staggered enter animations. (If you have frontend-design or make-interfaces-feel-better skills, use them.)
  • Decide: does it need to store data across visits? If yes, you need the KV backend (step 3).

2. Build — no-build or a real build step

Two ways, same deploy. The host only serves static files, so pick whichever fits:

  • No-build (htm + esm.sh) — a folder of index.html + JS modules that import deps from a CDN at runtime. Zero tooling, instant. Great for quick/simple apps. Mind the htm gotchas below.
  • Real build step (Vite + React + TypeScript) — author in .tsx with real JSX, types, and HMR; vite build emits a dist/ you deploy. No htm gotchas. Reach for this once the app is non-trivial or you want types. Jump to "Real build step" below, or copy examples/guestbook.

No-build: multi-file React from esm.sh

A folder with index.html at the root, plus JS modules. Pin esm.sh versions. Each app is served at the root of its own origin (<slug>.jasonv.app), so relative (./app.js), root-absolute (/app.js), and CDN (https://…) paths all resolve — relative is safest.

index.html:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>My App</title>
    <link rel="stylesheet" href="./styles.css" />
    <script type="importmap">
      { "imports": {
        "react": "https://esm.sh/[email protected]",
        "react-dom/client": "https://esm.sh/[email protected]/client",
        "htm": "https://esm.sh/[email protected]"
      } }
    </script>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="./app.js"></script>
  </body>
</html>

app.js uses htm for markup, not JSX. There's no transform step, so JSX would not run.

import React, { useState } from "react";
import { createRoot } from "react-dom/client";
import htm from "htm";
const html = htm.bind(React.createElement);

function App() {
  const [n, setN] = useState(0);
  return html`<button onClick=${() => setN(n + 1)}>count ${n}</button>`;
}
createRoot(document.getElementById("root")).render(html`<${App} />`);

Gotchas that will bite you:

GotchaFix
&lt;slug&gt; shows up literally in outputHTML entities in htm template literals render as text. Use a JS expression: ${"<slug>"}.
Multi-line <pre>/code mashes onto one linehtm collapses whitespace (incl. newlines) between elements, so one <span> per line gets concatenated. Render the block as one string with real \n: html`<pre>${code}</pre>` where code = ["line1","line2"].join("\n"). (String children keep their newlines; only inter-element whitespace is dropped.)
Styled <button>/<input> text is dark/invisibleButtons & inputs do not inherit color — they default to the browser's (dark) UA color. Set color: var(--ink) (or your theme color) explicitly on every interactive element.
App crashes white-screen with React error #62An inline style="..." string. React wants style as an object, and htm passes the attribute through verbatim. Use style=${{ marginTop: "10px" }}, or just a CSS class.
Bare <script> JSXNo build step. Use htm, or precompiled output.
Asset path confusionEach app owns its origin (<slug>.jasonv.app), so ./foo.js, /foo.js, and CDN URLs all work. Prefer relative.
import "react" failsPin it in the importmap ([email protected]), don't rely on bare specifiers resolving.
No index.html at root/ 404s. The root document must be index.html.

Real build step: Vite + React + TypeScript

When you want JSX/.tsx, types, and HMR, use a normal Vite app and deploy its build output — the htm gotchas above don't apply (real JSX handles style objects, whitespace, and entities correctly).

bun create vite@latest my-app -- --template react-ts   # or copy examples/guestbook
cd my-app && bun install
bun run build                 # → dist/
artifact deploy ./dist --slug my-app --visibility public
  • Set base: "./" in vite.config.ts so the built index.html references relative ./assets/... (resolves on the app's own origin).
  • Put llms.txt and any other root files in public/ — Vite copies them to the dist root.
  • The loop becomes: edit srcbun run buildartifact deploy ./dist. Everything else — versions, --staging, rollback, the KV backend below — is identical; you're just deploying a folder.
  • Working example with both KV modes: examples/guestbook (real .tsx).

3. Optional KV backend

If the app stores data, provision a per-app key-value store, then read/write from the frontend (same origin):

bun run cli backend <slug>     # prints a per-app data key (shown once); embed it in the app
await fetch("/api/kv/scores/top", {
  method: "PUT", headers: { "X-Data-Key": KEY }, body: JSON.stringify(value),
});
// GET → { value }; GET /api/kv/<collection> lists. No X-End-User = app-shared data.

Per-user data (ART-5). Send an X-End-User header and the rows become private to that visitor. Mint a per-visitor secret once and keep it in localStorage — different visitors can't see each other's data, and the same visitor gets theirs back on return:

const EU = localStorage.getItem("eu") ?? (() => { const v = crypto.randomUUID(); localStorage.setItem("eu", v); return v; })();
const h = { "X-Data-Key": KEY, "X-End-User": EU };
await fetch("/api/kv/notes/draft", { method: "PUT", headers: h, body: JSON.stringify(text) });
const { value } = await (await fetch("/api/kv/notes/draft", { headers: h })).json();

It's capability-based, not a login: the id is a secret per device (not cross-device, not authenticated). Drop X-End-User for shared state (leaderboards, global counters); include it for "my stuff."

The data key only exists after the app does, so it's a three-step bootstrap: deploy once, run backend <slug> to get the key, embed it, then deploy again. After that, normal redeploys.

4. Add an llms.txt (do this by default)

Every app should ship an llms.txt — a short markdown manifest served at <slug>.jasonv.app/llms.txt so another agent can understand and drive your app without reading the source. It's the agent-native README. Write one for every app unless it's truly throwaway.

For a multi-file app, just put llms.txt in the folder — it deploys with everything else and is served at /llms.txt automatically (a folder file always wins). For a single-file share, pass --llms ./llms.txt. (Under the hood it's also a first-class llmsTxt field on the API/MCP, ≤64 KB, and it honors the app's visibility — an unlisted app's llms.txt needs the ?k= token.)

Template:

# <App name>

> One-sentence description of what this app does.

Two or three sentences: what a visitor or agent can do here, and how it behaves.

## Routes
- `/` — the main view
- `/<other>` — (list any additional pages)

## Data API
- KV collection `<name>`: key = `<shape>`, value = `<shape>`. Per-visitor (X-End-User) or shared.
- (omit this whole section if the app has no backend)

## Notes for agents
- How to drive it programmatically: query params, expected inputs, anything non-obvious.

Keep it current: update llms.txt whenever routes or the data model change, and redeploy.

5. Deploy

Credentials: set ARTIFACT_API_KEY (mint one at studio.artifacts.jasonv.dev → Settings; the share-artifact skill has the details). The API base defaults to the hosted studio.

bun run cli deploy ./my-app --slug my-app --visibility public --title "My App"
# build-step app? deploy the output folder instead:  bun run cli deploy ./dist --slug my-app

Prints https://my-app.jasonv.app. Unlisted links carry a ?k= token.

Redeploy = a new immutable version. Re-run deploy with the same --slug: same URL, same token, but each deploy is a saved version. Files you drop from the folder are just absent in the new version (no prune). Leave --visibility off to keep the current setting.

  • artifact rollback <slug> [version] — undo a bad deploy (live points back at an earlier version).
  • artifact deploy <dir> --slug x --staging → preview at x--staging.jasonv.app without touching live, then artifact promote x.
  • artifact versions <slug> — list versions (live/staging marked).

The loop is build → deploy → tweak → deploy, with rollback as the safety net and --staging when you want to review before going live.

6. Verify before you hand it over

Load the live URL in a browser, confirm it renders and the console is clean, and screenshot it. Don't claim it works unseen. The most common live failure is a wrong esm.sh path or a relative-path 404, both visible in the console.

Eyeball these — they're the gotchas above, made visible (a clean console won't catch them):

  • Console + network clean (no failed module/asset loads).
  • Code/preformatted blocks render on separate lines, not mashed onto one (htm whitespace collapse).
  • All text is readable — no dark-on-dark, especially button/tab/card labels (the no-color button trap).
  • No literal &lt; / &gt; / &amp; showing as text anywhere (the htm entity trap).
  • Every interactive element (buttons, tabs, inputs) actually responds.
  • curl -s https://<slug>.jasonv.app/llms.txt returns your manifest (not "Not found") — the agent-readable description shipped.

Caches lie when verifying a fix. After a redeploy, a browser (and a headless/automation browser's persistent cache) can keep serving the old app.js/styles.css and make a correct fix look broken. Before concluding a fix didn't land, check the source of truth: curl -s https://<slug>.jasonv.app/app.js | grep <your-change>. If the server has it, it's just cache — hard-refresh (Cmd/Ctrl+Shift+R).

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,782. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.