G. AI / collaborator prompt
Paste into your AI assistant (ChatGPT, Claude, Copilot, Gemini, etc.). Same content ships in the Add presentation modal as “Copy prompt”.
You are packaging an interactive presentation as a web app for **Superprez drop-code (zip upload)**. The zip MUST work on **Superprez** (install → build → start / preview) and as a **Superengine-compatible port** (manifest + Handlebars compose templates).
## Public references (only these — do not assume access to any private repo)
- Official Superengine ports catalog (study file shapes): https://software.superengine.tech/superengine/
- Superengine documentation: https://www.superengine.tech/docs
- Port license (text for LICENSE.txt): https://www.superengine.tech/legal/software-license-agreement-for-ports
On the ports catalog, open any published port (e.g. wordpress-port) and mirror its **structural** conventions: manifest app-spec 1.2, templates array, compose service shape, logging/restart, embedded_ui overview link pattern, LICENSE.txt / NOTICE.txt / picture.png at port root. Slide decks use a **simpler** entitlement set than database apps — follow the templates in **this prompt**, not WordPress’s full entitlement list.
## Context
- Superprez hosts the deck as a real web app from GitHub or zip. Builders run install, build, and start. The presentation **title in the Superprez UI** is separate from your HTML `<title>`.
- Default architecture: **static files under `html/`**, nginx in production (via Superengine compose), Node static server for Superprez preview. Do **not** use Vite/React/Next unless the user explicitly requires it.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
STEP 0 — IF THE USER GAVE YOU HTML + ASSETS (most common failure)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Do this **before** adding Superengine boilerplate:
1. Create folder **`html/`** at the package root (sibling of `package.json`, not inside it).
2. Put the entry file at **`html/index.html`**. Rename the user’s file (e.g. `deck.html`, `My Slides.html`) to `index.html`.
3. Move **every** asset the HTML references (CSS, JS, images, fonts, data JSON) under **`html/`**, preserving relative paths. Examples:
- `styles.css` next to `index.html` → `html/styles.css`
- `assets/logo.png` → `html/assets/logo.png`
4. Update `<link>`, `<script>`, `<img>`, `url(...)` paths in `index.html` if needed so they still resolve from `/` when served.
5. **Never** leave `index.html` at the zip root alongside `package.json`. Superprez/nginx serves **`html/`**, not the repo root.
6. After edits, add cache-bust query strings on static references, e.g. `styles.css?v=DECK_SLUG-1.0.0`, `app.js?v=DECK_SLUG-1.0.0` (use the user’s real filenames, not only styles.css/app.js).
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GOLDEN RULES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. **One docroot name everywhere:** `html/` in compose volume, `npm start`, and `validate-build.js`.
2. **Entitlements:** declare **only** what `compose.yaml.hbs` and `embedded_ui/*.hbs` use. For static nginx decks that is **`port-number`**, **`host`**, **`os`** — in that order. Do **not** add `folder-path`, `uid`, or `gid` unless compose actually references them.
3. **Compose port line** must be a YAML list item with **unquoted** Handlebars:
` - {{entitlements.port-number}}:80`
Wrong: quoted whole mapping, spaces inside `{{ }}`, or missing list dash.
4. **Zip root** = `package.json` + `manifest.json` + `compose.yaml.hbs` + `html/` + legal files — OR exactly **one** top-level folder containing those (no `Deck/Deck/package.json`).
5. **Lockfile required:** commit `package-lock.json` (even with zero npm dependencies).
6. **`npm start`** must bind **`0.0.0.0`**, honor **`process.env.PORT`** (default **8080**), serve **`html/`**.
7. **LF line endings** in JSON/YAML (no Windows CRLF in manifest/compose).
8. **No secrets** in the zip; `.env.example` with placeholders only.
9. **Exclude** from zip: `node_modules`, `dist`, `build`, `.next`, `__MACOSX`, `.DS_Store`, nested zips.
10. If you **cannot** run shell commands, produce all files completely and tell the user exactly how to verify (see Verification).
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TARGET FILE TREE (create every path)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
.
├── package.json
├── package-lock.json
├── server.js
├── manifest.json
├── compose.yaml.hbs
├── picture.png
├── LICENSE.txt
├── NOTICE.txt
├── README.md
├── .env.example
├── html/
│ ├── index.html
│ └── … (all deck assets)
├── embedded_ui/
│ ├── overview.html.hbs
│ └── about.html
└── scripts/
├── validate-build.js
└── write-app-icon.js
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FILE TEMPLATES — copy, then replace DECK_* placeholders
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Replace:
- DECK_TITLE — human-readable deck name (manifest + UI copy)
- DECK_SLUG — lowercase alphanumeric hyphenated id (cache-bust strings)
- DECK_VERSION — semver, bump on every redeploy (e.g. 1.0.0)
- DECK_SERVICE_SUFFIX — short unique lowercase suffix for compose service (e.g. slides, deck42)
--- manifest.json ---
{
"app-spec-version": "1.2",
"subject": {
"name": { "en": "DECK_TITLE" },
"version": "DECK_VERSION"
},
"entitlements": [
{
"type": "PortUnspecifiedStaticNumber",
"name": "port-number",
"description": { "en": "HTTP port mapped to nginx container port 80." }
},
{
"type": "HostHostnameOrIPAddress",
"name": "host",
"description": { "en": "Hostname or IP address of your server." }
},
{
"type": "OperatingSystem",
"name": "os",
"description": { "en": "Operating system." }
}
],
"templates": [
"compose.yaml.hbs",
"embedded_ui/overview.html.hbs"
],
"actions": []
}
--- compose.yaml.hbs ---
services:
{{app_id}}-DECK_SERVICE_SUFFIX:
image: nginx:1.27-alpine
restart: always
volumes:
- ./html:/usr/share/nginx/html:ro
ports:
- {{entitlements.port-number}}:80
logging:
driver: local
--- embedded_ui/overview.html.hbs ---
<section class="section">
<div class="container">
<h2 class="title is-4">DECK_TITLE</h2>
<p class="mb-3">Interactive HTML presentation.</p>
<a href="http://{{entitlements.host}}:{{entitlements.port-number}}"
target="_blank" rel="noopener noreferrer" class="button is-dark">Open deck</a>
</div>
</section>
--- embedded_ui/about.html ---
<section class="section">
<div class="container">
<h2 class="title is-4">DECK_TITLE</h2>
<p>Static HTML deck packaged for Superprez and Superengine.</p>
<p>Production: nginx serves <code>./html</code> read-only. Compose port mapping uses unquoted <code>{{entitlements.port-number}}:80</code>.</p>
<p>License: <a href="https://www.superengine.tech/legal/software-license-agreement-for-ports" target="_blank" rel="noopener noreferrer">Software License Agreement for Ports</a>.</p>
</div>
</section>
--- LICENSE.txt ---
This software is licensed under the Software License Agreement for Ports available at the URI below:
https://www.superengine.tech/legal/software-license-agreement-for-ports
If, for any reason, the URI provided in this file to the Software License Agreement for Ports does not redirect to the website of the copyright owner of this software, and, to the correct page on this website, you may not use this software.
This license does not apply to the files not under Superengine copyright as detailed in the NOTICE.txt file.
This software uses the following third-party software:
- the nginx image with the tag 1.27-alpine from Docker Hub
Third-party software has a different copyright owner and license.
--- NOTICE.txt ---
Copyright 2025 Superengine, except for the following files:
- picture.png
- html/ (presentation content and assets)
--- package.json ---
{
"name": "DECK_SLUG",
"version": "DECK_VERSION",
"private": true,
"description": "DECK_TITLE — Superprez / Superengine static deck",
"engines": { "node": ">=18" },
"scripts": {
"icon": "node scripts/write-app-icon.js",
"build": "node scripts/validate-build.js",
"start": "node server.js"
}
}
--- package-lock.json (zero-dependency projects — required for npm ci) ---
{
"name": "DECK_SLUG",
"version": "DECK_VERSION",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "DECK_SLUG",
"version": "DECK_VERSION",
"engines": { "node": ">=18" }
}
}
}
--- .env.example ---
# Optional local preview (Superprez sets PORT in hosting)
PORT=8080
--- README.md (minimum) ---
# DECK_TITLE
Static interactive deck for **Superprez** (zip upload) and **Superengine** (manifest + compose).
## Local preview
npm ci
npm run icon
npm run build
npm run start
Open http://127.0.0.1:8080/ (or set PORT). GET / must return html/index.html.
## Superengine files
- manifest.json (app-spec 1.2)
- compose.yaml.hbs (nginx → ./html)
- embedded_ui/overview.html.hbs, embedded_ui/about.html
- picture.png, LICENSE.txt, NOTICE.txt
Official port examples: https://software.superengine.tech/superengine/
--- server.js ---
const http = require("http");
const fs = require("fs");
const path = require("path");
const PORT = Number.parseInt(process.env.PORT || "8080", 10);
const PUBLIC = path.join(__dirname, "html");
const MIME = {
".html": "text/html; charset=utf-8",
".js": "application/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".json": "application/json",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".svg": "image/svg+xml",
".ico": "image/x-icon",
".webp": "image/webp",
".woff": "font/woff",
".woff2": "font/woff2",
".txt": "text/plain; charset=utf-8",
};
function safeJoin(rootDir, requestPath) {
const decoded = decodeURIComponent(requestPath.split("?")[0]);
const rel = path.normalize(decoded).replace(/^\/+/, "");
if (rel.includes("..")) return null;
const full = path.join(rootDir, rel);
if (!full.startsWith(rootDir)) return null;
return full;
}
const server = http.createServer((req, res) => {
if (!req.url || req.method !== "GET") {
res.writeHead(405, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Method Not Allowed");
return;
}
let pathname = req.url.split("?")[0];
if (pathname === "/") pathname = "/index.html";
const filePath = safeJoin(PUBLIC, pathname);
if (!filePath) {
res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Forbidden");
return;
}
fs.stat(filePath, (err, st) => {
if (err || !st.isFile()) {
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Not Found");
return;
}
const ext = path.extname(filePath).toLowerCase();
res.writeHead(200, { "Content-Type": MIME[ext] || "application/octet-stream" });
fs.createReadStream(filePath).pipe(res);
});
});
server.listen(PORT, "0.0.0.0", () => {
console.log("Serving html/ at http://0.0.0.0:" + PORT);
});
--- scripts/validate-build.js ---
const fs = require("fs");
const path = require("path");
const root = path.join(__dirname, "..");
const fail = (msg) => {
console.error("FAIL:", msg);
process.exit(1);
};
const requireFile = (rel) => {
if (!fs.existsSync(path.join(root, ...rel.split("/")))) fail("missing " + rel);
};
[
"html/index.html",
"picture.png",
"manifest.json",
"compose.yaml.hbs",
"LICENSE.txt",
"NOTICE.txt",
"embedded_ui/overview.html.hbs",
"embedded_ui/about.html",
"package-lock.json",
].forEach(requireFile);
const composeRaw = fs.readFileSync(path.join(root, "compose.yaml.hbs"), "utf8");
let portsOk = false;
for (const line of composeRaw.split("\n")) {
const t = line.trim();
if (!t.startsWith("-") || !t.includes("entitlements.port-number")) continue;
portsOk = true;
if (!/\{\{entitlements\.port-number\}\}:80/.test(t)) {
fail("compose.yaml.hbs ports line must contain {{entitlements.port-number}}:80 unquoted");
}
if (/^-\s*["']/.test(t)) fail("compose.yaml.hbs ports line must not quote the whole mapping");
}
if (!portsOk) fail("compose.yaml.hbs missing ports mapping for {{entitlements.port-number}}:80");
if (!composeRaw.includes("./html:/usr/share/nginx/html")) {
fail("compose.yaml.hbs must mount ./html:/usr/share/nginx/html:ro");
}
let manifest;
try {
manifest = JSON.parse(fs.readFileSync(path.join(root, "manifest.json"), "utf8"));
} catch (e) {
fail("manifest.json invalid JSON: " + e.message);
}
if (manifest["app-spec-version"] !== "1.2") fail('manifest.json needs app-spec-version "1.2"');
if (!Array.isArray(manifest.actions) || manifest.actions.length !== 0) fail('manifest.json needs "actions": []');
const templates = manifest.templates;
if (!Array.isArray(templates) || templates[0] !== "compose.yaml.hbs" || templates[1] !== "embedded_ui/overview.html.hbs") {
fail('manifest.json templates must be ["compose.yaml.hbs", "embedded_ui/overview.html.hbs"]');
}
const expected = [
{ name: "port-number", type: "PortUnspecifiedStaticNumber" },
{ name: "host", type: "HostHostnameOrIPAddress" },
{ name: "os", type: "OperatingSystem" },
];
const ent = manifest.entitlements;
if (!Array.isArray(ent) || ent.length !== expected.length) {
fail("manifest.json entitlements must be exactly port-number, host, os (3 entries)");
}
for (let i = 0; i < expected.length; i++) {
if (!ent[i] || ent[i].name !== expected[i].name || ent[i].type !== expected[i].type) {
fail("manifest.json entitlements[" + i + "] must be " + JSON.stringify(expected[i]));
}
}
const overview = fs.readFileSync(path.join(root, "embedded_ui/overview.html.hbs"), "utf8");
if (!overview.includes("{{entitlements.host}}") || !overview.includes("{{entitlements.port-number}}")) {
fail("embedded_ui/overview.html.hbs must link with host and port-number entitlements");
}
console.log("Build OK — Superprez / Superengine static deck checklist passed.");
process.exit(0);
--- scripts/write-app-icon.js (creates picture.png — no npm deps) ---
const zlib = require("zlib");
const fs = require("fs");
const path = require("path");
const W = 256, H = 256, R = 0x2e, G = 0x5b, B = 0xea;
const PNG_SIG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const crcTab = new Uint32Array(256);
for (let n = 0; n < 256; n++) {
let c = n;
for (let k = 0; k < 8; k++) c = (c & 1) !== 0 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
crcTab[n] = c >>> 0;
}
function crc32(buf) {
let c = 0xffffffff;
for (let i = 0; i < buf.length; i++) c = crcTab[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
return (c ^ 0xffffffff) >>> 0;
}
function chunk(typ, data) {
const t = Buffer.from(typ, "ascii");
const len = Buffer.alloc(4);
len.writeUInt32BE(data.length, 0);
const body = Buffer.concat([t, data]);
const crc = Buffer.alloc(4);
crc.writeUInt32BE(crc32(body), 0);
return Buffer.concat([len, body, crc]);
}
const ihdrData = Buffer.alloc(13);
ihdrData.writeUInt32BE(W, 0);
ihdrData.writeUInt32BE(H, 4);
ihdrData[8] = 8;
ihdrData[9] = 2;
const rowLen = 1 + W * 3;
const raw = Buffer.alloc(rowLen * H);
for (let y = 0; y < H; y++) {
const off = y * rowLen;
raw[off] = 0;
for (let x = 0; x < W; x++) {
const p = off + 1 + x * 3;
raw[p] = R; raw[p + 1] = G; raw[p + 2] = B;
}
}
const idatPayload = zlib.deflateSync(raw, { level: 9 });
const out = Buffer.concat([PNG_SIG, chunk("IHDR", ihdrData), chunk("IDAT", idatPayload), chunk("IEND", Buffer.alloc(0))]);
fs.writeFileSync(path.join(__dirname, "..", "picture.png"), out);
console.log("Wrote picture.png");
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
HOSTED CACHE (after redeploy)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Bump query strings on CSS/JS (and other static) references in html/index.html, bump manifest.json subject.version, then hard-refresh or use a private window on the live URL.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ZIP FOR SUPERPREZ UPLOAD
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- Compress the **package root** (folder that directly contains package.json and manifest.json).
- On macOS: do not zip the parent folder so the archive opens as `package.json` at top level (or one named folder — not double-nested).
- Keep uncompressed size under ~12 MB.
- Path segments: ASCII letters, digits, dot, underscore, hyphen only (avoid spaces and special characters in folder/file names).
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
VERIFICATION (required before telling the user the zip is ready)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
If you have a terminal, run from package root:
npm ci
npm run icon
npm run build
npm run start
Then GET http://127.0.0.1:8080/ must return html/index.html (HTTP 200, HTML body).
If you **do not** have a terminal, say so explicitly and give the user these commands. Do **not** claim PASS on the checklist without evidence.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DELIVERABLES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Print the final file tree.
2. List every file you created or changed.
3. For each item below, print PASS or FAIL with the evidence path (or “NOT RUN — user must verify”):
- html/index.html exists; all referenced assets exist under html/
- package.json at zip root OR single top-level folder (no double nesting)
- manifest.json, compose.yaml.hbs, picture.png, LICENSE.txt, NOTICE.txt at package root
- manifest: app-spec 1.2; templates order; actions []; entitlements port-number → host → os only
- compose: nginx, ./html volume, unquoted {{entitlements.port-number}}:80
- overview.html.hbs: Open link uses {{entitlements.host}} and {{entitlements.port-number}}
- package.json: build + start; package-lock.json present; server.js binds 0.0.0.0 and honors PORT
- Cache-bust query params on static assets in index.html if redeploying
- .env.example present; no secrets
- npm run build exits 0 (or user instructed to run it)
Fix every FAIL before delivering the zip.
When repackaging after edits: rebuild zip, bump version + cache-bust strings, redeploy on Superprez, hard-refresh browser.
Full prose contract (topology, scripts, networking, patterns): sections A–F above. Superengine port references: superengine.tech/docs.