diff --git a/scripts/check_site.sh b/scripts/check_site.sh
index 614fbf3..78d62fe 100755
--- a/scripts/check_site.sh
+++ b/scripts/check_site.sh
@@ -23,15 +23,110 @@ need() {
grep -q -- "$1" "$HTML" || fail "site/index.html missing: $1"
}
need "
Python Tutor"
+need 'name="description"'
+need 'name="robots"'
+need 'name="theme-color"'
+need 'rel="canonical"'
need 'property="og:title"'
+need 'property="og:description"'
+need 'property="og:type"'
+need 'property="og:url"'
+need 'property="og:site_name"'
need 'property="og:image"'
+need 'property="og:image:secure_url"'
+need 'property="og:image:type"'
+need 'property="og:image:width"'
+need 'property="og:image:height"'
+need 'property="og:image:alt"'
need 'name="twitter:card"'
+need 'name="twitter:title"'
+need 'name="twitter:description"'
+need 'name="twitter:image"'
+need 'name="twitter:image:alt"'
+need 'rel="apple-touch-icon"'
+need 'rel="manifest"'
need 'id="why"'
need 'id="loop"'
need 'id="screens"'
need 'id="start"'
ok "required and section anchors present"
+# Open Graph / Twitter image must be an absolute URL (most scrapers reject relative).
+grep -qE 'property="og:image"[^>]*content="https://' "$HTML" \
+ || fail "og:image must use an absolute https:// URL"
+grep -qE 'name="twitter:image"[^>]*content="https://' "$HTML" \
+ || fail "twitter:image must use an absolute https:// URL"
+ok "og:image and twitter:image use absolute URLs"
+
+# Social-share asset files must exist on disk at the right sizes.
+need_file() { [ -f "$1" ] || fail "missing asset: $1"; }
+need_file "$SITE/assets/og-image.png"
+need_file "$SITE/assets/og-image-square.png"
+need_file "$SITE/assets/favicon.svg"
+need_file "$SITE/assets/favicon.ico"
+need_file "$SITE/assets/favicon-16.png"
+need_file "$SITE/assets/favicon-32.png"
+need_file "$SITE/assets/apple-touch-icon.png"
+need_file "$SITE/assets/icon-192.png"
+need_file "$SITE/assets/icon-512.png"
+need_file "$SITE/site.webmanifest"
+ok "favicon, manifest, and social-share assets present"
+
+# Validate critical image dimensions where we can.
+python3 - "$SITE" <<'PY'
+import sys, struct, os
+site = sys.argv[1]
+def png_size(p):
+ with open(p, "rb") as f:
+ head = f.read(24)
+ if head[:8] != b"\x89PNG\r\n\x1a\n":
+ return None
+ w, h = struct.unpack(">II", head[16:24])
+ return w, h
+expected = {
+ "assets/og-image.png": (1200, 630),
+ "assets/og-image-square.png": (1200, 1200),
+ "assets/apple-touch-icon.png": (180, 180),
+ "assets/favicon-16.png": (16, 16),
+ "assets/favicon-32.png": (32, 32),
+ "assets/icon-192.png": (192, 192),
+ "assets/icon-512.png": (512, 512),
+}
+bad = []
+for rel, want in expected.items():
+ p = os.path.join(site, rel)
+ got = png_size(p)
+ if got != want:
+ bad.append(f"{rel}: got {got}, want {want}")
+if bad:
+ print("✗ wrong image dimensions:", file=sys.stderr)
+ for b in bad: print(" " + b, file=sys.stderr)
+ sys.exit(1)
+print("✓ all PNG asset dimensions correct")
+PY
+
+# webmanifest must reference real icon files and be valid JSON.
+python3 - "$SITE" <<'PY'
+import json, os, sys
+site = sys.argv[1]
+mf = os.path.join(site, "site.webmanifest")
+data = json.load(open(mf))
+icons = data.get("icons", [])
+if not icons:
+ print("✗ site.webmanifest has no icons", file=sys.stderr); sys.exit(1)
+missing = []
+for ic in icons:
+ src = ic.get("src", "")
+ rel = src[2:] if src.startswith("./") else src
+ if not os.path.exists(os.path.join(site, rel)):
+ missing.append(src)
+if missing:
+ print("✗ manifest icons missing on disk:", file=sys.stderr)
+ for m in missing: print(" " + m, file=sys.stderr)
+ sys.exit(1)
+print(f"✓ site.webmanifest valid JSON with {len(icons)} resolvable icons")
+PY
+
# Start-page install content must be visible — this page is the entry point
# to the repo, so the clone/install/run commands have to be there literally.
need "git clone https://github.com/StewAlexander-com/python-tutor.git"
diff --git a/site/README.md b/site/README.md
index da6c159..e58e92e 100644
--- a/site/README.md
+++ b/site/README.md
@@ -13,14 +13,54 @@ on every push to `main` that touches `site/`.
```
site/
-├── index.html # the landing page
+├── index.html # the landing page (full SEO + social meta)
├── style.css # design tokens mirror frontend/base.css
+├── site.webmanifest # PWA manifest, references the icons below
└── assets/
- ├── favicon.svg
- ├── og-image.png # 1200×630 social card (reused from frontend)
+ ├── favicon.svg # vector favicon, primary
+ ├── favicon.ico # 16/32/48 multi-res ICO for legacy clients
+ ├── favicon-16.png
+ ├── favicon-32.png
+ ├── apple-touch-icon.png # 180×180, full-bleed dark
+ ├── icon-192.png # PWA / Android home-screen
+ ├── icon-512.png # PWA / Android home-screen
+ ├── og-image.png # 1200×630 — Facebook, LinkedIn, Messenger, X
+ ├── og-image-square.png # 1200×1200 — square share / iMessage previews
└── screenshots/ # six UI screenshots, lazy-loaded
```
+## Social preview & SEO
+
+`index.html` includes:
+
+- standard SEO: ``, `description`, `keywords`, `robots`, canonical
+- Open Graph (Facebook / LinkedIn / Messenger / iMessage / Slack):
+ `og:type`, `og:site_name`, `og:title`, `og:description`, `og:url`,
+ `og:image` (+ `secure_url`, `type`, `width`, `height`, `alt`)
+- Twitter / X: `twitter:card=summary_large_image` plus title, description,
+ image, and `twitter:image:alt`
+- JSON-LD `SoftwareApplication` for Google rich results
+- a full favicon set + `site.webmanifest` for PWA installs
+
+`og:image` and `twitter:image` use **absolute** `https://` URLs (most
+social scrapers reject relative paths). All other assets use relative
+paths so the page works under the `/python-tutor/` GitHub Pages subpath
+and under `file://` previews.
+
+### Validate the social preview
+
+After deploy, paste the live URL into one of these debuggers — they
+fetch the page server-side and show what each platform will render:
+
+- Facebook / Messenger:
+- LinkedIn:
+- X / Twitter: (or just paste
+ into a draft tweet)
+- Generic:
+
+If you change the OG image, click "scrape again" in the FB debugger to
+bust the cache; LinkedIn caches for ~7 days and has no manual flush.
+
## Preview locally
The page is pure static HTML + CSS — no build step.
@@ -45,8 +85,14 @@ overview that points them at the repo and the two-command install.
`scripts/check_site.sh` runs from the repo root and verifies:
-- referenced screenshots and OG image exist on disk
-- `` and Open Graph tags are present
+- referenced screenshots and social assets exist on disk
+- complete `` meta package: title, description, robots, canonical,
+ theme-color, full Open Graph set, full Twitter card set
+- `og:image` and `twitter:image` are absolute `https://` URLs
+- favicon package (svg, ico, 16/32 png, apple-touch-icon 180×180,
+ 192 / 512 PWA icons) and `site.webmanifest` are present, with all
+ PNGs at their declared dimensions
+- `site.webmanifest` is valid JSON and every icon resolves
- no `localhost:` URLs are baked into hrefs/srcs
- key sections (`#why`, `#loop`, `#screens`, `#start`) are wired up
diff --git a/site/assets/apple-touch-icon.png b/site/assets/apple-touch-icon.png
new file mode 100644
index 0000000..2738ce4
Binary files /dev/null and b/site/assets/apple-touch-icon.png differ
diff --git a/site/assets/favicon-16.png b/site/assets/favicon-16.png
new file mode 100644
index 0000000..276d2dc
Binary files /dev/null and b/site/assets/favicon-16.png differ
diff --git a/site/assets/favicon-32.png b/site/assets/favicon-32.png
new file mode 100644
index 0000000..6b26e15
Binary files /dev/null and b/site/assets/favicon-32.png differ
diff --git a/site/assets/favicon.ico b/site/assets/favicon.ico
new file mode 100644
index 0000000..7f8124f
Binary files /dev/null and b/site/assets/favicon.ico differ
diff --git a/site/assets/icon-192.png b/site/assets/icon-192.png
new file mode 100644
index 0000000..afed3b2
Binary files /dev/null and b/site/assets/icon-192.png differ
diff --git a/site/assets/icon-512.png b/site/assets/icon-512.png
new file mode 100644
index 0000000..86324db
Binary files /dev/null and b/site/assets/icon-512.png differ
diff --git a/site/assets/og-image-square.png b/site/assets/og-image-square.png
new file mode 100644
index 0000000..b554294
Binary files /dev/null and b/site/assets/og-image-square.png differ
diff --git a/site/assets/og-image.png b/site/assets/og-image.png
index 177df90..3768161 100644
Binary files a/site/assets/og-image.png and b/site/assets/og-image.png differ
diff --git a/site/index.html b/site/index.html
index 110884d..1ca1c25 100644
--- a/site/index.html
+++ b/site/index.html
@@ -3,26 +3,63 @@
- Python Tutor — Private Python practice with a local AI tutor
-
+ Python Tutor — Offline-first Python practice with a local AI tutor
+
+
+
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
-
+
-
-
-
+
+
+
+
+
+
+
diff --git a/site/site.webmanifest b/site/site.webmanifest
new file mode 100644
index 0000000..71fc2b8
--- /dev/null
+++ b/site/site.webmanifest
@@ -0,0 +1,39 @@
+{
+ "name": "Python Tutor — Private Python practice with a local AI tutor",
+ "short_name": "Python Tutor",
+ "description": "Offline-first Python tutor with a local AI mentor. Two-command install, runs entirely on your laptop with Ollama and Gemma. Source-backed docs when online.",
+ "start_url": "./",
+ "scope": "./",
+ "display": "standalone",
+ "background_color": "#0c0c0d",
+ "theme_color": "#0c0c0d",
+ "icons": [
+ {
+ "src": "./assets/favicon-16.png",
+ "sizes": "16x16",
+ "type": "image/png"
+ },
+ {
+ "src": "./assets/favicon-32.png",
+ "sizes": "32x32",
+ "type": "image/png"
+ },
+ {
+ "src": "./assets/apple-touch-icon.png",
+ "sizes": "180x180",
+ "type": "image/png"
+ },
+ {
+ "src": "./assets/icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "any maskable"
+ },
+ {
+ "src": "./assets/icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "any maskable"
+ }
+ ]
+}