Claude Sonnet 4.5
-------------
Theme & Style
-------------
Use the Tailwind 'Stone' palette. Headings should be deep red and serif; body text sans-serif. Do not display any comments or notes.
Links: Bold font, navy blue color. Links must open in the current window.
Screen width limited to 1024 pixels.
--------------------
GLOBAL CONFIGURATION
--------------------
- BRANDING: SVG logo of "文" (2.5em, deep red) followed by "OpenLang.ai" in large bold text. Link to "/".
- LANG: French Language Learning Games
- SOURCE: Right-justified button with the text "SOURCE" that downloads the build-instructions.txt file
-----------------
Header Components
-----------------
Title: "OpenLang.ai French".
Favicon: A data URI containing the character "文" (deep red on white).
Top Bar
Smartphone:
BRANDING, LANG, SOURCE (on a single line)
IOS-INSTALL-BTN appears on the same line as SOURCE, immediately to its left
but only when isIOS is true and navigator.standalone is not true.
Tablet:
BRANDING, LANG (1.3rem), SOURCE
IOS-INSTALL-BTN appears on the same line as SOURCE, immediately to its left.
Desktop (strictly in this order):
BRANDING, LANG (2.0rem), SOURCE
IOS-INSTALL-BTN appears on the same line as SOURCE, immediately to its left.
Note: IOS-INSTALL-BTN is the #ios-install-btn button. It is hidden by
default (display:none) and shown by JS only on iOS Safari outside standalone
mode. When hidden it takes no space — use display:none not visibility:hidden.
Do not include any comments or notes in the index.html file.
--------------
Launcher Block
--------------
- Consolidate the provided URL List into a grid of interactive cards.
- Layout: 3 columns on mobile, 3 on tablet, 4 on desktop.
- Interaction: Each card should have a 60x60px rounded-xl background (Deep Red) containing the Icon Text in white.
- Hover Effect: Apply a subtle scale-up (e.g., scale-105) and shadow enhancement on hover.
- Links: Must use relative paths or absolute URLs as provided; ensure they open in the current window to stay within the PWA standalone frame.
- AD SLOT — one slot inserted between the first and second rows of cards:
Position: below all game cards, after the last card in the grid.
The ad slot must span the full width of the grid (grid-column: 1 / -1).
Insert it as the last child of the grid container, after all card elements.
The slot contains a Media.net ad unit. Use a placeholder publisher
ID of "123456789" — this will be replaced with the real ID at launch.
Media.net requires two script tags: one global loader in and
one per-slot inline script in the ad div.
In (alongside other meta tags):
Use grid-column: 1 / -1 to span full width regardless of column count.
HTML for the ad slot (insert as the last item in the grid, after all cards):
Reserved for future ads
CSS for the ad slot:
.ad-slot: grid-column: 1 / -1, width: 100%, display: flex
flex-direction: column, align-items: center, justify-content: center
gap: 0.25rem, background: stone-100, border: 1px dashed stone-300
border-radius: 8px, padding: 0.4rem 0, margin: 0.25rem 0.
.ad-label: font-size: 0.65rem, color: stone-400, font-family: sans-serif
letter-spacing: 0.08em, text-transform: uppercase, pointer-events: none.
#media-net-ad: min-height: 50px, width: 320px, max-width: 100%.
-------------------------
PWA & OFFLINE (INSTALLER)
-------------------------
- DELIVERABLES: Four files, all deployed to the same directory:
index.html — app shell and launcher UI
sw.js — service worker (plain JS only, no HTML wrapper)
manifest.json — web app manifest (real file, not a data URI)
icon.svg — branding icon (real file, not a data URI)
- SELF-CONTAINED REQUIREMENT:
- index.html must have zero external dependencies (no CDN scripts
no external fonts). All CSS and JS must be inline. Use system font
stacks: Georgia/'Times New Roman' for serif headings, 'Helvetica
Neue'/Arial for body text.
- sw.js contains only plain JavaScript — no HTML, no module imports.
- WHY REAL FILES FOR MANIFEST AND ICON:
Chrome 93+ requires the manifest to be served from the same origin
as the page with Content-Type: application/manifest+json. A data:
URI manifest silently fails the installability check, beforeinstall-
prompt never fires, and the browser falls back to "Create shortcut"
instead of a proper PWA install. Icon src values must also be
fetchable URLs — data: URI icons fail the same check.
- WHY TWO FILES — DO NOT ATTEMPT SINGLE-FILE WORKAROUNDS:
All single-file approaches have been tried and fail. Do not attempt
to reintroduce any of the following:
1. Self-registration via query string (index.html?sw):
Nginx resolves Content-Type from the file extension via its
types{} map before any if{} block is evaluated. .html is
already mapped to text/html, so default_type inside an if{}
is never reached. There is no Nginx directive that can
conditionally override a known extension's Content-Type based
on a query string without restructuring the location blocks in
ways that break normal HTML serving. This approach is a dead end.
2. Blob URL (URL.createObjectURL):
A Blob SW's script URL is blob:https://origin/. The SW
spec derives the maximum allowed scope from the script path
which for a Blob is //. Registering with scope '/French/'
fails with a DOMException. Requires a Service-Worker-Allowed
response header to fix — same server-side problem, more complex.
3. data: URI as SW script URL:
Browsers block SW registration from data: URIs; the origin is
opaque and registration throws immediately.
Since all assets are on the same HTTPS domain, sw.js is served
with the correct application/javascript MIME type automatically
by virtue of its .js extension. No server configuration is needed.
- MANIFEST (manifest.json — static file, not generated at runtime):
- Hardcode start_url as the absolute path to the launcher:
"/French/index.html". Do not use window.location.pathname.
- Fields: "name", "short_name", "description"
"display": "standalone", "background_color": "#fafaf9"
"theme_color": "#991b1b", "start_url": "/French/index.html".
- ICONS: Two entries, both pointing to "icon.svg" (relative path).
Sizes "192x192" with "purpose": "any maskable", and "512x512".
- ICON (icon.svg — static file):
- A plain SVG: white/stone-50 background, the "文" character
centred in deep red (#991b1b), Georgia serif font.
- Designed to work at both 192x192 and 512x512.
- index.html MANIFEST LINK:
- Use a static tag — no JavaScript needed:
- SERVICE WORKER (sw.js):
- Plain top-level JS. No IIFE, no typeof-window guard, no HTML.
- All strings must use only straight ASCII apostrophes and quotes.
- PAGE_URL: Derived at the top of sw.js by replacing the sw.js
filename with index.html:
var PAGE_URL = self.location.href.replace(/sw\.js.*$/, 'index.html');
Resolves to the exact absolute URL used as the cache key for the
launcher shell. Do not use self.registration.scope.
- CACHE_NAME: Short ASCII string, versioned: e.g. 'openlang-v9'.
Increment the version number whenever sw.js or the URL list changes.
- TWO URL ARRAYS — phase 1 (shell) and phase 2 (games):
Derive manifest and icon absolute URLs the same way as PAGE_URL:
var BASE = self.location.href.replace(/sw\.js.*$/, '');
var SHELL_URLS = [ PAGE_URL, BASE + 'manifest.json', BASE + 'icon.svg' ];
var GAME_URLS = [ ];
- broadcast helper (module-level):
function broadcast(msg) {
self.clients.matchAll({includeUncontrolled:true, type:'window'})
.then(function(clients) {
clients.forEach(function(c) { c.postMessage(msg); });
});
}
- precacheOne(cache, url): fetches and stores one URL, ignoring errors.
Never use cache.add() or cache.addAll() — they key entries by Request
object, causing cache.match(urlString) lookups to miss.
- INSTALL — cache shell only:
- Open CACHE_NAME, run precacheOne for each SHELL_URLS entry.
- No progress broadcast during install — the shell is small and
fast; no overlay is shown at this stage.
- After Promise.all, call skipWaiting().
- ACTIVATE:
- Delete every cache whose name !== CACHE_NAME.
- Call self.clients.claim().
- MESSAGE HANDLER — listen for {type:'CACHE_GAMES'} from the page:
When the SW receives this message, cache all GAME_URLS with
progress broadcasting:
var done = 0;
caches.open(CACHE_NAME).then(function(cache) {
return Promise.all(GAME_URLS.map(function(url) {
return precacheOne(cache, url).then(function() {
done++;
broadcast({type:'PRECACHE_PROGRESS'
done:done, total:GAME_URLS.length});
});
}));
}).then(function() {
broadcast({type:'PRECACHE_DONE', total:GAME_URLS.length});
});
Wrap the handler in event.waitUntil() so the SW stays alive
for the duration of the downloads.
- FETCH HANDLER — three-path strategy:
1. Non-GET requests: pass straight to fetch(event.request)
no caching.
2. Cross-origin requests: pass straight to fetch(event.request)
no caching. Never store opaque responses.
3. Same-origin GET requests: cache-first.
url = event.request.url (use this string as the cache key)
cache.match(url)
hit -> return cached response
miss -> fetch from network
ok: cache.put(url, response.clone())
return response
failed: return cache.match(PAGE_URL)
hit -> return shell (offline fallback)
miss -> return new Response('Offline'
{status: 503})
- JAVASCRIPT STRUCTURE (in index.html) — TWO separate script blocks:
BLOCK A: A