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 <head> 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: `<title>`, `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: <https://developers.facebook.com/tools/debug/> +- LinkedIn: <https://www.linkedin.com/post-inspector/> +- X / Twitter: <https://cards-dev.twitter.com/validator> (or just paste + into a draft tweet) +- Generic: <https://www.opengraph.xyz/> + +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 -- `<title>` and Open Graph tags are present +- referenced screenshots and social assets exist on disk +- complete `<head>` 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 @@ <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" /> - <title>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" + } + ] +}