# The Store's source Every item in the wayBetter Store is one folder here. A pull request that adds a folder adds an item; nothing else has to be written by hand. The catalog, the item's page on waybetter.fyi/store/, its card, search entry, link preview, structured data and sitemap entry are all generated from these folders. ``` store/ featured.json the storefront's "Featured" shelf: ["x/theme/nord", …] x/ one folder per app id ("x" = wayBetter for X) plugins// .plugin.js the plugin, exactly as it installs item.json { "category": "tools", "tags": ["notes", "profiles"] } preview.svg | preview.png optional areas// item.json an area plugin's page: { "category", "tags", "license", "credits", media } themes// theme.json the theme, as Appearance → Export writes it item.json { "version": "1.0.0" } preview.svg | preview.png optional strings// .strings.json a string pack (waybetter/docs/STRING-PACKS.md): name, authors, version, description and language are the pack's own; every string must fit the app's inventory (node scripts/check-store.mjs checks it) item.json { "category": "language", "tags", "license", "credits" } icons// .icons.json an icon pack: data only, at most 120 KB; its "id" is the folder name; name, author, description and license are the pack's own item.json { "category": "icons", "version", "tags", "license", "credits" } preview.svg | preview.png optional ``` The icon packs `wb-line`, `wb-classic`, `wb-aero` and `wb-pixel` are the ones the app bundles, published under the same ids (installing one changes nothing); `retro-98` and `retro-xp` are Store-only. Their `*.icons.json` and `preview.svg` are written by `node scripts/build-store-icons.mjs` from `apps/desktop/src/inject/packs/` (`node scripts/check-store.mjs` fails when they differ). The area plugins (`areas//`) are also listed in the signed catalog as `builtin` items for the app's own Store (no file; the build writes them from the area folders and the app's words, so there is nothing to author). The site keeps its own page for each area. The folder name is the item's **id**: `a-z`, `0-9` and `-`, at most 40 characters, unique among the app's plugins (or themes). It is also the item's address: `/store/plugins//`. ## What goes where Everything the app shows on its install screen comes from the item's own file, so the Store and the install screen can never disagree. `item.json` holds only what the file cannot say. | | Plugin | Theme | | --- | --- | --- | | Name, author, description | the header: `@name`, `@author`, `@description` | `name`, `author`, `description` in `theme.json` | | Version | the header: `@version` | `"version"` in `item.json` | | Permissions | the header: `@permissions` | none (a theme is data) | | Category | `"category"` in `item.json`: `reading`, `writing`, `filters`, `focus` or `tools` | `dark` or `light`, from the theme's `mode` (or `"category"`) | | Tags | `"tags"` in `item.json`: up to 12 search words | the same (default: the category) | | Preview | `preview.svg` or `preview.png`, else a card drawn from the name and category | the same, else a miniature drawn from the theme | Rules the checks enforce: - **Description**: at least 50 characters that say what it does. The app keeps the first 300 of a plugin's; search engines show the first 160 or fewer, cut at a sentence. - **Plugins** start with a `/** … */` header that has `@name`, `@version`, `@description`, `@author` and a `@permissions` line naming `network` or `network:none`. `network:none` is refused when the code calls `fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource` or `sendBeacon`. The gated API (`api.storage`, `api.ui`, `api.toast`, `api.copyText`, `api.account`, `api.users`, `api.commands`) must be declared. No `eval`, `new Function`, `import()` or added `