General Text & Computing Company

Publishing to the Gallery

This is the fourth page of the guide to building General Text apps. Once your app works (see the overview for building and testing), these are the finishing touches that make it land in the gallery and for anyone who meets it outside General Text: a good icon, a user-facing README, a seeded live demo, and a graceful standalone splash.

Designing the app icon

A custom SVG mark that says what the app does (a waveform for an audio tool, a branching line for a versioner) beats a stock line icon. The platform recolors every icon to a single theme tint and renders it small, so build for that:

  • One color, tintable. Draw with currentColor: stroke="currentColor" for line marks, fill="currentColor" for solid ones. Don't hard-code colors, gradients, or multiple fills; anything baked in is flattened to the tint, so a two-tone design just turns into a blob.
  • Line style, Lucide-weight. The built-in icons are line icons at stroke-width="2" on a 0 0 24 24 viewBox with fill="none", round caps/joins (stroke-linecap="round" stroke-linejoin="round"). Match that weight so a custom mark sits next to them cleanly. A simple solid silhouette is fine too; just pick one idiom.
  • viewBox="0 0 24 24", optically centered, with a little breathing room (keep the artwork roughly within a 20×20 area). Let it scale: avoid a fixed width/height, or set both to 24.
  • Keep it to a few simple paths. It's shown ~16-24px; fine detail disappears. Two or three shapes read better than twenty.
  • No scripts, event handlers, external references (<image>, remote href), <style>, gradients, or filters; they're stripped or rejected by the publish sanitizer.

Example (a tintable line mark):

{
  "kind": "svg",
  "value": "<svg viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M4 18V6m0 12h16M8 14l3-4 3 3 4-6'/></svg>"
}

App README (the gallery description)

Give your app a README and it renders on its gallery detail page (Markdown). This is a user-facing description, shown to people browsing the gallery: what the app does, how to use it, and which plaintext files it writes. It is not your repository's developer README (build steps, contributing); those serve different audiences, so keep them separate.

Two ways, matching the two app shapes:

  • Single-file: add an inline script in the <head>. Its body is the Markdown:

    <script type="text/markdown" id="gt-readme">
      # Scratch
    
      A live-syncing scratchpad. Notes persist as a plain `.md` file you own.
    </script>
    
  • Multi-file: ship a gt-readme.md (preferred), or README.md as a fallback, at your app's served web root, i.e. the build output root next to gt.json. For Vite that's the public/ folder: public/gt-readme.md (alongside public/gt.json). General Text fetches it on install. Use gt-readme.md so it stays distinct from your repository's top-level README.md, since your repo README is developer docs and is never served, so it's never used as the gallery README.

The README is optional. Keep it focused: what the app does, how to use it, what files it writes (e.g. one JSONL record per item under v1/).

Be specific about the data format, because the README is also the contract an AI agent reads. A user can point an agent at their workspace (Agent Access) and have it read and write your app's data directly. The README travels with the installed app, so it's what the agent consults first: spell out the paths under data/, the shape of each file (fields, one-record-per-line vs. one-file-per-item), and any invariants an editor must preserve. A clear README lets an agent participate in your app's data correctly instead of guessing.

Live demo (seed sample data)

Your gallery detail page has a "Try it live" button: it runs the real app in the sandbox against a throwaway, in-memory workspace: no account, nothing saved, gone on refresh. It's the best way for someone to get what your app does, so make it land with sample content instead of an empty screen.

Detect the demo session and seed once, on startup, when the workspace is empty:

await window.gt.ready
if (window.gt.mode === 'demo' && (await gt.listFiles()).length === 0) {
  await gt.writeFile('v0/items.jsonl', '{"text":"Buy milk","done":false}\n…')
  // …a few realistic records so the demo shows the app at its best
}
  • gt.mode is 'demo' in the live demo (and in the App Builder preview) and 'live' in a normal workspace. It's set by the time gt.ready resolves.
  • Only seed when the workspace is empty, never overwrite a real user's files.
  • Demo writes go to the same throwaway hub; they're never persisted, so seeding is free to be generous.

Opened outside General Text: the standalone splash + demo

Your built app can also be visited directly on its own deployed origin: someone follows the link on your repo, or lands on the URL. There is no General Text around it, so nothing injects window.gt, and an app that calls await window.gt.ready at startup just hangs on a blank screen with Uncaught TypeError: window.gt is undefined in the console. Handle it: this is the first impression for anyone who meets your app outside the gallery.

The expectation (every first-party app does this): when there's no runtime, render a small splash that says the app runs inside General Text and how to install it, and offer a "Try the demo" button that boots a local in-browser workspace and shows the same seeded sample data as the gallery demo.

Gate your mount on the runtime existing, and only load it yourself (with a local workspace) when the visitor opts into the demo:

// main.tsx — window.gt is present iff a host injected it (inside General Text, or the
// dev plugin during `pnpm dev`). If it's absent, we're standalone on the deployed site.
const HAS_RUNTIME = typeof window !== 'undefined' && !!window.gt

function loadRuntime() {
  return new Promise((resolve, reject) => {
    if (window.gt) return resolve()
    window.__gtConfig = { local: true } // force a local in-browser workspace
    window.__demo = true // your own marker → seed sample data (see below)
    const s = document.createElement('script')
    s.src = 'https://www.generaltext.org/__gt/runtime.js' // must load BEFORE you mount
    s.onload = () => (window.gt ? resolve() : reject(new Error('runtime loaded but no window.gt')))
    s.onerror = () => reject(new Error('failed to load the runtime'))
    document.head.appendChild(s)
  })
}

if (HAS_RUNTIME) mountApp()
else renderSplash({ onTryDemo: () => loadRuntime().then(mountApp).catch(renderSplash) })

Then widen the seed check from the section above so the standalone demo fills too, not only the gallery:

if ((window.gt.mode === 'demo' || window.__demo) && (await gt.listFiles()).length === 0) {
  // …seed the same sample data
}
  • The runtime must be on window before your app mounts and calls gt.ready; load it inside the promise above, not after you render.
  • Inside General Text and in pnpm dev the runtime is already injected, so HAS_RUNTIME is true and none of this runs, so behavior there is unchanged.
  • This is the same window.__gtConfig = { local: true } self-hosted-demo mechanism described under Developing and testing on the Building Apps page, driven by a button instead of a /demo route.