A universal sharepic generator. Pick a template, edit text, choose a palette and a format, export to PNG.
Shapio is deliberately small: a Node/Express backend for password-gated access plus a static client where templates and palettes live as plain ES modules. Drop in a new file, register it in an index, refresh.
- 20 built-in templates — manifest, quote, event, save-the-date, schedule, timeline, glossary, hashtags, triptych, open letter, photo story, polaroid, statistic, myth-vs-fact, carousel slide, slogan, gradient hero, checklist, spotlight, numbered list.
- 7 default palettes — Ink, Paper, Ocean, Sunset, Forest, Dusk, Spectrum.
- Three formats — 1:1 post (1080×1080), 9:16 story (1080×1920), 16:9 wide (1920×1080).
- Photo upload with zoom & pan for photo, spotlight and polaroid templates.
- PNG export in full canvas resolution.
- Password-protected with a forced first-login change.
- Modular — one file per template, one file per palette, branding lives in a single config.
git clone <your-fork-url> shapio
cd shapio
npm install
npm startThen open http://localhost:3000.
- Initial password:
admin - After the first successful login you'll be asked to set a new password. The hash is stored in
auth/auth.json(gitignored).
To run in watch mode (auto-restart on changes):
npm run devCopy .env.example to .env and adjust:
| Variable | Default | Notes |
|---|---|---|
PORT |
3000 |
Port the server listens on. |
SESSION_SECRET |
dev fallback | Set a strong secret in production. Used to sign session cookies. |
shapio/
├── server.js # Express server (auth + static)
├── package.json
├── auth/
│ ├── auth.default.json # Bootstrap config — "admin" / mustChange: true
│ └── auth.json # Generated on first run (gitignored)
└── public/ # Served statically
├── index.html
├── styles.css
├── app.js
├── config/
│ └── branding.js # App name, logo, defaults
├── palettes/
│ ├── index.js # Palette registry
│ ├── ink.js
│ ├── paper.js
│ ├── ocean.js
│ ├── sunset.js
│ ├── forest.js
│ ├── dusk.js
│ └── spectrum.js
├── templates/
│ ├── index.js # Template registry
│ └── *.js # one file per template
└── assets/
└── img/logo.svg
Edit public/config/branding.js:
export const BRANDING = {
appName: "Your App",
appTagline: "Your tagline",
appLogoUrl: "/assets/img/your-logo.svg",
tagline: "Shown in the header eyebrow",
templateLogo: {
enabled: true,
imageUrl: "/assets/img/your-logo.svg",
name: "Your App",
sub: "Your subtitle",
},
defaultPalette: "ink",
defaultFormat: "1:1",
defaultTemplate: "manifest",
};Drop your logo into public/assets/img/ and reference it via the URL above.
-
Create
public/palettes/your-palette.js:export default { id: "your-id", name: "Display name", swatch: "linear-gradient(135deg, #112233 0%, #112233 50%, #FF8800 50%, #FF8800 100%)", tokens: { bg: "#112233", bg2: "#0B1A2A", fg: "#FFFFFF", fgSoft: "#CBD5E1", accent: "#FF8800", accentFg: "#0B1A2A", eyebrow: "#FFB266", }, };
-
Register it in
public/palettes/index.js:import yourPalette from "./your-palette.js"; export const PALETTES = [..., yourPalette];
Tokens are exposed as CSS custom properties (--c-bg, --c-bg-2, --c-fg, --c-fg-soft, --c-accent, --c-accent-fg, --c-eyebrow) on the .canvas element, so templates reference them via var(--c-fg) etc.
-
Create
public/templates/your-template.js:export default { id: "your-id", name: "Display name", desc: "Short description shown in the gallery.", fields: [ { key: "headline", label: "Headline", type: "input", max: 80 }, { key: "body", label: "Body", type: "textarea", max: 200 }, ], supportsPhoto: false, defaults: { headline: "Default headline", body: "Default body copy.", }, render: (t, state, { esc, LOGO_HTML }) => ` <div class="tpl"> <h1 class="tpl-headline">${esc(t.headline)}</h1> <p class="tpl-body">${esc(t.body)}</p> ${state.showLogo ? LOGO_HTML() : ""} </div> `, thumb: () => ` <div style="position:absolute;inset:0;background:#FAFAF9;padding:8px;">Preview</div> `, };
-
Add matching styles to
public/styles.css, scoped to your id:.canvas[data-template="your-id"] .tpl { /* layout */ }
-
Register it in
public/templates/index.js:import yours from "./your-template.js"; export const TEMPLATES = { ..., yours }; export const TEMPLATE_ORDER = [..., "your-id"];
The render function receives the current text values (t), the full app state (state — includes photoUrl, photoZoom, photoX, photoY, showLogo) and a context object with utility helpers (esc, cssEsc, LOGO_HTML, photoTransformStyle).
If your template supports photo upload set supportsPhoto: true and include the photo block in your render output (see photo.js, spotlight.js, polaroid.js for examples).
If you've forgotten the password, delete auth/auth.json and restart the server. It will be recreated from auth/auth.default.json (initial password: admin).
To change the initial password (before any first login), edit auth/auth.default.json and delete auth/auth.json.
- Browser fetches
GET /api/auth/status. The server returns{ authenticated, mustChange }. - If the user isn't authenticated, the login form is shown. Submitting it calls
POST /api/auth/login. - On the first successful login (
mustChange: true), a change-password form is shown. It callsPOST /api/auth/changewith the current and new password. - Sessions are stored server-side (signed cookie). Use
POST /api/auth/logoutto clear.
Passwords are hashed with bcrypt (10 rounds). The plain password is never persisted; only the hash sits in auth/auth.json.
Modern evergreen browsers (Chrome, Firefox, Safari, Edge). Uses ES modules in the browser, so no bundler is required.
GNU General Public License v3.0 or later. See LICENSE.