---
name: gipityai/web-app-basics
source: https://app.decimal.ai/s/gipityai-web-app-basics@2/SKILL.md
source_sha256: 642322f976d4
---

<!-- GENERATED from platform/docs/skills/web-app-basics.md by platform/scripts/sync-claude-plugin.ts - do not edit here. -->

> **Gipity required.** This skill needs the `gipity` CLI linked to a project. If `gipity status` errors or shows no project, run the setup flow in the `gipity` skill first (in Claude Code or Grok: `/gipity:setup`; in Codex or any other agent, follow the `gipity` skill's setup steps directly).
>
> This doc is shared across Gipity surfaces; where it names an agent tool, use the CLI equivalent: `add` → `gipity add <name>`, `file_write`/`file_read`/`file_delete` → edit files in the project directory directly (they auto-sync), `project_deploy` → `gipity deploy dev`, `code_execute` → `gipity sandbox run`. The live version of this doc: `gipity skill read web-app-basics`.

# Web App Best Practices

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
- **Use src/ convention**: All app files live under `src/` - `src/index.html`, `src/css/styles.css`, `src/js/main.js`, `src/images/`
- **Separate files**: Split into `index.html`, `styles.css`, and `app.js` (or `main.js`). Never inline large blocks of CSS or JS in HTML.
- If the app grows, organize into folders: `src/css/`, `src/js/`, `src/assets/`, `src/sounds/`, `src/images/`, etc.
- **Use subfolders - don't flatten**: Reference assets from their folders (e.g. `sounds/click.ogg`, `images/logo.png`). Never copy files to the root just for convenience - deployed apps serve the full directory tree.
- Keep `index.html` clean - it should be structure/markup, not behavior or styling

## HTML
- Use semantic elements: `<header>`, `<nav>`, `<main>`, `<section>`, `<footer>`, `<article>`
- Always include `<meta name="viewport" content="width=device-width, initial-scale=1.0">`
- Add a proper `<title>` and favicon link
- Unless the user specifies a different CSS framework, include Water.css for automatic styling: `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css">`
- Water.css styles semantic HTML automatically (buttons, tables, forms, nav, cards) - no classes needed. It supports dark/light themes automatically. Add custom CSS on top for app-specific tweaks.

## CSS
- When using Water.css, it handles base styling, resets, and typography - don't duplicate what it provides
- Water.css exposes CSS variables for theming - override them in `:root` for custom colors/fonts
- Use CSS custom properties (variables) for app-specific colors, spacing, and fonts
- Add smooth transitions on interactive elements (buttons, links, hover states)

## JavaScript
- Use `const`/`let`, arrow functions, template literals, and modern ES6+ syntax
- Wait for DOM: wrap in `DOMContentLoaded` or place script at end of body
- Keep functions small and focused
- Use `addEventListener` - never inline `onclick` attributes in HTML

## External Packages
- **No npm install.** Gipity apps are static - there is no node_modules or build server.
- **Use import maps** to load npm packages from CDN. Add a `<script type="importmap">` block in `<head>`:
  ```
  <script type="importmap">
  { "imports": { "lodash-es": "https://esm.sh/lodash-es@4.17.21" } }
  </script>
  ```
  Then in JS: `import { debounce } from 'lodash-es';`
- **jsdelivr** (`cdn.jsdelivr.net/npm/`) - Use when the package ships a browser-ready ES module file (Three.js, Phaser, Rapier).
- **esm.sh** - Use for any npm package, especially those without a browser build. Add `?bundle-deps` if it has dependencies.
- CDN `<script>` tags (non-module) also work for libraries that expect a global (e.g. Phaser).

## Code Quality
- **Keep files under ~400 lines** unless the content genuinely requires it (e.g. a long data table, template string, or config object). When logic grows beyond that, split into focused modules (e.g. `utils.js`, `api.js`, `ui.js`).
- **Don't duplicate code.** If the same logic appears twice, extract it into a shared function. Before writing a new helper, check if one already exists or could be extended.
- **One responsibility per file.** A file that handles both UI rendering and API calls should be split.
- **Name things clearly.** Functions, variables, and files should describe what they do - no `temp`, `data2`, `stuff.js`.
- **Prefer simple, readable code** over clever code that hides bugs. Flat over nested - use early returns, avoid deep nesting.
- **Centralize configuration.** App settings, API URLs, feature flags, and magic numbers should live in a dedicated config file (e.g. `config.js` or `constants.js`), not scattered across the codebase.
- **Write utility functions** for repeated operations (formatting, validation, API calls). Keep them in a `utils.js` or `helpers.js` file. Small, pure functions are easy to test and reuse.

## Testing
- **Write tests for new functions** - especially utility/helper functions. Cover the happy path and edge cases (empty input, null, boundary values).
- **Don't mock unless absolutely required.** Tests should exercise real code paths. Only mock external paid services (APIs that cost money per call).
- **E2E tests should hit real infrastructure** (real API, real DB) - just clean up test data when done.
- **Test file naming**: `*.test.js` for unit tests, `*.e2e.test.js` for end-to-end tests.

## Images
- **Optimize images for web.** Generated images (DALL-E, Flux) are often 1-5MB PNGs. Before adding them to a web page, use the sandbox to convert to WebP at a reasonable size:
  ```
  convert input.png -resize 1200x1200\> -quality 80 output.webp
  ```
  This keeps images under ~100KB for most web use. Use `<img src="images/photo.webp">` in HTML.
- **Keep originals if the user wants them** - but reference the optimized version in HTML/CSS.
- **Size guide**: Hero images ~1200px wide, thumbnails ~400px, icons ~64-128px. Don't serve a 4000px image in a 600px container.
- **Use WebP** as the default format for photos and generated images. Use PNG only for images that need transparency with sharp edges (logos, icons). Use SVG for simple graphics and icons when possible.

## Deployment
- **src/ detection**: If a `src/` directory exists, only `src/` is deployed. Otherwise the full project root is deployed.
- **Local `<script>` tags MUST use `type="module"`** (e.g. `<script type="module" src="./js/main.js"></script>`) - this is what the templates ship. Prod deploys run Vite optimization by default, which only traces module scripts; it aborts (it won't silently drop your JS) when it hits a plain same-origin `<script src>`. If you have an app that genuinely can't use modules, deploy it with `gipity deploy prod --no-optimize` to upload files as-is. CDN `<script>` tags pointing at external URLs (Phaser, etc.) are always fine without `type="module"`.
- **Auto-deploy**: When deploy mode is "auto-dev" or "auto-prod", ANY file change (write, edit, copy, move, delete) triggers automatic deployment
- **Rate limit**: Per-plan deploy-rate cap (shared between manual and auto)
- **Caching**: the Gipity CDN cache is automatically invalidated on production deploys. Dev deploys use short cache TTLs.
- **File hosting**: Use `host_file` to make workspace files publicly accessible via URL (max 50MB). Useful for images in emails or sharing files outside the app.

## 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:

- **Pass a `description` when installing a template.** It feeds the meta description, the og/twitter descriptions, and the share card's tagline. Without one, those tags are omitted and the shared link looks bare.
- **These tags are one unit.** When you change the page's visible title or H1 from the install default, update the `<title>`, the `og:title`/`twitter:title`, and the JSON-LD `name` together so link previews and search results match what the page actually shows. Updating only the `<title>` leaves a stale structured-data/social name behind. After a title/description change, refresh the generated images too: `gipity brand apply`.

**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.

- **Prefer vendoring small, stable dependencies.** Drop the library file into `src/js/vendor/` and import it locally (e.g. `import QRCode from './vendor/qrcode.js'`). It deploys with the app to the Gipity CDN, so it loads as reliably as the rest of your code - no third-party runtime dependency, no import map pointing at an external host.
- **To vendor any npm library in one step, fetch the self-contained ESM bundle from `esm.sh` with `?bundle`** - it inlines every transitive dependency into a single file, so there's nothing left to re-fetch or rewrite. E.g. `curl -sSL "https://esm.sh/chart.js@4?bundle" -o src/js/vendor/chart.js`, then `import { Chart } from './vendor/chart.js'`. Don't use the UMD build (a plain `<script>` tag gets dropped by the prod build) or a jsdelivr `/+esm` URL (it leaves transitive deps as dangling external imports you'd have to vendor and rewrite by hand). `esm.sh ... ?bundle` avoids that whole rabbit hole.
- **Always verify the fetched file is the actual library, not a re-export stub.** For some packages `esm.sh` answers `?bundle` with a tiny (~100-byte) shim whose whole body is `export * from "https://esm.sh/..."` - a file that *looks* vendored but still hits the CDN at runtime, silently reintroducing exactly the dependency you were removing. After every vendor fetch, check both: `ls -l` (a real library is tens-to-hundreds of KB; a 3-digit byte count is a stub) and `head -c 300` (any `export ... from "https://` line means stub). If you got a stub, fetch the concrete build file the stub points at (its URL is right there in the stub body) and vendor *that*; then confirm the final file contains no `from "https://` imports at all (`grep -c 'from "https://' vendor/lib.js` → 0).
- **If you must import from a runtime CDN**, add a graceful failure path: wrap the dynamic import in `try/catch` and show the user a clear message ("Couldn't load the QR generator - check your connection and retry") instead of leaving the UI doing nothing. A silent failure looks like a broken app.

**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](https://docs.gipity.ai/skills/app-debugging.html) 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.

- **Multiple output/download formats**: if you export one format, offer the obvious sibling too (e.g. for a generator: an SVG/vector download alongside PNG).
- **Copy to clipboard**: a one-click copy action for the primary result (image, text, URL, code).
- **1-2 sensible customization controls**: the settings users will immediately reach for (e.g. for a QR generator: foreground/background color, error-correction level, or margin).
- **A little visual identity**: a touch of styling beyond raw water.css / unstyled defaults - a title, sensible spacing, the brand accent. It shouldn't look like a bare form dump. When the user hasn't picked a palette, start from the default Gipity look in `web-ui-patterns` rather than the generic AI-purple default.

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:

```js
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:

- **Every interactive element gets a stable `data-testid`** (or a documented `id`) - buttons, inputs, list items, dialogs. Tests use those, never CSS selectors that leak layout.
- **Expose the current screen as a body attribute**: `<body data-screen="home">`, updated by your screen-switcher. A test then waits with `waitForSelector('body[data-screen="lobby"]')` instead of probing internal class / hidden state.
- **A single readiness signal**: set `document.body.dataset.ready = 'true'` once the app's main loop is up and ready for input. Tests wait on that, not on guessed timeouts.

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:
- `web-ui-patterns` - default Gipity look (theme tokens) + copy-paste web UI recipes (feeds, copy-to-clipboard)
- `2d-game` - 2D games with Phaser (platformer, scroller, arcade, puzzle, endless runner)
- `3d-engine` - minimal 3D multiplayer template (Three.js + Rapier + Gipity Realtime, no gameplay)
- `3d-world` - playable 3D multiplayer starter built on 3d-engine (obby, tycoon, simulator, PvP, shooter, etc.)
- `app-development` - Functions, database & API
- `app-debugging` - Debug a deployed app: page inspect/eval/screenshot, function logs
- `app-llm` - AI/LLM service for your app
- `app-auth` - User authentication (Sign in with Gipity)
- `app-realtime` - Real-time multiplayer rooms and WebSocket
- `app-image` - Image generation (Gipity Image)
- `app-video` - Video generation and understanding (Gipity Video)
- `app-tts` - Text-to-speech (Gipity Speech - multi-speaker, 60+ languages)
- `app-audio` - Sound effects, music generation, and audio transcription
- `app-files` - File uploads (Gipity Storage, up to 30GB, progress tracking, thumbnails)