Imported from dsplay/template-rss (
AGENTS.md). Install upstream withnpx skills add dsplay/template-rss. Copyright stays with the author.
AGENTS.md
Guidance for AI agents (and humans) working in this repository.
What this project is
The DSPLAY RSS template — a React app built with Vite, showing a single news/RSS item (title, description, optional image, optional QR code). Requires Node.js 22.22.2+, 24.15.0+, or 26+ (see .nvmrc). See README.md for the template's variables and expected media shape.
Directory structure
index.html <-- Vite entry point
vite.config.js <-- includes @dsplay/template-manifest's Vite plugin (see below)
public/
dsplay-data.js <-- mock DSPLAY data for local development
test-assets/ <-- dev-only assets, excluded from the release build
src/
index.jsx <-- React entry point
setup-tests.js <-- Vitest setup (referenced by vite.config.js)
util/defaults.js <-- default values for the color/background Template Vars
components/
app/ <-- top-level component (loader, fonts)
news-item/ <-- lays out title/image/description
title/ <-- logo + title text
description/ <-- description text + QR code
image/ <-- item image, hidden on banner formats when unset
logo/ <-- logo image
qr-code/ <-- QR code linking to media.link
fit-text/ <-- local FitText wrapper (see note below)
debugger/ <-- dev-only overlay dumping media/template/config/screen-info, toggled by the `debug` variable
intro/ <-- loading placeholder
build.sh <-- zips the Vite build output into template.zip
File and folder naming
- kebab-case everywhere in
src/(and anywhere else in this repo we author ourselves) — folders, JS/JSX files, Sass files, test files. Doesn't apply to files whose name is a fixed convention from tooling (package.json,vite.config.js, etc.) or to vendored/third-party assets we don't control the naming of. - Author styles as
.sass(indented syntax), never.css— this applies to our own hand-authored stylesheets specifically; it does not apply to vendored or tool-generated CSS we don't hand-edit (a self-hosted Google Fonts@font-facefile, a Flaticon/IcoMoon icon-font export, a vendored library like Bootstrap) — those stay.csssince they'd be regenerated/replaced wholesale, not edited by hand..sass's indented syntax has no braces or semicolons — converting a.cssfile means rewriting it to the indented syntax, not just renaming it. - Every component gets its own folder with an
index.jsx. For a simple component,index.jsxis the component. For one that grows into several files,index.jsxbecomes a barrel re-exporting the folder's public API. - Always import a component by its folder, never by reaching into
index—import Title from '../title', never.../title/index. - Enforced automatically by ESLint's
unicorn/filename-caserule for the naming half of this; the folder+index.jsx+import-by-folder structure is not machine-checked, just convention. src/components/fit-textduplicates@dsplay/react-template-utils's ownFitTextcomponent (whichsrc/components/descriptionuses directly) with a slightly different config (maxFontSize: 1000vs10000, no ready-state visibility hiding). Pre-existing duplication, not touched during the 2026 migration — worth consolidating onto the shared component at some point, but that's a behavior change (adds hide-until-ready), not a pure refactor, so it wasn't done without a deliberate decision to do so.
Runtime model
public/dsplay-data.jsdefinesdsplay_config/dsplay_media/dsplay_templatemock globals used only in development.build.shblanks its content in the production build — the DSPLAY Android app injects the realwindow.DSPLAY.getData()before any script runs.- This is a JSON-service-backed media type — most of what's on screen comes from
media.*(see README's "Expected media data" section), not from Template Vars. Template Vars here only control styling (colors) and theshow_qr_code/debugtoggles. @dsplay/react-template-utilsexposesuseTemplateVal/useTemplateBoolVal/useMedia/useScreenInfo/Loader/FitText/QrCode(used throughoutsrc/components/).- **Always read template data through
@dsplay/react-template-utils's hooks (useTemplateVal/useTemplateBoolVal/useTemplateIntVal/useTemplateFloatVal/useTemplate()/useMedia()/useConfig()), called inside the function component that uses the value — never call@dsplay/template-utils's vanillatval/tbval/tival/tfval/config/media/templatedirectly, and never read them at module scope as a one-time constant.@dsplay/template-utilsshould not appear as a direct dependency in this template'spackage.json(it's still pulled in transitively via@dsplay/react-template-utils). - New
dsplay_templatevariable keys should usesnake_case(e.g.background_color, notbackgroundColor) — the DSPLAY CMS Manager auto-generates each variable's on-screen label from its key name, and snake_case reads more naturally there. This only applies to variables added from now on — never rename this template's existing keys just to match, since they're already registered/in use in production CMS configurations.
Template variable manifest
vite.config.js registers @dsplay/template-manifest's Vite plugin, which on every build statically scans src/ for tval/useTemplateVal-style reads and captures public/dsplay-data.js as example data, writing template-variables.json + template-example-data.json into the build output — and therefore into template.zip (npm run zip runs build.sh, which zips the whole build output). The DSPLAY CMS reads these two files to auto-detect a template's variables and seed default preview values, instead of requiring manual registration. See @dsplay/template-manifest for exactly what it detects.
Known CMS registration gap
Cross-checked against the CMS's actual registered variables for this template (tbl_template_var, canonical template id 1059): it also registers a bg (image) variable that this codebase's code doesn't read anywhere — likely a planned background-image feature that was registered in the CMS but never wired up in code, predating this migration. Left as-is (not invented/implemented here) since implementing it would be a feature addition, not a migration task — flagged here for whoever picks it up next.
Browser/WebView compatibility (Android SDK 23 minimum)
DSPLAY's Android app supports devices back to Android 6.0 (API 23). On locked-down signage hardware that never receives WebView updates via Play Store, the actual JS engine can be stuck around the Chrome ~40-51 era that shipped with that OS generation — not a modern evergreen browser. @vitejs/plugin-legacy exists specifically to cover this: it builds a modern ES-module bundle plus a transpiled+polyfilled "legacy" nomodule bundle for anything the browserslist target in package.json doesn't natively support.
Two things must never regress, or the legacy bundle silently stops protecting old devices while still looking correctly configured:
package.json'sbrowserslistmust keepChrome >= 45andAndroid >= 4.4(alongside the generic>0.2%/not dead/etc. entries) — dropping these two narrows the resolved target list to whatever's "current" (verify withnpx browserslist), which silently stops emitting transpiled code for anything old, even though@vitejs/plugin-legacystays nominally wired up.vite.config.js'sbuild.minifymust stay'terser', not the defaultoxc—oxc's minifier has a known bug where it reintroduces?./??into the legacy chunk after Babel already expanded them away, silently breaking the one guarantee the legacy build exists to provide.
After touching either of these, verify by actually running npm run build and grepping the emitted build/assets/index-legacy-*.js for untranspiled arrow functions (=>) or real ?./?? usage — a config that looks right can still emit a broken legacy bundle if a dependency version bump reintroduces one of these, so don't assume correctness from the config file alone.
Commands
npm start— dev server (Vite).npm run build— production build (runs the linter first via theprebuildscript).npm test/npm run test:watch— Vitest.npm run linter/npm run linter:fix— ESLint onsrc.npm run zip— builds, then runsbuild.shto producetemplate.zipready for the DSPLAY Web Manager.build/andtemplate.zipare gitignored.
Dependency management
Regular npm dependencies, not vendored files — npm outdated / npm update for in-range bumps. For an out-of-range (typically major) bump, apply it deliberately and verify npm start, npm run build, and npm test still work before committing.
Known pending bump: ESLint 9 -> 10
eslint/@eslint/js are pinned to ^9.39.5 (latest is 10.x). Bumping them currently fails on peer dependency conflicts: eslint-plugin-import, eslint-plugin-jsx-a11y, and eslint-plugin-react haven't declared ESLint 10 support yet as of 2026-08-12 — they're still the actively-maintained canonical packages, not abandoned or superseded, just lagging behind the major. eslint-plugin-react-hooks already supports it. eslint-plugin-unicorn is pinned to 65.0.1 for the same reason (66.0.0+ requires ESLint >=10.4). Don't force this with --legacy-peer-deps — re-check peer ranges periodically and bump all of them together once the laggards catch up.
Commit messages
Every commit title must start with an emoji, followed by a short, imperative summary — e.g. ⬆️ upgrading deps.
- The human maintainer uses gitmoji-cli for manual commits, so gitmoji conventions (
✨feature,🐛fix,⬆️upgrade deps,♻️refactor,🔥remove code,📝docs) are a good default. - Agents are not required to stick to the official gitmoji list — pick whichever emoji best represents the actual change in that commit, as long as it's placed at the start of the title.
- Version bumps (
package.json'sversionfield) get their own commit, titled with just the version number and no emoji (e.g.2.0.0), separate from the commit(s) that made the actual change.