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