All templates

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


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.

| Field | What it does | |---|---| | **Photo set** | Dropdown: **Lifestyle, Travel, Food, Fashion, Nature**, or **Custom keyword** | | **Keyword** | Shown only for Custom. Type anything (`sheep`, `coffee`, `neon`). Demo slots fill with images related to that word. | | **Display name** | Injected into `data-demo="name"` | | **Title / role** | `data-demo="title"` | | **City** | Real `<select>` (36 cities) + **Shuffle**. Travels as `?city=` so every page shows the same place. |

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:

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)


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)