Website template gallery, personalization docs
How the gallery, demos, and photo personalizer work, plus issues we hit while building it.
Live site: https://website-templates-dcf.pages.dev/
Repo: Paddy0x/website-templates (deploy branch main, Cloudflare Pages, output dir public/)
What you get
- 101 unique templates under
public/templates/<id>/with desktop + mobile screenshots - Homepage with search + intent filters (what you are building / vibe / effects)
- Personalizer that fills every demo with a name, title, city, and a photo set
- Shared chrome on every template: theme toggle, back home, prev/next, AuraicPulse credit
Personalizer (current design)
Opened from Personalize demos on the homepage. Saved values live in localStorage under template-demo and ride on every View template link as URL params.
Save writes the fields and scrolls to the template grid. It does not open a template.
How photos are chosen
1. Theme lifestyle|travel|food|fashion|nature uses that word as the search term.
2. Custom uses whatever keyword you typed.
3. The client queries Openverse (https://api.openverse.org/v1/images/?q=...) for 5–10 images (Flickr, Wikimedia, etc.). No API key.
4. If search fails, picsum seeds (picsum.photos/seed/<keyword>-life-N) so slots are never empty.
5. The same set is shuffled per template (keyword + template id + path) so each demo page feels distinct.
Demo chrome ‹ › passes theme / q / name / title / city to the next template so the identity sticks.
URL contract
/templates/01-nova/?theme=travel&name=Avery&title=Guide&city=Tokyo
/templates/01-nova/?theme=custom&q=sheep&name=Sheep%20Farm&title=Wool&city=Dublin
Legacy ?ig=handle is treated as a custom keyword (no Instagram fetch).
Code map
| Path | Role | |---|---| | `public/index.html` | Homepage + personalizer modal | | `public/js/gallery.js` | Finder, card grid, personalizer form, keyword photo load for card thumbs | | `public/assets/personalize.js` | Demo runtime: name/title/city/avatar/photos into `data-demo*` slots | | `public/assets/demo-chrome.js` | Floating All templates / Edit demo / Theme / ‹ › + credit | | `public/assets/theme-toggle.js` | dark/light cycle, `localStorage template-theme` | | `public/assets/mobile-shell.js` | Mobile chrome (`app` \| `drawer` \| `header` per template) | | `public/templates.json` | Registry (101 records: id, name, category, style, description, path, mobile) | | `public/templates/<id>/index.html` | One template each; includes theme-toggle, personalize, demo-chrome, mobile-shell | | `public/css/gallery.css` | Homepage + personalizer styles (includes working city `<select>`) | | `public/screenshots/<id>-desktop.jpg` / `-mobile.jpg` | Card screenshots | | `scripts/capture-screenshots.mjs` | Playwright screenshot script | | `scripts/bake-instagram.mjs` | **Legacy.** Baked IG / stand-in photos into `public/ig-cache/`. Not used by the theme picker. | | `functions/api/instagram.js` | **Legacy.** IG scrape API. Not used by the theme picker. |Template slot contract
Templates mark fillable spots with:
data-demo="name|title|handle|bio|city|avatar|..."(text / attributes)data-demo-href="profileurl"data-demo-photo="N"(one image slot;Nwraps over the feed)data-demo-photos="count"(rebuilds a grid of images)- Leftover
picsum|unsplash|placehold<img>tags are swapped for the keyword feed
Every template loads (before </body>):
<script src="/assets/theme-toggle.js"></script>
<script src="/assets/personalize.js"></script>
<script src="/assets/demo-chrome.js"></script>
<script src="/assets/mobile-shell.js"></script>
Gallery rules (product constraints)
- Brand string is exactly 100+ Unique templates
- Credit: design by AuraicPulse →
https://www.auraicpulse.info/ - Cards: desktop + mobile screenshot, one View template button
- Demo chrome buttons match homepage (glass pills + cyan gradient primary)
- Homepage is templates + find-by-need only (no contact form / marketing sections)
- Dark + light on every template; every template has a way home
- No GitHub or
.jsonlinks in the UI - Never use em dashes or en dashes in user-facing copy
Issues found (and how they were handled)
1. Instagram photos could not be loaded in production
Symptom: Demo photo slots stayed empty or showed generic stock.
Cause: Cloudflare Pages/Workers block egress to Instagram. Cookie-warmup scrapes from the edge return login walls. web_profile_info stays 401 even with cookies.
Also: Instagram CDN URLs are signed. Rewriting s150x150_tt6 → s640x640_tt6 breaks the signature (403).
Outcome: Instagram is not used for demo photos. The personalizer uses Openverse keyword search instead (see below).
2. Bing “web images” returned WhatsApp logos and random graphics
Symptom: Typing handles like ladygaga filled every slot with a WhatsApp mark; realdonaldtrump showed random brand graphics.
Cause: Fallback searched Bing Images for {handle} instagram photos. That query returns share cards, OG images (1200×630), and brand logos, not a personal feed.
Fix: Removed Bing/Google web-image scraping from the live photo path.
Lesson: “Search for {name} instagram” is a branding search, not a people-photo search.
3. Baked web-image cache shipped broken and brand files
Symptom: 84-byte .jpg files (HTML error pages), 1200×630 OG cards, logo walls on gallery thumbs.
Cause: Download step trusted content-type and saved whatever came back.
Fix (in scripts/bake-instagram.mjs, legacy): sniff JPEG/PNG/GIF/WEBP magic bytes, reject HTML, reject OG 1200×630 and tiny marks. Not required for the new theme picker.
4. Every template showed the same photo order
Symptom: Changing pages did not change which image sat in which slot.
Cause: Shuffle seed was only the handle/keyword, not the template.
Fix: Seed is keyword | templateKey | pathname in personalize.js.
5. City dropdown arrow did nothing
Symptom: input + <datalist> showed a native chevron that opened no usable list (especially with custom modal styling).
Fix: Replaced with a real <select> populated from CITIES in gallery.js, plus Shuffle.
6. loremflickr / Unsplash Source are not reliable
Symptom: loremflickr.com/.../sheep returned HTTP 401; source.unsplash.com returned 503.
Fix: Use Openverse (api.openverse.org, CORS *, no key, ~20 req/min burst / 200/day sustained). Picsum seeds as offline fallback.
7. Openverse rate limits
Anon limits are roughly 20 requests/minute and 200/day. The homepage loads one search for card thumbs; each demo page loads one search. Fine for a portfolio. If you expect heavy traffic, cache results in the browser (sessionStorage) or behind a tiny Pages Function.
8. LocalStorage could keep old test values
If you tested a handle/keyword and hit Save, it sticks in that browser only (template-demo). Open the personalizer, pick a set (or type a new keyword), and Save. Hard-refresh after deploys.
Run locally
# static site (gallery + demos; Openverse is called from the browser)
python3 -m http.server 8080 --directory public
# screenshots (after changing templates)
node scripts/capture-screenshots.mjs
Deploy is git push origin main → Cloudflare Pages (Framework preset None, Build command empty, Build output directory public).
Personalizer QA checklist (before you publish)
- [ ] Photo set dropdown lists Lifestyle / Travel / Food / Fashion / Nature / Custom
- [ ] Custom shows a keyword box;
sheepfills demo slots with sheep photos - [ ] City dropdown opens and lists cities; Shuffle changes the value
- [ ] Save scrolls to the template grid (does not open a template)
- [ ] Card thumbs show the keyword feed; View template URL includes
themeorq+ name/title/city - [ ] Demo chrome ‹ › keeps the same name/title/city/keyword
- [ ] Dark and light both work on a couple of templates
- [ ] Mobile viewport: chrome and photo grids still look right