Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions scripts/check_site.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,110 @@ need() {
grep -q -- "$1" "$HTML" || fail "site/index.html missing: $1"
}
need "<title>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"
Expand Down
56 changes: 51 additions & 5 deletions site/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand Down
Binary file added site/assets/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/favicon-16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/favicon-32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/favicon.ico
Binary file not shown.
Binary file added site/assets/icon-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added site/assets/og-image-square.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified site/assets/og-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
59 changes: 48 additions & 11 deletions site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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</title>
<meta name="description" content="A private, offline Python tutor that runs entirely on your own machine. Lessons, an interactive code lab, and a local AI mentor — no accounts, no cloud, no telemetry. Clone the repo and run two commands to start." />
<title>Python Tutor — Offline-first Python practice with a local AI tutor</title>
<meta name="description" content="Offline-first Python tutor with a local AI mentor. Two-command install, then fully offline with Ollama and Gemma. Source-backed docs when online. No accounts, no cloud, no telemetry." />
<meta name="keywords" content="Python tutor, offline Python, local AI tutor, learn Python, Ollama, Gemma, private LLM, local-first, open source" />
<meta name="author" content="Stew Alexander" />
<meta name="robots" content="index, follow, max-image-preview:large" />
<meta name="theme-color" content="#0c0c0d" />
<link rel="icon" type="image/svg+xml" href="./assets/favicon.svg" />
<meta name="color-scheme" content="dark" />
<link rel="canonical" href="https://stewalexander-com.github.io/python-tutor/" />

<!-- Open Graph -->
<!-- Favicons -->
<link rel="icon" type="image/svg+xml" href="./assets/favicon.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="./assets/favicon-32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="./assets/favicon-16.png" />
<link rel="shortcut icon" href="./assets/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="./assets/apple-touch-icon.png" />
<link rel="mask-icon" href="./assets/favicon.svg" color="#e8a13b" />
<link rel="manifest" href="./site.webmanifest" />
<meta name="apple-mobile-web-app-title" content="Python Tutor" />
<meta name="application-name" content="Python Tutor" />

<!-- Open Graph / Facebook / LinkedIn / Messenger / iMessage -->
<meta property="og:type" content="website" />
<meta property="og:title" content="Python Tutor — Private Python practice with a local AI tutor" />
<meta property="og:description" content="Lessons, a real code lab, and an AI mentor — all on your laptop. No accounts. No cloud. No telemetry." />
<meta property="og:image" content="./assets/og-image.png" />
<meta property="og:site_name" content="Python Tutor" />
<meta property="og:locale" content="en_US" />
<meta property="og:title" content="Python Tutor — Offline-first Python practice with a local AI tutor" />
<meta property="og:description" content="Two-command install, then fully offline with Ollama and Gemma. Lessons, a real code lab, and an AI mentor — all on your laptop. No accounts. No cloud. No telemetry." />
<meta property="og:url" content="https://stewalexander-com.github.io/python-tutor/" />
<meta property="og:image" content="https://stewalexander-com.github.io/python-tutor/assets/og-image.png" />
<meta property="og:image:secure_url" content="https://stewalexander-com.github.io/python-tutor/assets/og-image.png" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content="https://stewalexander-com.github.io/python-tutor/" />
<meta property="og:image:alt" content="Python Tutor — Private Python practice with a local AI tutor. Two commands: ./install.sh and ./run.sh --open-browser." />

<!-- Twitter / X -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Python Tutor — Private Python practice with a local AI tutor" />
<meta name="twitter:description" content="Lessons, a real code lab, and an AI mentor — all on your laptop. No accounts. No cloud. No telemetry." />
<meta name="twitter:image" content="./assets/og-image.png" />
<meta name="twitter:title" content="Python Tutor — Offline-first Python practice with a local AI tutor" />
<meta name="twitter:description" content="Two-command install, then fully offline with Ollama and Gemma. Lessons, a real code lab, and an AI mentor — all on your laptop. No accounts. No cloud. No telemetry." />
<meta name="twitter:image" content="https://stewalexander-com.github.io/python-tutor/assets/og-image.png" />
<meta name="twitter:image:alt" content="Python Tutor — Private Python practice with a local AI tutor. Two commands: ./install.sh and ./run.sh --open-browser." />

<!-- JSON-LD structured data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Python Tutor",
"description": "Offline-first Python tutor with a local AI mentor. Two-command install, then fully offline with Ollama and Gemma. Source-backed docs when online.",
"url": "https://stewalexander-com.github.io/python-tutor/",
"image": "https://stewalexander-com.github.io/python-tutor/assets/og-image.png",
"applicationCategory": "EducationalApplication",
"operatingSystem": "macOS, Linux",
"license": "https://opensource.org/licenses/MIT",
"codeRepository": "https://github.com/StewAlexander-com/python-tutor",
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" }
}
</script>

<link rel="stylesheet" href="./style.css" />
</head>
Expand Down
39 changes: 39 additions & 0 deletions site/site.webmanifest
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
Loading