When building apps or websites, follow these practices for professional-quality output.

Getting Started - Start Here

STRONGLY RECOMMENDED: Start every new web app by adding a template with the add tool. Pick the right one (web-simple for static frontend-only, web-fullstack for backend+DB, api for pure API). It creates the standard src/ structure with favicon, meta tags, and working files wired up and ready to build on - no demo to delete first. Deploy automatically uses src/ when it exists. Only hand-roll files if the user explicitly tells you to skip the template.

Naming: Use the user's name verbatim if they gave one. If you need to invent a name, blend "Gip" or "Gipity" into it (e.g. "Gipity Notes", "GipPic", "Gip Tac Toe") - be creative but don't force it if it genuinely doesn't fit.

Starting over in an existing project: If src/ (or functions/, migrations/ for fullstack/api) already exists and the user wants a clean rebuild, call file_delete on those directories first, then run add normally. Or pass force=true to add to overwrite in one step - destructive, so confirm with the user first. Non-template content (media, data, notes) is preserved either way.

Where things live (web-simple) - what to edit: For a content or markup change, edit src/index.html. For visible display text (labels, button copy), edit src/js/strings.js. For styling, edit src/css/styles.css. src/js/main.js holds the app logic. The rest - config.js, i18n.js, settings.js, translations.js - is boilerplate you only open when enabling i18n or feature flags. Don't read every file before a simple edit; go straight to the one that owns the thing you're changing.

Templates install real files - Read one before you change it. add writes a full set of starter files (HTML/CSS/JS, gipity.yaml, functions, and more), already on disk with placeholders ({{TITLE}}, …) substituted - so they are not new files. A blind file_write on one you haven't read fails with "File has not been read yet", and editing from memory of the template misses the exact-string match (the title is already baked into <h1>, not {{TITLE}}) and loops. One file_read of the file you're about to change defuses both - just that file, not the whole tree.

Multi-language (web-simple): The template ships a dormant i18n system. Flip config.features.i18n to true in src/js/config.js to enable the language picker and translations.js lookup; the code in src/js/strings.js, src/js/i18n.js, and src/js/main.js is self-documenting - read those to see the render() + i18n:changed event pattern.

File Structure

HTML

CSS

JavaScript

External Packages

Code Quality

Testing

Images

Deployment

Keep page metadata consistent

Every template installs a full share/SEO head: <title>, meta description, canonical URL, the complete Open Graph + Twitter card set (including an absolute og:image), theme-color, favicon/apple-touch-icon/manifest links, and an application/ld+json structured-data object. The matching image assets (favicons, iOS Home Screen icon, PWA manifest, and a 1200x630 src/images/og-image.png share card) are generated at install from the app's title and description - so a link shared on X/iMessage/Slack shows a real card, and "Add to Home Screen" gets a real icon, with zero extra work.

Two rules keep that polish intact:

Custom app icon + share card: gipity brand set re-renders every generated asset deterministically - e.g. gipity brand set --emoji 🦍 (an emoji icon reads far more "this is my app" than the default letter), --color "#3b82f6" for the accent, --tagline "..." for the share-card subtitle. Then gipity deploy dev to publish. For fully custom art, overwrite src/images/og-image.png (1200x630) or the icon files directly (e.g. via gipity generate image) - brand apply regenerates them, so skip it after hand-replacing.

Older apps (installed before templates shipped this head): their index.html has no og:image/twitter/apple-touch-icon/manifest tags, so regenerated assets alone won't show up in link previews. Run gipity brand apply --fix-head once: it regenerates the assets AND splices the current share/SEO head block into src/index.html, preserving the page's own title and description. Then deploy.

Third-party libraries

A deployed app should not depend on a third-party CDN being up to perform its core function. The app's own files deploy to the Gipity CDN; a runtime import from esm.sh/unpkg/jsdelivr does not - if that CDN is slow, down, or the user is offline, the import fails and a feature that relies on it silently does nothing.

If the user asks for a QR code to the app/URL itself (not an in-app generator) - e.g. "put a QR code on the front desk" - actually produce the image. Generate it in the sandbox with qrencode (see the worked example in sandbox-tools), save the PNG into src/images/ so it deploys, optionally embed it on the page, and tell the user the file path. Don't hand back the URL and tell them to make the QR themselves - that leaves the explicit ask unfulfilled.

Browser Debugging

Deploy, then look at the page - never assume it worked. gipity deploy dev --inspect deploys and reports the live page in one step (console errors, failed resources, timing, layout overflow); gipity page screenshot <url> shows what it actually renders; gipity page test <url> --action <js> --observe <js> drives an interactive feature and asserts the headline behavior really works (e.g. "type a message → get an AI reply") instead of just proving the page loaded. Don't hand-roll a DOM-poking page eval script for that.

Full debugging loop → the app-debugging skill: every flag on inspect/screenshot/eval, reading function logs, calling a function directly, and what the headless browser can't test.

Build Incrementally

For non-trivial apps, don't write the whole thing in one pass. Work in small verified steps:

  1. Add a template (add tool / gipity add <template>) and deploy - confirm the starter renders.
  2. Add ONE feature or screen, deploy, verify.
  3. Repeat.

A 300+ line single-file rewrite is hard to debug - a single bad API call or typo can break everything silently. Small increments keep the failure surface tiny and let you bisect by diff.

Personal data defaults to per-user scoping

When the request implies user-private data - "my receipts", a personal vault, private notes, journals, anything storing a user's own uploads or records - default to scoping storage and listing per authenticated user via app-auth. A "my X" app where anyone with the URL sees and can delete everyone else's data is a privacy hole, not just a missing feature. So: gate writes behind sign-in and key every row to ctx.auth.userGuid (the stable external id - not the internal numeric userId; see "Using auth" in app-development), and filter listings to the signed-in user. If you intentionally ship a public or shared version instead (e.g. a community wall), that's a valid choice - but say so explicitly in your summary so the user can decide, rather than shipping public-by-default silently. Load app-auth for the sign-in flow.

Go One Step Past the Literal Request

For a single-purpose utility (a QR generator, a color picker, a unit converter), doing only the bare ask ships something that works but feels unfinished. These tools have obvious adjacent affordances that are cheap to add and clearly raise quality. Keep it scoped: pick a couple of the cheap-polish moves below, not a feature dump.

Don't bloat it - a couple of these turn a 4/5 into a 5/5; ten of them turn a simple tool into a confusing one.

For the concrete recipes behind this section - the default Gipity theme, entry lists/feeds, copy-to-clipboard - load web-ui-patterns.

Deploy Verification

Verify a deploy when it matters - the first deploy, structural changes (new pages, new frameworks, changed imports), or anything that might have broken. Skip it for trivial changes (copy tweaks, style values).

gipity deploy dev --inspect deploys and reports the live page in one step: console errors, failed resources, timing, layout overflow. A clean console is necessary but NOT sufficient for Canvas/WebGL - also capture gipity page screenshot <url> and look at it, because render failures are silent. A blank page, black canvas, or wrong-looking UI with a clean console is a real failure, not a pass.

Full loop - reading function logs, calling a function directly, driving the page: the app-debugging skill.

Games

Building a game? Don't hand-roll it - add the template and load its skill. 3D or multiplayer (obby, tycoon, PvP, shooter): 3d-engine for a blank slate, 3d-world for a playable starter - Three.js + Rapier physics + Gipity Realtime, genre recipes in the skill. 2D (platformer, scroller, arcade, puzzle, endless runner): 2d-game - Phaser 3, no build step. Simple games with no engine need (wordle, quiz, cards) stay on web-simple.

Keyboard controls must ignore keys typed into form fields. A game's global keydown/keyup listeners (WASD/arrows/Space) also fire while the player types into an <input> - a high-score name field, a chat box - and any preventDefault() there makes those letters impossible to type (the classic symptom: "I can't type W/A/S/D into the name field"). Start every global key handler with:

if (e.target.closest('input, textarea, select') || e.target.isContentEditable) return;

and clear any held-key state when a field gains focus (focusin), so the player doesn't keep moving on a key whose release the game never saw. The game templates ship this guard; hand-rolled games on web-simple/web-fullstack must add it themselves. Test name-entry UI by typing the movement keys into it.

Make it testable

A few small rules turn a flaky click test into a reliable one:

These are about ten lines of code in total. For multiplayer apps, also read the URL-param test mode pattern in app-realtime - it turns a click-driven 2-client test into two passive page loads.

Related Skills

Load the ones that match what you're building - frontend recipes, game and 3D templates, i18n, or backend services: