Quellcode durchsuchen

check-links: archive stale entries and update metadata

Assisted-by: OpenCode (deepseek-v4-flash)
Thibaud Colas vor 23 Stunden
Ursprung
Commit
2371ab792a
5 geänderte Dateien mit 1246 neuen und 85 gelöschten Zeilen
  1. 115 0
      .agents/skills/check-links/SKILL.md
  2. 911 0
      .agents/skills/check-links/scripts/check_links.py
  3. 29 84
      README.md
  4. 11 1
      docs/CONTRIBUTING.md
  5. 180 0
      docs/archive.md

+ 115 - 0
.agents/skills/check-links/SKILL.md

@@ -0,0 +1,115 @@
+---
+name: check-links
+description: Guides the automated archival workflow of awesome links. Fetching links data, moving stale entries.
+license: CC0
+metadata:
+  audience: maintainers
+  # Hide the skill from discovery: https://github.com/vercel-labs/skills#optional-fields
+  internal: true
+  disable-model-invocation: true
+---
+
+## Overview
+
+Analyze and maintain links in the README. Fetches metadata for each link, detects stale entries, auto-updates GitHub repo titles and descriptions, and produces a structured report for archival decisions.
+
+## Requirements
+
+- **Python 3.12+** with [`uv`](https://docs.astral.sh/uv/)
+- **`gh` CLI** authenticated (`gh auth status`)
+- Network access for HTTP requests (httpx)
+
+## Available scripts
+
+- **`scripts/check_links.py`** — Fetches metadata for all links in `README.md` and generates a Markdown report on stdout.
+
+## Workflow
+
+### 1. Run the link checker
+
+```bash
+./scripts/check_links.py --dry-run
+```
+
+Always run with `--dry-run` first to preview changes without modifying
+the README. Re-run without `--dry-run` to auto-update GitHub repo titles
+and descriptions when you're ready.
+
+Add `--verbose` to see progress and API call details on stderr.
+
+The database is stored at `.awesome-wagtail.duckdb` by default. Subsequent
+runs reuse cached data (24 h for GitHub, 30 d for web pages).
+
+### 2. Read the report
+
+The report printed to stdout is your action plan. It contains these sections:
+
+#### Auto-Archive Candidates (GitHub)
+
+Repos that are **archived on GitHub** or **have not been pushed to in over
+24 months**. These should be moved to `docs/archive.md` and removed from
+`README.md`.
+
+#### Needs Review — 12–24 months (GitHub)
+
+Repos with no commits in 12–24 months. For each entry, check whether the
+repo's GitHub page or its entry in `README.md` mentions "archived",
+"deprecated", or "unmaintained". If so, move to archive. If the repo is
+still active despite infrequent commits, leave it in place.
+
+#### Updated Link Titles (GitHub)
+
+Repo link titles that differ from the project name found in the repo's
+README heading. Review the list — many README headings contain badge
+markup or instructions rather than a clean project name. Only update
+titles that are clearly better.
+
+#### Updated Link Descriptions (GitHub)
+
+Repo descriptions that differ from GitHub's project description. In most
+cases the GitHub description is shorter and more accurate. Apply these
+updates to `README.md`.
+
+#### Auto-Archive Candidates (web pages)
+
+Pages that have not been updated in over 5 years (based on HTTP
+`Last-Modified` headers, `<meta>` tags, or schema.org data). Move these
+to `docs/archive.md`.
+
+#### Title Mismatches (web pages)
+
+Page titles in `README.md` that differ from the actual `<h1>` or
+`<title>` of the page. Review and update if the fetched title is a
+better description of the link.
+
+### 3. Create or update the archive file
+
+The archive lives at `docs/archive.md`. Mirror the same section structure
+as `README.md` (section > subsection). For each archived link:
+
+1. Remove the line from `README.md`
+2. Add the line under the matching section in `docs/archive.md`
+
+If the archive file does not exist yet, create it with:
+
+```markdown
+# Awesome Wagtail Archive
+
+> Archived projects from the main README that are no longer actively maintained.
+
+## Apps
+
+### Blogging/news
+
+... archived items moved here ...
+```
+
+### 4. Commit changes
+
+When finished, review the diff and commit:
+
+```bash
+git diff
+git add -A
+git commit -m "check-links: archive stale entries and update metadata" --trailer "Assisted-by: <Agent harness> (<Model name>)"
+```

+ 911 - 0
.agents/skills/check-links/scripts/check_links.py

@@ -0,0 +1,911 @@
+#!/usr/bin/env -S uv run
+# /// script
+# requires-python = ">=3.12"
+# dependencies = [
+#   "httpx>=0.27",
+#   "beautifulsoup4>=4.12",
+#   "duckdb>=1.0",
+#   "lxml",
+# ]
+# ///
+
+"""
+Check links in README.md — fetch metadata, detect stale entries, and auto-update
+GitHub repo titles and descriptions.
+
+Usage:
+  .agents/skills/check-links/scripts/check_links.py [OPTIONS]
+
+Options:
+  --readme PATH       Path to README.md (default: README.md)
+  --db PATH           DuckDB database path (default: .awesome-wagtail.duckdb)
+  --dry-run           Don't modify files, only report
+  --verbose           Print progress to stderr
+  --help              Show this message
+"""
+
+import argparse
+import base64
+import json
+import re
+import subprocess
+import sys
+import time
+from datetime import datetime, timedelta, timezone
+from pathlib import Path
+from typing import Any
+
+import duckdb
+import httpx
+from bs4 import BeautifulSoup
+
+REPO_CACHE_MAX_AGE = timedelta(hours=24)
+PAGE_CACHE_MAX_AGE = timedelta(days=30)
+
+STALE_12_MONTHS = timedelta(days=365)
+STALE_24_MONTHS = timedelta(days=730)
+STALE_5_YEARS = timedelta(days=1825)
+
+SKIP_SECTIONS = {"Contents", "Contribute", "License"}
+
+
+def parse_date_utc(date_str: str | None) -> datetime | None:
+    if not date_str:
+        return None
+    try:
+        dt = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
+        if dt.tzinfo is None:
+            return dt.replace(tzinfo=timezone.utc)
+        return dt
+    except (ValueError, TypeError):
+        try:
+            dt = datetime.strptime(date_str, "%a, %d %b %Y %H:%M:%S %Z")
+            return dt.replace(tzinfo=timezone.utc)
+        except (ValueError, TypeError):
+            return None
+
+
+def parse_args() -> argparse.Namespace:
+    parser = argparse.ArgumentParser(
+        description="Check links in README.md and fetch metadata."
+    )
+    parser.add_argument(
+        "--readme",
+        default="README.md",
+        help="Path to README.md (default: README.md)",
+    )
+    parser.add_argument(
+        "--db",
+        default=".awesome-wagtail.duckdb",
+        help="DuckDB database path (default: .awesome-wagtail.duckdb)",
+    )
+    parser.add_argument(
+        "--dry-run",
+        action="store_true",
+        help="Don't modify files, only report",
+    )
+    parser.add_argument(
+        "--verbose",
+        action="store_true",
+        help="Print progress to stderr",
+    )
+    return parser.parse_args()
+
+
+def log(msg: str, verbose: bool = False) -> None:
+    if verbose:
+        print(msg, file=sys.stderr)
+
+
+# ── README Parsing (adapted from .github/api.py) ──────────────────────
+
+
+def parse_line(
+    line: str, section: str, subsection: str | None
+) -> dict[str, str] | None:
+    line = line.strip()
+    if not line.startswith("- [") and not line.startswith("– ["):
+        return None
+    match = re.match(r"^[-–] \[([^\]]+)\]\(([^\)]+)\)(?:\s*[-–]\s*(.*))?$", line)
+    if not match:
+        return None
+    name = match.group(1)
+    url = match.group(2)
+    description = (match.group(3) or "").strip()
+    category = f"{section} > {subsection}" if subsection else section
+    return {
+        "name": name,
+        "url": url,
+        "description": description,
+        "section": section,
+        "subsection": subsection,
+        "category": category,
+    }
+
+
+def parse_readme_links(readme: str) -> list[dict[str, str]]:
+    lines = readme.split("\n")
+    links: list[dict[str, str]] = []
+    current_section: str | None = None
+    current_subsection: str | None = None
+
+    for line in lines:
+        if line.startswith("## ") and not line.startswith("### "):
+            heading = line[2:].strip()
+            if heading in SKIP_SECTIONS:
+                current_section = None
+                current_subsection = None
+            else:
+                current_section = heading
+                current_subsection = None
+        elif line.startswith("### ") and current_section:
+            current_subsection = line[4:].strip()
+        elif line.startswith("- [") or line.startswith("– ["):
+            if current_section:
+                link = parse_line(line, current_section, current_subsection)
+                if link:
+                    links.append(link)
+    return links
+
+
+# ── URL Classification ──────────────────────────────────────────────
+
+
+def is_youtube_url(url: str) -> bool:
+    return bool(re.search(r"(youtube\.com|youtu\.be)", url))
+
+
+def is_github_url(url: str) -> bool:
+    return bool(re.match(r"https?://github\.com/", url))
+
+
+def extract_github_repo(url: str) -> tuple[str, str] | None:
+    match = re.match(r"https?://github\.com/([^/]+)/([^/#\?]+)", url)
+    if match:
+        owner = match.group(1)
+        repo = match.group(2).replace(".git", "")
+        return owner, repo
+    return None
+
+
+# ── DuckDB Cache ──────────────────────────────────────────────────
+
+
+def init_db(db_path: str) -> duckdb.DuckDBPyConnection:
+    con = duckdb.connect(db_path)
+    con.execute("""
+        CREATE TABLE IF NOT EXISTS link_metadata (
+            url VARCHAR PRIMARY KEY,
+            link_type VARCHAR,
+            category VARCHAR,
+            fetched_at TIMESTAMP,
+            repo_full_name VARCHAR,
+            repo_description VARCHAR,
+            star_count INTEGER,
+            pushed_at VARCHAR,
+            is_archived BOOLEAN,
+            readme_content VARCHAR,
+            project_name VARCHAR,
+            topics VARCHAR,
+            page_title VARCHAR,
+            h1_title VARCHAR,
+            meta_description VARCHAR,
+            author VARCHAR,
+            page_last_updated VARCHAR
+        )
+    """)
+    # Migrate old schema: rename tags -> topics if needed
+    try:
+        con.execute("ALTER TABLE link_metadata DROP COLUMN tags")
+    except Exception:
+        pass
+    try:
+        con.execute("ALTER TABLE link_metadata ADD COLUMN topics VARCHAR")
+    except Exception:
+        pass
+    return con
+
+
+def get_cached(
+    con: duckdb.DuckDBPyConnection, url: str, max_age: timedelta
+) -> dict[str, Any] | None:
+    row = con.execute(
+        "SELECT * FROM link_metadata WHERE url = ? AND fetched_at > ?",
+        [url, datetime.now(timezone.utc) - max_age],
+    ).fetchone()
+    if row is None:
+        return None
+    columns = [desc[0] for desc in con.description]
+    return dict(zip(columns, row))
+
+
+def set_cached(con: duckdb.DuckDBPyConnection, url: str, data: dict[str, Any]) -> None:
+    columns = list(data.keys())
+    placeholders = ", ".join(["?" for _ in columns])
+    col_names = ", ".join(columns)
+    values = [data.get(col) for col in columns]
+    con.execute(
+        f"INSERT OR REPLACE INTO link_metadata ({col_names}) VALUES ({placeholders})",
+        values,
+    )
+
+
+# ── GitHub Metadata Fetching ─────────────────────────────────────
+
+
+def run_gh(args: list[str]) -> subprocess.CompletedProcess:
+    return subprocess.run(
+        ["gh"] + args,
+        capture_output=True,
+        text=True,
+        check=False,
+    )
+
+
+def fetch_github_batch(repos: list[tuple[str, str, str]]) -> dict[str, dict[str, Any]]:
+    """Fetch metadata for a batch of GitHub repos via GraphQL.
+
+    Args:
+        repos: list of (url, owner, repo) tuples
+    Returns:
+        dict of url -> metadata dict
+    """
+    if not repos:
+        return {}
+
+    query_parts = []
+    alias_map: dict[str, str] = {}
+
+    for i, (url, owner, repo) in enumerate(repos):
+        alias = f"r{i}"
+        alias_map[alias] = url
+        query_parts.append(
+            f'  {alias}: repository(owner: "{owner}", name: "{repo}") {{\n'
+            f"    name\n"
+            f"    description\n"
+            f"    stargazerCount\n"
+            f"    pushedAt\n"
+            f"    isArchived\n"
+            f"    url\n"
+            f"    repositoryTopics(first: 10) {{\n"
+            f"      nodes {{\n"
+            f"        topic {{\n"
+            f"          name\n"
+            f"        }}\n"
+            f"      }}\n"
+            f"    }}\n"
+            f"  }}"
+        )
+
+    query = "{\n" + "\n".join(query_parts) + "\n}"
+    result = run_gh(["api", "graphql", "-f", f"query={query}"])
+
+    if result.returncode != 0:
+        log(f"  GraphQL error: {result.stderr[:300]}", verbose=True)
+        return {}
+
+    try:
+        data = json.loads(result.stdout)
+    except json.JSONDecodeError:
+        log(f"  JSON parse error in GraphQL response", verbose=True)
+        return {}
+
+    url_to_repo = {url: (owner, repo) for url, owner, repo in repos}
+
+    results: dict[str, dict[str, Any]] = {}
+    errors = data.get("errors", [])
+    if errors:
+        for err in errors:
+            log(f"  GraphQL error: {err.get('message', '')}", verbose=True)
+
+    for alias, url in alias_map.items():
+        repo_data = data.get("data", {}).get(alias)
+        if repo_data is None:
+            results[url] = {"error": "Repository not found"}
+        else:
+            owner, repo_name = url_to_repo.get(url, ("", ""))
+            topic_nodes = (repo_data.get("repositoryTopics") or {}).get("nodes") or []
+            topics = [
+                n["topic"]["name"]
+                for n in topic_nodes
+                if n and n.get("topic") and n["topic"].get("name")
+            ]
+            results[url] = {
+                "repo_full_name": f"{owner}/{repo_name}"
+                if owner and repo_name
+                else None,
+                "repo_name": repo_data.get("name"),
+                "repo_description": repo_data.get("description"),
+                "star_count": repo_data.get("stargazerCount"),
+                "pushed_at": repo_data.get("pushedAt"),
+                "is_archived": repo_data.get("isArchived"),
+                "repo_url": repo_data.get("url"),
+                "topics": json.dumps(topics) if topics else None,
+            }
+
+    return results
+
+
+def fetch_github_readme(owner: str, repo: str) -> str | None:
+    result = run_gh(["api", f"repos/{owner}/{repo}/readme"])
+    if result.returncode != 0:
+        return None
+    try:
+        data = json.loads(result.stdout)
+        content = data.get("content", "")
+        return base64.b64decode(content).decode("utf-8")
+    except (json.JSONDecodeError, Exception):
+        return None
+
+
+
+
+
+def extract_project_name(readme_content: str | None) -> str | None:
+    if not readme_content:
+        return None
+    match = re.search(r"^#\s+(.+)$", readme_content, re.MULTILINE)
+    if match:
+        return match.group(1).strip()
+    return None
+
+
+# ── Web Page Metadata Fetching ───────────────────────────────────
+
+
+def fetch_page_metadata(url: str) -> dict[str, Any]:
+    metadata: dict[str, Any] = {
+        "page_title": None,
+        "h1_title": None,
+        "meta_description": None,
+        "author": None,
+        "page_last_updated": None,
+    }
+
+    try:
+        with httpx.Client(follow_redirects=True, timeout=15.0) as client:
+            response = client.get(
+                url, headers={"User-Agent": "awesome-wagtail-link-checker/1.0"}
+            )
+        response.raise_for_status()
+    except httpx.HTTPError as e:
+        metadata["error"] = str(e)
+        return metadata
+
+    soup = BeautifulSoup(response.text, "lxml")
+
+    title_tag = soup.find("title")
+    metadata["page_title"] = title_tag.get_text(strip=True) if title_tag else None
+
+    h1 = soup.find("h1")
+    metadata["h1_title"] = h1.get_text(strip=True) if h1 else None
+
+    meta_desc = soup.find("meta", attrs={"name": "description"})
+    if meta_desc and meta_desc.get("content"):
+        metadata["meta_description"] = meta_desc["content"].strip()
+    else:
+        meta_og_desc = soup.find("meta", attrs={"property": "og:description"})
+        if meta_og_desc and meta_og_desc.get("content"):
+            metadata["meta_description"] = meta_og_desc["content"].strip()
+
+    metadata["author"] = find_author(soup)
+
+    metadata["page_last_updated"] = find_last_updated(soup, response)
+
+    return metadata
+
+
+def find_author(soup: BeautifulSoup) -> str | None:
+    meta_author = soup.find("meta", attrs={"name": "author"})
+    if meta_author and meta_author.get("content"):
+        return meta_author["content"].strip()
+
+    twitter_creator = soup.find("meta", attrs={"name": "twitter:creator"})
+    if twitter_creator and twitter_creator.get("content"):
+        return twitter_creator["content"].strip()
+
+    article_author = soup.find("meta", attrs={"property": "article:author"})
+    if article_author and article_author.get("content"):
+        return article_author["content"].strip()
+
+    rel_author = soup.find("link", attrs={"rel": "author"})
+    if rel_author and rel_author.get("href"):
+        return rel_author["href"].strip()
+
+    for script in soup.find_all("script", type="application/ld+json"):
+        try:
+            data = json.loads(script.string)
+            if isinstance(data, dict):
+                author = data.get("author", {})
+                if isinstance(author, dict):
+                    return author.get("name")
+                elif isinstance(author, str):
+                    return author
+            elif isinstance(data, list):
+                for item in data:
+                    if isinstance(item, dict):
+                        author = item.get("author", {})
+                        if isinstance(author, dict):
+                            return author.get("name")
+        except (json.JSONDecodeError, AttributeError):
+            pass
+
+    return None
+
+
+def find_last_updated(soup: BeautifulSoup, response: httpx.Response) -> str | None:
+    for meta_name in ("date", "revised", "dcterms.modified"):
+        meta = soup.find("meta", attrs={"name": meta_name})
+        if meta and meta.get("content"):
+            return meta["content"].strip()
+
+    for meta_prop in ("article:modified_time", "article:published_time"):
+        meta = soup.find("meta", attrs={"property": meta_prop})
+        if meta and meta.get("content"):
+            return meta["content"].strip()
+
+    time_tag = soup.find("time", attrs={"datetime": True})
+    if time_tag and time_tag.get("datetime"):
+        return time_tag["datetime"].strip()
+
+    for script in soup.find_all("script", type="application/ld+json"):
+        try:
+            data = json.loads(script.string)
+            if isinstance(data, dict):
+                modified = data.get("dateModified") or data.get("datePublished")
+                if modified:
+                    return modified
+            elif isinstance(data, list):
+                for item in data:
+                    if isinstance(item, dict):
+                        modified = item.get("dateModified") or item.get("datePublished")
+                        if modified:
+                            return modified
+        except (json.JSONDecodeError, AttributeError):
+            pass
+
+    if "last-modified" in response.headers:
+        return response.headers["last-modified"]
+
+    return None
+
+
+# ── README Auto-Update for GitHub Repos ──────────────────────────
+
+
+def auto_update_readme_lines(
+    lines: list[str],
+    updates: list[dict[str, str]],
+) -> list[str]:
+    updated = list(lines)
+    for upd in updates:
+        old_url = upd["url"]
+        new_title = upd.get("new_title")
+        new_desc = upd.get("new_description")
+        for i, line in enumerate(updated):
+            if old_url in line:
+                link_match = re.match(
+                    r"^(\s*[-–]\s*)\[([^\]]+)\]\(([^\)]+)\)(\s*[-–]\s*(.*))?$",
+                    line.strip(),
+                )
+                if link_match:
+                    prefix = link_match.group(1)
+                    old_desc = (link_match.group(5) or "").strip()
+                    if new_title:
+                        if new_desc and new_desc != old_desc:
+                            updated[i] = (
+                                f"{prefix}[{new_title}]({old_url}) - {new_desc}\n"
+                            )
+                        else:
+                            updated[i] = (
+                                f"{prefix}[{new_title}]({old_url}) - {old_desc}\n"
+                                if old_desc
+                                else f"{prefix}[{new_title}]({old_url})\n"
+                            )
+    return updated
+
+
+# ── Report Generation ─────────────────────────────────────────────
+
+
+def generate_report(
+    all_links: list[dict[str, Any]],
+    today: datetime,
+) -> str:
+    lines: list[str] = []
+    lines.append("# Link Check Report\n")
+    lines.append(f"Generated: {today.isoformat()}\n")
+
+    gh_repos = [l for l in all_links if l.get("link_type") == "github"]
+    web_links = [l for l in all_links if l.get("link_type") == "web"]
+    youtube_links = [l for l in all_links if l.get("link_type") == "youtube"]
+    errors = [l for l in all_links if l.get("error")]
+
+    lines.append("## Summary\n")
+    lines.append(f"| Metric | Count |")
+    lines.append(f"|--------|-------|")
+    lines.append(f"| Total links | {len(all_links)} |")
+    lines.append(f"| GitHub repos | {len(gh_repos)} |")
+    lines.append(f"| Web pages | {len(web_links)} |")
+    lines.append(f"| YouTube (skipped) | {len(youtube_links)} |")
+    lines.append(f"| Errors | {len(errors)} |\n")
+
+    if gh_repos:
+        lines.append("## GitHub Repos\n")
+
+        auto_archive = []
+        needs_review = []
+        active = []
+        title_updates = []
+
+        for link in gh_repos:
+            pushed_at = link.get("pushed_at")
+            is_archived = link.get("is_archived", False)
+            stale_date = None
+            pushed_date = parse_date_utc(pushed_at)
+            if pushed_date:
+                stale_date = today - pushed_date
+
+            if link.get("new_title") and link["new_title"] != link.get("name"):
+                title_updates.append(link)
+
+            if is_archived:
+                auto_archive.append(link)
+            elif stale_date and stale_date > STALE_24_MONTHS:
+                auto_archive.append(link)
+            elif stale_date and stale_date > STALE_12_MONTHS:
+                needs_review.append(link)
+            else:
+                active.append(link)
+
+        if auto_archive:
+            lines.append(f"### Auto-Archive Candidates ({len(auto_archive)})\n")
+            lines.append(
+                "These repos are archived or have not been updated in over 24 months.\n"
+            )
+            lines.append("| # | Link | Stars | Last Commit | Archived | Category |")
+            lines.append("|---|------|-------|-------------|----------|----------|")
+            for i, link in enumerate(auto_archive, 1):
+                stars = link.get("star_count") or "?"
+                pushed = (link.get("pushed_at") or "?")[:10]
+                arch = "Yes" if link.get("is_archived") else "No"
+                url = link["url"]
+                title = link.get("name", "")
+                desc = link.get("description", "")
+                link_text = f"[{title}]({url})" + (f" - {desc}" if desc else "")
+                lines.append(
+                    f"| {i} | {link_text} | {stars} | {pushed} | {arch} | {link.get('category', '')} |"
+                )
+            lines.append("")
+
+        if needs_review:
+            lines.append(f"### Needs Review — 12–24 months ({len(needs_review)})\n")
+            lines.append(
+                "These repos have not been updated in 12–24 months. Check if they mention 'archived' or 'deprecated'.\n"
+            )
+            lines.append("| # | Link | Stars | Last Commit | Category |")
+            lines.append("|---|------|-------|-------------|----------|")
+            for i, link in enumerate(needs_review, 1):
+                stars = link.get("star_count") or "?"
+                pushed = (link.get("pushed_at") or "?")[:10]
+                url = link["url"]
+                title = link.get("name", "")
+                desc = link.get("description", "")
+                link_text = f"[{title}]({url})" + (f" - {desc}" if desc else "")
+                lines.append(
+                    f"| {i} | {link_text} | {stars} | {pushed} | {link.get('category', '')} |"
+                )
+            lines.append("")
+
+        if active:
+            lines.append(f"### Active Repos ({len(active)})\n")
+            lines.append("| # | Link | Stars | Last Commit | Archived | Category |")
+            lines.append("|---|------|-------|-------------|----------|----------|")
+            for i, link in enumerate(active, 1):
+                stars = link.get("star_count") or "?"
+                pushed = (link.get("pushed_at") or "?")[:10]
+                arch = "Yes" if link.get("is_archived") else "No"
+                url = link["url"]
+                title = link.get("name", "")
+                desc = link.get("description", "")
+                link_text = f"[{title}]({url})" + (f" - {desc}" if desc else "")
+                lines.append(
+                    f"| {i} | {link_text} | {stars} | {pushed} | {arch} | {link.get('category', '')} |"
+                )
+            lines.append("")
+
+        if title_updates:
+            lines.append(f"### Updated Link Titles ({len(title_updates)})\n")
+            lines.append(
+                "The following GitHub repo links had titles auto-updated to match their README project name.\n"
+            )
+            lines.append("| URL | Old Title | New Title |")
+            lines.append("|-----|-----------|-----------|")
+            for link in title_updates:
+                lines.append(
+                    f"| {link['url']} | {link.get('name', '')} | {link.get('new_title', '')} |"
+                )
+            lines.append("")
+
+        if title_updates:
+            lines.append(f"### Updated Link Descriptions ({len(title_updates)})\n")
+            lines.append(
+                "The following GitHub repo links had descriptions auto-updated to match the GitHub project description.\n"
+            )
+            lines.append("| URL | Old Description | New Description |")
+            lines.append("|-----|-----------------|-----------------|")
+            for link in title_updates:
+                old_desc = link.get("description", "")
+                new_desc = link.get("repo_description", "") or "(none)"
+                if old_desc != new_desc:
+                    lines.append(f"| {link['url']} | {old_desc} | {new_desc} |")
+            lines.append("")
+
+    if web_links:
+        lines.append("## Other Links\n")
+
+        auto_archive_web = []
+        title_mismatches = []
+
+        for link in web_links:
+            last_updated = link.get("page_last_updated")
+            updated_date = parse_date_utc(last_updated)
+            if updated_date and today - updated_date > STALE_5_YEARS:
+                auto_archive_web.append(link)
+
+            page_title = link.get("h1_title") or link.get("page_title")
+            old_title = link.get("name", "")
+            if (
+                page_title
+                and old_title
+                and old_title not in page_title
+                and page_title not in old_title
+            ):
+                title_mismatches.append(link)
+
+        if auto_archive_web:
+            lines.append(
+                f"### Auto-Archive Candidates — 5+ years ({len(auto_archive_web)})\n"
+            )
+            lines.append("These pages have not been updated in over 5 years.\n")
+            lines.append("| # | URL | Title | Last Updated | Category |")
+            lines.append("|---|-----|-------|-------------|----------|")
+            for i, link in enumerate(auto_archive_web, 1):
+                title = (
+                    link.get("h1_title")
+                    or link.get("page_title")
+                    or link.get("name", "")
+                )
+                updated = (link.get("page_last_updated") or "?")[:10]
+                url = link["url"]
+                lines.append(
+                    f"| {i} | [{title}]({url}) | {title} | {updated} | {link.get('category', '')} |"
+                )
+            lines.append("")
+
+        if title_mismatches:
+            lines.append(f"### Title Mismatches ({len(title_mismatches)})\n")
+            lines.append("The README link title differs from the page title.\n")
+            lines.append("| # | URL | README Title | Page Title |")
+            lines.append("|---|-----|-------------|------------|")
+            for i, link in enumerate(title_mismatches, 1):
+                page_title = link.get("h1_title") or link.get("page_title") or ""
+                lines.append(
+                    f"| {i} | {link['url']} | {link.get('name', '')} | {page_title} |"
+                )
+            lines.append("")
+
+    if errors:
+        lines.append("## Errors\n")
+        lines.append("| URL | Error |")
+        lines.append("|-----|-------|")
+        for link in errors:
+            lines.append(f"| {link['url']} | {link.get('error', 'Unknown')} |")
+        lines.append("")
+
+    return "\n".join(lines)
+
+
+# ── Main ──────────────────────────────────────────────────────────
+
+
+def main() -> None:
+    args = parse_args()
+    verbose = args.verbose
+    dry_run = args.dry_run
+
+    readme_path = Path(args.readme)
+    if not readme_path.exists():
+        print(f"Error: README not found at {readme_path}", file=sys.stderr)
+        sys.exit(1)
+
+    log(f"Reading {readme_path}...", verbose)
+    readme = readme_path.read_text(encoding="utf-8")
+
+    log("Parsing links...", verbose)
+    links = parse_readme_links(readme)
+    log(f"  Found {len(links)} links", verbose)
+
+    log("Initializing DuckDB cache...", verbose)
+    con = init_db(args.db)
+
+    today = datetime.now(timezone.utc)
+
+    classified_links: list[dict[str, Any]] = []
+
+    for link in links:
+        url = link["url"]
+        if is_youtube_url(url):
+            link["link_type"] = "youtube"
+            classified_links.append(link)
+            continue
+
+        if is_github_url(url):
+            link["link_type"] = "github"
+        else:
+            link["link_type"] = "web"
+        classified_links.append(link)
+
+    # ── Fetch GitHub metadata ──────────────────────────────
+
+    gh_links = [l for l in classified_links if l["link_type"] == "github"]
+    non_cached_gh: list[dict[str, Any]] = []
+
+    for link in gh_links:
+        cached = get_cached(con, link["url"], REPO_CACHE_MAX_AGE)
+        if cached:
+            link.update(cached)
+            link["from_cache"] = True
+            log(f"  [cache] {link['url']}", verbose)
+        else:
+            link["from_cache"] = False
+            non_cached_gh.append(link)
+
+    if non_cached_gh:
+        log(f"Fetching metadata for {len(non_cached_gh)} GitHub repos...", verbose)
+        repo_batches: list[tuple[str, str, str]] = []
+        for link in non_cached_gh:
+            repo = extract_github_repo(link["url"])
+            if repo:
+                repo_batches.append((link["url"], repo[0], repo[1]))
+
+        BATCH_SIZE = 50
+        for i in range(0, len(repo_batches), BATCH_SIZE):
+            batch = repo_batches[i : i + BATCH_SIZE]
+            log(
+                f"  Batch {i // BATCH_SIZE + 1}/{(len(repo_batches) + BATCH_SIZE - 1) // BATCH_SIZE}",
+                verbose,
+            )
+            batch_results = fetch_github_batch(batch)
+
+            for link in non_cached_gh:
+                if link["url"] in batch_results:
+                    result = batch_results[link["url"]]
+                    if "error" in result:
+                        link["error"] = result["error"]
+                    else:
+                        link.update(result)
+
+            for url, owner, repo in batch:
+                result = batch_results.get(url, {})
+                if "error" in result:
+                    continue
+
+                log(f"  Fetching README for {owner}/{repo}...", verbose)
+                readme_content = fetch_github_readme(owner, repo)
+                if readme_content:
+                    link = next((l for l in non_cached_gh if l["url"] == url), None)
+                    if link:
+                        link["readme_content"] = readme_content
+                        link["project_name"] = extract_project_name(readme_content)
+
+                time.sleep(0.1)
+
+        for link in non_cached_gh:
+            cache_data = {
+                "url": link["url"],
+                "link_type": "github",
+                "category": link.get("category", ""),
+                "fetched_at": datetime.now(timezone.utc),
+                "repo_full_name": link.get("repo_full_name"),
+                "repo_description": link.get("repo_description"),
+                "star_count": link.get("star_count"),
+                "pushed_at": link.get("pushed_at"),
+                "is_archived": link.get("is_archived"),
+                "readme_content": link.get("readme_content"),
+                "project_name": link.get("project_name"),
+                "topics": link.get("topics"),
+            }
+            set_cached(con, link["url"], cache_data)
+
+    # ── Fetch web page metadata ────────────────────────────
+
+    web_links = [l for l in classified_links if l["link_type"] == "web"]
+    non_cached_web: list[dict[str, Any]] = []
+
+    for link in web_links:
+        cached = get_cached(con, link["url"], PAGE_CACHE_MAX_AGE)
+        if cached:
+            link.update(cached)
+            link["from_cache"] = True
+            log(f"  [cache] {link['url']}", verbose)
+        else:
+            link["from_cache"] = False
+            non_cached_web.append(link)
+
+    if non_cached_web:
+        log(f"Fetching metadata for {len(non_cached_web)} web pages...", verbose)
+        for link in non_cached_web:
+            log(f"  Fetching {link['url']}...", verbose)
+            page_data = fetch_page_metadata(link["url"])
+            link.update(page_data)
+            if "error" in page_data:
+                log(f"    Error: {page_data['error']}", verbose)
+
+            cache_data = {
+                "url": link["url"],
+                "link_type": "web",
+                "category": link.get("category", ""),
+                "fetched_at": datetime.now(timezone.utc),
+                "page_title": page_data.get("page_title"),
+                "h1_title": page_data.get("h1_title"),
+                "meta_description": page_data.get("meta_description"),
+                "author": page_data.get("author"),
+                "page_last_updated": page_data.get("page_last_updated"),
+            }
+            set_cached(con, link["url"], cache_data)
+
+            time.sleep(0.2)
+
+    # ── Determine updates for GitHub repos ─────────────────
+
+    desc_updates: list[dict[str, str]] = []
+    for link in gh_links:
+        new_title = link.get("project_name") or link.get("repo_name")
+        new_desc = link.get("repo_description")
+        if new_title and new_title != link.get("name"):
+            link["new_title"] = new_title
+        if new_desc and new_desc != link.get("description"):
+            link["new_description"] = new_desc
+            desc_updates.append(
+                {
+                    "url": link["url"],
+                    "new_description": new_desc,
+                }
+            )
+
+    # ── Auto-update README (descriptions only, titles are LLM-reviewable) ──
+
+    if desc_updates and not dry_run:
+        log(
+            f"Updating {len(desc_updates)} GitHub link descriptions in README...",
+            verbose,
+        )
+        readme_lines = readme.split("\n")
+        updated_lines = auto_update_readme_lines(readme_lines, desc_updates)
+        readme_path.write_text("\n".join(updated_lines), encoding="utf-8")
+        log("  README descriptions updated.", verbose)
+
+    # ── Generate report ─────────────────────────────────────
+
+    report = generate_report(classified_links, today)
+    print(report)
+
+    # ── Print cache stats ───────────────────────────────────
+
+    gh_cached = con.execute(
+        "SELECT COUNT(*) FROM link_metadata WHERE link_type = 'github'"
+    ).fetchone()[0]
+    web_cached = con.execute(
+        "SELECT COUNT(*) FROM link_metadata WHERE link_type = 'web'"
+    ).fetchone()[0]
+    log(
+        f"\nCache stats: {gh_cached} GitHub repos, {web_cached} web pages stored in {args.db}",
+        verbose,
+    )
+
+    con.close()
+
+
+if __name__ == "__main__":
+    main()

+ 29 - 84
README.md

@@ -59,34 +59,26 @@ _You might also like [Awesome Django](https://github.com/wsvincent/awesome-djang
 
 - [Puput](https://puput.readthedocs.io/en/latest/) - Puput is a powerful and simple Django app to manage a blog. It uses the awesome Wagtail CMS as content management system.
 - [wagtail_blog](https://gitlab.com/thelabnyc/wagtail_blog) - A WordPress-like blog app implemented in Wagtail.
-- [wagtailnews](https://github.com/neon-jungle/wagtailnews) - A plugin for Wagtail that provides news / blogging functionality.
-- [wagtail-blog-app](https://github.com/Tivix/wagtail-blog-app) - A blog application for the Wagtail Django CMS.
-- [Django Wagtail Feeds](https://github.com/chrisdev/django-wagtail-feeds) - Add support for RSS Feeds, Facebook Instant Articles and Apple News Publisher to your Wagtail CMS Projects.
-- [Snotra_RSS](https://github.com/olopost/snotra_rss) - Snotra_RSS is an Atom and RSS news aggregator app for Wagtail.
-- [wagtail-live](https://github.com/wagtail/wagtail-live) - Build live blogs with Wagtail.
+
 
 ### Rich text editor extensions
 
-- [wagtail-readability](https://github.com/neon-jungle/wagtail-readability) - Test how readable the content you enter into Wagtail is.
-- [wagtailembedder](https://github.com/springload/wagtailembedder) - Snippets embedder for Wagtail richtext fields.
-- [Wagtail TinyMCE](https://github.com/isotoma/wagtailtinymce) - A TinyMCE editor integration for Wagtail.
-- [Wagtail Medium Editor](https://github.com/dperetti/Django-wagtailmedium) - A customizable Medium Editor for Wagtail, with link anchors support.
-- [WagtailDraftail](https://github.com/springload/wagtaildraftail) – Draft.js editor for Wagtail, built upon [Draftail](https://github.com/wagtail/draftail) and [draftjs_exporter](https://github.com/wagtail/draftjs_exporter).
+
 - [Wagtail EditorJS](https://github.com/Nigel2392/wagtail_editorjs) - An [EditorJS](https://editorjs.io/) widget with great support for Wagtail's page, image and document choosers.
 - [Wagtail Terms](https://github.com/smark-1/wagtailterms) - A plugin to add a glossary terms entity to the Draftail editor.
-- [Wagtail Text Alignment](https://github.com/Nigel2392/wagtail_text_alignment) - Align text, headings and more in your Wagtail richtext editor.
+
 - [wagtailmdx](https://github.com/julinodev/wagtailmdx) - A [MDXEditor](https://github.com/mdx-editor/editor) integration for Wagtail as textfield widget.
 
 ### Widgets
 
 - [wagtailgmaps](https://github.com/springload/wagtailgmaps) - Simple Google Maps address formatter for Wagtail fields.
 - [Wagtail-Geo-Widget](https://github.com/Frojd/wagtail-geo-widget) - Google Maps widget for the GeoDjango PointField field in Wagtail.
-- [wagtail-leaflet-widget](https://github.com/icpac-igad/wagtail-leaflet-widget) - A Leaflet JS - OSM based wagtail geo-location widget.
+
 - [wagtail-markdown](https://github.com/torchbox/wagtail-markdown) - Markdown fields and blocks for Wagtail.
 - [wagtail-autocomplete](https://github.com/wagtail/wagtail-autocomplete) - Autocompleting choosers for `ForeignKey`, `ParentalKey`, and `ManyToMany` fields.
 - [wagtail-instance-selector](https://github.com/ixc/wagtail-instance-selector) - A `ForeignKey` widget to create and select related items. Similar to Django's `raw_id_fields`.
 - [wagtail-generic-chooser](https://github.com/wagtail/wagtail-generic-chooser) - provides base classes for building chooser popups and form widgets for the Wagtail admin, matching the look and feel of Wagtail's built-in choosers for pages, documents, snippets and images.
-- [wagtail-multi-upload](https://github.com/spapas/wagtail-multi-upload) - allows uploading of multiple related images for a page.
+
 - [wagtail-color-panel](https://github.com/marteinn/wagtail-color-panel) - Introduces panels for selecting colors in Wagtail.
 - [Wagtail Ace Editor](https://github.com/Nigel2392/wagtail_ace_editor) - An IDE-like code editor right in your Wagtail admin.
 - [Wagtail HTML Editor](https://github.com/kkm-horikawa/wagtail-html-editor) - A CodeMirror 6-powered HTML editor for Wagtail with syntax highlighting, Emmet support, and dark mode.
@@ -94,49 +86,36 @@ _You might also like [Awesome Django](https://github.com/wsvincent/awesome-djang
 ### StreamField
 
 - [Wagtail FontAwesome](https://gitlab.com/alexgleason/wagtailfontawesome) - Add FontAwesome icons to StreamField.
-- [Wagtail Commonblocks](https://github.com/springload/wagtailcommonblocks) - Common StreamField blocks for Wagtail.
-- [Wagtail SVGmap](https://github.com/City-of-Helsinki/wagtail-svgmap) - ImageMap functionality for Wagtail through inline SVGs.
-- [Wagtail ClearStream](https://github.com/hminnovation/wagtailclearstream) - An app to make Wagtail's StreamField more modular.
-- [UWKM Streamfields](https://github.com/UWKM/uwkm_streamfields) – A basic set of Wagtail StreamField blocks for fun and profit.
+
 - [wagtail-inventory](https://github.com/cfpb/wagtail-inventory) - Search Wagtail pages by the StreamField blocks they contain.
 - [Wagtail Code Block](https://github.com/wagtail-nest/wagtailcodeblock) - StreamField code blocks for the Wagtail CMS with real-time PrismJS Syntax Highlighting.
-- [Wagtail Blocks](https://github.com/ibrahimawadhamid/wagtail_blocks) - A Collection of awesome Wagtail CMS stream-field blocks and Charts.
-- [Wagtail Cache Block](https://github.com/AccordBox/wagtail_cache_block) - A templatetag which add HTML fragment cache to your StreamField block
-- [Wagtail UIKit Block](https://github.com/kpsaurus/wagtail-uikitblocks) - A collection of UIKit components that can be used as a Wagtail StreamField block.
+
 
 ### Static site generation
 
 - [Wagtail-bakery](https://github.com/wagtail-nest/wagtail-bakery) - A set of helpers for baking your Django Wagtail site out as flat files.
-- [Wagtail-Netlify](https://github.com/tomdyson/wagtail-netlify) - Easily publish your statically rendered Wagtail site to Netlify.
-- [wagtail-freezer](https://github.com/gasman/wagtail-freezer) - Generates static HTML sites from a Wagtail project.
+
 
 ### Settings management
 
-- [Wagtail-Constance](https://github.com/MechanisM/wagtail-constance) - django-constance integration for Wagtail CMS.
+
 - [Wagtail-Flags](https://github.com/cfpb/wagtail-flags) - Feature flags for Wagtail sites.
-- [Wagtail-Waffle](https://github.com/TheCodingSheikh/wagtail-waffle) - Manage Django Waffle in Wagtail.
+
 
 ### E-commerce
 
-- [wagtailinvoices](https://github.com/LiamBrenner/wagtailinvoices) - A Wagtail module for creating invoices.
-- [longclaw](https://github.com/longclawshop/longclaw) - A shop template for Wagtail CMS.
-- [django-oscar-wagtail](https://github.com/LabD/django-oscar-wagtail) - Wagtail integration for Oscar Commerce (or Oscar Commerce integration for Wagtail?).
+
 - [django-salesman](https://github.com/dinoperovic/django-salesman) - Headless e-commerce framework for Django with Wagtail modeladmin integration.
 
 ### SEO and SMO
 
-- [wagtail-metadata](https://github.com/neon-jungle/wagtail-metadata) - A tool to assist with metadata for social media and search engines.
-- [wagtail-metadata-mixin](https://github.com/bashu/wagtail-metadata-mixin) - OpenGraph, Twitter Card and Google+ snippet tags for Wagtail CMS pages.
-- [wagtail-schema.org](https://github.com/neon-jungle/wagtail-schema.org) - Schema.org JSON-LD tags for Wagtail sites.
-- [wagtail-opengraph-image-generator](https://github.com/candylabshq/wagtail-opengraph-image-generator) - Assists you in automatically creating Open Graph images for your Wagtail pages.
-- [wagtail-redirect-importer](https://github.com/Frojd/wagtail-redirect-importer) - Your friendly neighborhood importer that lets you import redirects from different tabular data formats, such as .csv and .xls
+
 - [wagtail-meta-preview](https://github.com/Frojd/wagtail-meta-preview) - Adds ability to get share previews for Facebook, Twitter and Google in the Wagtail admin.
 - [Wagtail Yoast](https://github.com/Aleksi44/wagtailyoast) - A tool to improve readability of your texts with SEO recommendations.
 - [Wagtail SEO](https://github.com/coderedcorp/wagtail-seo) - Search engine and social media optimization for Wagtail.
 
 ### Analytics
 
-- [Wagtail Analytics](https://github.com/tomdyson/wagalytics) - A Google Analytics dashboard in your Wagtail admin.
 
 ### Customer experience
 
@@ -145,38 +124,26 @@ _You might also like [Awesome Django](https://github.com/wsvincent/awesome-djang
 
 ### Security
 
-- [wagtailenforcer](https://github.com/springload/wagtailenforcer) - If you need to enforce security protocols on your Wagtail site you've come to the right place.
-- [wagtail-yubikey](https://github.com/ahopkins/wagtail-yubikey) - Enable YubiKey two factor authentication on Wagtail admin panel.
+
 - [wagtail-2fa](https://github.com/labd/wagtail-2fa) - Add two-factor authentication to Wagtail by integrating it with django-otp.
 
 ### Media
 
 - [wagtailmedia](https://github.com/torchbox/wagtailmedia) - A Wagtail module for managing video and audio files within the admin.
-- [Wagtail Alt Generator](https://github.com/marteinn/wagtail-alt-generator) - A module for generating image description and tags based on computer vision.
-- [Wagtail FilePreviews](https://github.com/filepreviews/wagtail-filepreviews) - Extend Wagtail's Documents with image previews and metadata from FilePreviews.io.
-- [Wagtail-Textract](https://github.com/fourdigits/wagtail_textract) - Make Wagtail search Documents contents (PDF, Excel and Word, etc.).
-- [Wagtail-Lazyimages](https://github.com/ptrck/wagtail-lazyimages) - A plugin that generates tiny blurry placeholder images for lazy loading Wagtail images medium.com style.
-- [Wagtail Image Import](https://github.com/emilytoppm/wagtail-image-import) - A plugin for importing images from Google Drive.
-- [Wagtail SVG](https://github.com/Aleksi44/wagtailsvg) - A Wagtail module for managing SVG files within the admin.
-- [Wagtail Makeup](https://github.com/torchbox/wagtail-makeup) - A plugin that replaces all your images with Unsplash images.
-- [Rent Free Media](https://github.com/RentFreeMedia/rentfreemedia) - A media distribution framework built on Django and Wagtail. Premium / subscription-based publishing like Patreon or Substack.
-- [Wagtail CLIP](https://github.com/MattSegal/wagtail-clip) - A module for searching the contents of Wagtail images with natural language queries.
-- [Wagtail Stock Images](https://github.com/vicktornl/wagtail-stock-images) - Search stock images (e.g. via Unsplash) and save them to your Wagtail image library.
+
 - [Wagtail Transcription](https://github.com/j-bodek/wagtail-transcription) - Provides a field to automatically creates transcriptions from YouTube videos.
 
 ### Translations
 
 - [Wagtail Modeltranslation](https://github.com/infoportugal/wagtail-modeltranslation) - Simple app containing a mixin model that integrates [django-modeltranslation](https://github.com/deschler/django-modeltranslation) into Wagtail panels system.
-- [wagtailtrans](https://github.com/wagtail/wagtailtrans) - A Wagtail add-on for supporting multilingual sites.
+
 - [Wagtail Localize](https://github.com/wagtail/wagtail-localize) - A translation plugin for the Wagtail CMS, allows pages or snippets to be translated within Wagtail's admin interface.
 
 ### Forms
 
 - [Wagtail's built in Form Builder](https://docs.wagtail.org/en/stable/reference/contrib/forms/) for general use cases.
 - [Wagtail ReCaptcha](https://github.com/wagtail-nest/wagtail-django-recaptcha) - wagtail-django-captcha provides an easy way to integrate the [django-recaptcha](https://github.com/django-recaptcha/django-recaptcha) field when using the Wagtail formbuilder.
-- [Wagtail Simple Captcha](https://github.com/acarasimon96/wagtail-django-simple-captcha) - A self-hosted alternative to Wagtail ReCaptcha that easily integrates a [django-simple-captcha](https://github.com/mbi/django-simple-captcha) field into the Wagtail form builder.
-- [wagtailstreamforms](https://github.com/AccentDesign/wagtailstreamforms) - Build forms in Wagtail's admin for use in streamfields.
-- [wagtail-contact-reply](https://github.com/KalobTaulien/wagtail-contact-reply) - Reply directly to form submissions from the Wagtail admin
+
 - [Wagtail JotForm](https://github.com/torchbox/wagtail-jotform) - Embeddable Jotform forms for Wagtail pages.
 - [Wagtail Model Forms](https://github.com/vicktornl/wagtail-model-forms) - The Wagtail Form Builder functionalities available for your models/snippets.
 - [Wagtail Formation](https://github.com/mwesterhof/wagtail_formation) - Fully dynamic and easy to use CMS-able forms for wagtail
@@ -186,7 +153,7 @@ _You might also like [Awesome Django](https://github.com/wsvincent/awesome-djang
 - [wagtail-linkchecker](https://github.com/neon-jungle/wagtail-linkchecker) - A tool to assist with finding broken links on your Wagtail site.
 - [Wagtail Accessibility](https://github.com/wagtail-nest/wagtail-accessibility) – A plugin to assist with accessibility when developing in Wagtail.
 - [Wagtail Factories](https://github.com/wagtail/wagtail-factories) - Factory boy classes for Wagtail.
-- [Wagtail Foliage](https://github.com/harrislapiroff/wagtail-foliage) - Utilities for programmatically building page trees in Wagtail.
+
 
 ### Modeladmin
 
@@ -196,17 +163,13 @@ _You might also like [Awesome Django](https://github.com/wsvincent/awesome-djang
 
 ### Asynchronous (tasks)
 
-- [Wagtail Celery Beat](https://github.com/Nigel2392/wagtail_celery_beat) - A way to manage your Django Celery Beat tasks inside of the Wagtail admin.
 
 ### Content Management
 
 - [Wagtail Themes](https://github.com/moorinl/wagtail-themes) - Site-specific theme loader for Wagtail.
 - [Wagtail Sharing](https://github.com/cfpb/wagtail-sharing) – Easier sharing of Wagtail drafts.
 - [Wagtail Transfer](https://github.com/wagtail/wagtail-transfer) - An official extension for Wagtail allowing content to be transferred between multiple instances of a Wagtail project
-- [Wagtail Import Export](https://github.com/torchbox/wagtail-import-export) - Import/Export pages between Wagtail instances.
-- [Wagtail Import/Export Tool](https://github.com/berkalpyakici/wagtail-import-export-tool) - Refactor of [Wagtail Import Export](https://github.com/torchbox/wagtail-import-export). This tool supports importing/exporting images, documents, and snippets that are used on imported/exported pages.
-- [Wagtail Tag Manager](https://github.com/jberghoef/wagtail-tag-manager) - A Wagtail addon that allows for easier and GDPR compliant administration of scripts and tags.
-- [Wagtail Live Preview](https://github.com/KalobTaulien/wagtail-livepreview) - Live page previews beside your content.
+
 - [Wagtail Content Import](https://github.com/torchbox/wagtail-content-import) - Import content from Google Docs or Docx into StreamFields, using a customisable mapping system.
 - [Wagtail Headless Preview](https://github.com/torchbox/wagtail-headless-preview) - Previews for headless Wagtail setups
 - [Wagtail-FEdit](https://github.com/Nigel2392/wagtail_fedit) - Add frontend editing to your Wagtail site.
@@ -216,36 +179,34 @@ _You might also like [Awesome Django](https://github.com/wsvincent/awesome-djang
 - [wagtailmenus](https://github.com/jazzband/wagtailmenus) - An app to help you manage and render menus in your Wagtail projects more effectively.
 - [Wagtail Error Pages](https://gitlab.com/alexgleason/wagtailerrorpages) - Pretty, smart, customizable error pages for Wagtail.
 - [Wagtail Gridder](https://github.com/wharton/wagtailgridder) - Grid card layout similar to Google image search results, with an expanded area for card details.
-- [Wagtail Condensed Inline Panel](https://github.com/wagtail-deprecated/wagtail-condensedinlinepanel) - Drop-in replacement for Wagtail's InlinePanel suited for large number of inlines (collapsible with drag and drop support).
-- [Joyous](https://github.com/linuxsoftware/ls.joyous) - A calendar application for Wagtail.
+
 - [Wagtail App Pages](https://github.com/mwesterhof/wagtail_app_pages) - Extend Wagtail pages using an actual URL config and django views.
 - [Wagtail Cache](https://github.com/coderedcorp/wagtail-cache) - A simple page cache for Wagtail using the Django cache middleware.
-- [Wagtail GraphQL](https://github.com/tr11/wagtail-graphql) - App to automatically add GraphQL support to a Wagtail website.
+
 - [Wagtail Orderable](https://github.com/elton2048/wagtail-orderable) - Mixin support for drag-and-drop ordering in admin panel.
 - [Wagtail Resume](https://github.com/adinhodovic/wagtail-resume) – A Wagtail project made to simplify creation of resumes for developers.
 - [Wagtail Trash](https://github.com/Frojd/wagtail-trash) - Will place pages in a trash can from where they can be restored instead of being permanently deleted.
 - [Wagtail PDF View](https://github.com/donhauser/wagtail-pdf) - Render Wagtail pages and models as PDF document using Weasyprint or LaTeX.
 - [Wagtail Grapple](https://github.com/torchbox/wagtail-grapple) - A Wagtail app that makes building GraphQL endpoints a breeze.
-- [Wagtail Secret Sharing](https://github.com/vicktornl/wagtail-secret-sharing) - Keep sensitive information out of your chat logs and email via a secure sharing protocol
+
 - [Wagtail Cache Invalidator](https://github.com/vicktornl/wagtail-cache-invalidator) - Invalidate and purge (frontend) cache via an user-friendly interface in the Wagtail CMS.
-- [Wagtail Word](https://github.com/Nigel2392/wagtail_word) - A way to upload your word (.docx, .doc) documents as pages to Wagtail.
+
 
 ## Tools
 
 ### Templates & Starter Kits
 
-- [Wagtail Cookiecutter Foundation](https://github.com/chrisdev/wagtail-cookiecutter-foundation) - A Cookiecutter template for Wagtail CMS using Zurb Foundation 6.
-- [Beginner Wagtail Cookiecutter](https://github.com/hminnovation/beginner-wagtail) – A super simple implementation of Wagtail CMS.
+
 - [Wagtail Pipit](https://github.com/Frojd/Wagtail-Pipit) – Pipit is a Wagtail boilerplate which aims to provide an easy and modern developer workflow with a React-rendered frontend.
-- [Django Cookiecutter Wagtail](https://github.com/Jean-Zombie/cookiecutter-django-wagtail) – A Django Cookiecutter template with Wagtail. Based on the original 'Django Cookiecutter'. Features: Docker support using `docker-compose` for development and production (using Traefik with LetsEncrypt support), customizable PostgreSQL version, Bootstrap 4, media storage using Amazon S3 or Google Cloud Storage and many more.
-- [wagtail-webpack-dokku](https://github.com/helixsoftco/wagtail-webpack-dokku/) - A template with Wagtail, Webpack using django-webpack-loader, Bootstrap 5, production ready for Dokku.
+
+
 - [CodeRed CMS](https://github.com/coderedcorp/coderedcms) - a professionally supported WordPress alternative for building marketing websites. Create pages, blogs, forms, and every Bootstrap 4 component in the wagtail admin out-of-the-box! [Learn more](https://www.coderedcorp.com/cms/) or [watch the lightning talk](https://www.youtube.com/watch?v=U1Y-jgeGh7g&t=228s).
 - [Wordpress to Wagtail migration kit](https://github.com/wagtail/wagtail-wordpress-import) - Import WordPress blog content from an XML file into Wagtail.
 - [cookiecutter-wagtail-package](https://github.com/wagtail/cookiecutter-wagtail-package) - A cookiecutter template for building Wagtail add-on packages.
-- [Wagtail Tailwind & Stimulus blog](https://github.com/AccordBox/wagtail-tailwind-blog) - A Wagtail blog based on Tailwind CSS, Stimulus, it supports Markdown, Latex and user comments.
+
 - [Wagtail for Platform.sh](https://github.com/platformsh-templates/wagtail) - Wagtail template for Platform.sh.
 - [cookiecutter-wagtail-vix](https://github.com/engineervix/cookiecutter-wagtail-vix) - A matteries-included, reusable project skeleton to serve as a starting point for a Wagtail project.
-- [Wagtail Quickstart with docker](https://github.com/saevarom/wagtail-start-docker) - A template repository to get started quickly with the latest Wagtail in docker.
+
 - [State Design System (DSFR) starter](https://github.com/numerique-gouv/sites-conformes) - Wagtail template and starter kit from the French government.
 
 ### Templates (start command)
@@ -263,16 +224,12 @@ _You might also like [Awesome Django](https://github.com/wsvincent/awesome-djang
 
 ### Articles
 
-- [Extending The Functionality of Email Forms in Wagtail](https://lb.ee/posts/dev-wagtail-extending-the-functionality-of-email-forms-232c8469ac97/)
 - [Wagtail: 2 Steps for Adding Pages Outside of the CMS](https://www.caktusgroup.com/blog/2016/02/15/wagtail-2-steps-adding-pages-outside-cms/)
 - [Adding document previews to Wagtail CMS](https://filepreviews.io/blog/2017/04/20/adding-document-previews-to-wagtail/)
 - [Wagtail Tutorials: Build Blog Step by Step](https://saashammer.com/blog/wagtail-tutorials/) - The tutorials teach you how to create a standard blog from scratch step by step.
-- [Python CMS Framework Review: Wagtail vs Django-CMS](https://saashammer.com/blog/python-cms-framework-review-wagtail-vs-django-cms/) - Talk about the difference between Django-CMS and Wagtail, the two most popular CMS framework in Python world.
 - [Deploying Wagtail In Production](https://vix.digital/insights/deploying-wagtail-production/)
 - [Setting Up Foundation Sass With Wagtail](https://vix.digital/insights/setting-foundation-sass-wagtail/)
 - [Upgrading to Wagtail 2.0](https://wagtail.org/blog/upgrading-to-wagtail-2/) – Wagtail 2.0 is one of our biggest releases to date.
-- [Getting started with Draftail extensions](https://thib.me/getting-started-with-draftail-extensions) – Do you want to write extensions for Draftail? This is a good place to start.
-- [Amplify a Wagtail/Django site](https://parbhatpuri.com/amplify-wagtail-django-site-urls-part-1.html) - Prepare you Wagtail site for Accelerated Mobile Pages (AMP).
 - [Migrating your Drupal content to Wagtail](https://web.archive.org/web/20240929021314/https://medium.com/@kevinhowbrook/migrating-your-drupal-content-to-wagtail-d43bb34529e8) (archived)
 - [How to Add Buttons to ModelAdmin Index View](https://timonweb.com/wagtail/how-to-add-buttons-to-modeladmin-index-view-in-wagtail-cms/)
 - [How to Prevent Users from Creating Pages by Type](https://timonweb.com/wagtail/how-to-prevent-users-from-creating-certain-page-types-in-wagtail-cms/)
@@ -283,9 +240,6 @@ _You might also like [Awesome Django](https://github.com/wsvincent/awesome-djang
 
 ### Recipes
 
-- [Oscar Wagtail demo project](https://github.com/LUKKIEN/oscar-wagtail-demo) - A Django recipe for integrating Oscar E-commerce into a Wagtail CMS application.
-- [Serafeim's Wagtail FAQ](https://github.com/spapas/wagtail-faq) - Answers and recipes for Wagtail
-- [Consumer Financial Protection Bureau Wagtail development guide](https://github.com/cfpb/development/blob/main/guides/unittesting-django-wagtail.md) - Unit Testing Django and Wagtail
 
 ### Presentations
 
@@ -493,23 +447,14 @@ _You might also like [Awesome Django](https://github.com/wsvincent/awesome-djang
 - [bakerydemo](https://github.com/wagtail/bakerydemo) – Next generation Wagtail demo, born in Reykjavík.
 - [torchbox.com](https://github.com/torchbox/torchbox.com) – Wagtail build of Torchbox.com.
 - [Made with Wagtail](https://github.com/springload/madewithwagtail) - A showcase of sites and apps made with Wagtail CMS.
-- [OpenCanada.org](https://github.com/CIGIHub/opencanada) – The opencanada.org website source.
 - [Federal Election Commission](https://github.com/fecgov/fec-cms) – The content management system (CMS) for the new Federal Election Commission website.
-- [Table Tennis Wellington Business Class](https://github.com/jordij/bctt.nz) – Website for the table tennis business league in Wellington NZ.
-- [Jordi Joan’s blog](https://github.com/jordij/jordijoan.me) – Personal blog site using Wagtail CMS.
-- [Localore: Finding America](https://github.com/ghostwords/localore) – Wagtail-based CMS and Ansible playbooks for Localore: Finding America.
-- [Adventure Capitalists](https://github.com/AdventureCapitalists/website) – Wagtail powered website for the world's only investment band.
-- [NHS.UK Content Store](https://github.com/nhsuk-archive/nhsuk-content-store) – NHS.UK content store and editing app.
-- [dev.hel.fi](https://github.com/City-of-Helsinki/devheldev) – City of Helsinki development site with Wagtail.
-- [Digital Helsinki](https://github.com/City-of-Helsinki/digihel) – City of Helsinki Digital Helsinki Wagtail CMS.
-- [Secure the News](https://github.com/freedomofpress/securethenews) – An automated scanner and web dashboard for tracking TLS deployment across news organizations.
-- [RTEI](https://github.com/okfn/rtei) – Right to Education Index website (OKFN).
+
 - [BVSPCA](https://github.com/nfletton/bvspca) – Bow Valley SPCA website.
 - [Project TIER](https://github.com/ProjectTIER/projecttier.org) – Teaching Integrity in Empirical Research.
 - [SecureDrop](https://github.com/freedomofpress/securedrop.org) – Wagtail-powered website of the SecureDrop whistleblower document submission system.
 - [Consumer Financial Protection Bureau](https://github.com/cfpb/consumerfinance.gov) – The source code of the Wagtail-powered consumerfinance.gov is available here on GitHub.
 - [WesternFriend](https://github.com/WesternFriend/westernfriend.org) - community website with directory, ecommerce, and online subscription
-- [WagtailParadise](https://github.com/abrahamrome/WagtailParadise) - demo Wagtail site showing common features and recipes
+
 - [Outreachy website](https://github.com/outreachy/website/) - Website for Outreachy, who provide internships in open source and open science.
 - [Wagtail user guide](https://github.com/wagtail/guide) - A website to teach Wagtail to content editors, moderators and administrators.
 - [Penticon Public Library](https://github.com/danlerche/public-library-wagtailCMS) - An example public library website, originally created for the Penticton Public Library in Penticton, British Columbia, Canada.

+ 11 - 1
docs/CONTRIBUTING.md

@@ -2,6 +2,8 @@
 
 Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.
 
+## Guidelines for additions
+
 Please ensure your pull request adheres to the following guidelines:
 
 - Search previous suggestions before making a new one, as yours may be a duplicate.
@@ -11,7 +13,7 @@ Please ensure your pull request adheres to the following guidelines:
 - Use the following format: `[package](link) - Description.`
 - Additions should be added to the bottom of the relevant category.
 - New categories, or improvements to the existing categorization are welcome.
-- Link to the GitHub repo, not pypi.
+- Link to the code repository, not PyPI.
 - Keep descriptions short and simple, but descriptive.
 - Start the description with a capital and end with a full stop/period.
 - Check your spelling and grammar.
@@ -26,3 +28,11 @@ Thank you for your suggestions!
 The content of this list is published as an API hosted on [GitHub Pages](https://pages.github.com/): [https://wagtail.github.io/awesome-wagtail/api/v1/readme.json](https://wagtail.github.io/awesome-wagtail/api/v1/readme.json).
 
 Otherwise, directly access the Markdown README: [README.md](https://raw.githubusercontent.com/wagtail/awesome-wagtail/refs/heads/main/README.md).
+
+## Agent skills
+
+The project comes with [agent skills](https://agentskills.io/) to help with maintenance.
+
+### Link maintenance
+
+`/check-links` guides the automated archival workflow: running the script, reviewing its output, moving stale entries to the archive, and updating the README.

+ 180 - 0
docs/archive.md

@@ -0,0 +1,180 @@
+# Awesome Wagtail Archive
+
+> Projects from the main README that are no longer actively maintained — archived here for reference.
+
+## General resources
+
+## Apps
+
+### Blogging/news
+
+- [wagtailnews](https://github.com/neon-jungle/wagtailnews) - A plugin for Wagtail that provides news / blogging functionality.
+- [wagtail-blog-app](https://github.com/Tivix/wagtail-blog-app) - A blog application for the Wagtail Django CMS.
+- [Django Wagtail Feeds](https://github.com/chrisdev/django-wagtail-feeds) - Add support for RSS Feeds, Facebook Instant Articles and Apple News Publisher to your Wagtail CMS Projects.
+- [Snotra_RSS](https://github.com/olopost/snotra_rss) - Snotra_RSS is an Atom and RSS news aggregator app for Wagtail.
+- [wagtail-live](https://github.com/wagtail/wagtail-live) - Build live blogs with Wagtail.
+
+### Rich text editor extensions
+
+- [Wagtail TinyMCE](https://github.com/isotoma/wagtailtinymce) - A TinyMCE editor integration for Wagtail.
+- [wagtail-readability](https://github.com/neon-jungle/wagtail-readability) - Test how readable the content you enter into Wagtail is.
+- [wagtailembedder](https://github.com/springload/wagtailembedder) - Snippets embedder for Wagtail richtext fields.
+- [Wagtail Medium Editor](https://github.com/dperetti/Django-wagtailmedium) - A customizable Medium Editor for Wagtail, with link anchors support.
+- [WagtailDraftail](https://github.com/springload/wagtaildraftail) – Draft.js editor for Wagtail, built upon [Draftail](https://github.com/wagtail/draftail) and [draftjs_exporter](https://github.com/wagtail/draftjs_exporter).
+- [Wagtail Text Alignment](https://github.com/Nigel2392/wagtail_text_alignment) - Align text, headings and more in your Wagtail richtext editor.
+
+### Widgets
+
+- [wagtail-leaflet-widget](https://github.com/icpac-igad/wagtail-leaflet-widget) - A Leaflet JS - OSM based wagtail geo-location widget.
+- [wagtail-multi-upload](https://github.com/spapas/wagtail-multi-upload) - allows uploading of multiple related images for a page.
+
+### StreamField
+
+- [Wagtail Commonblocks](https://github.com/springload/wagtailcommonblocks) - Common StreamField blocks for Wagtail.
+- [Wagtail SVGmap](https://github.com/City-of-Helsinki/wagtail-svgmap) - ImageMap functionality for Wagtail through inline SVGs.
+- [Wagtail ClearStream](https://github.com/hminnovation/wagtailclearstream) - An app to make Wagtail's StreamField more modular.
+- [UWKM Streamfields](https://github.com/UWKM/uwkm_streamfields) – A basic set of Wagtail StreamField blocks for fun and profit.
+- [Wagtail Blocks](https://github.com/ibrahimawadhamid/wagtail_blocks) - A Collection of awesome Wagtail CMS stream-field blocks and Charts.
+- [Wagtail Cache Block](https://github.com/AccordBox/wagtail_cache_block) - A templatetag which add HTML fragment cache to your StreamField block
+- [Wagtail UIKit Block](https://github.com/kpsaurus/wagtail-uikitblocks) - A collection of UIKit components that can be used as a Wagtail StreamField block.
+
+### Static site generation
+
+- [Wagtail-Netlify](https://github.com/tomdyson/wagtail-netlify) - Easily publish your statically rendered Wagtail site to Netlify.
+- [wagtail-freezer](https://github.com/gasman/wagtail-freezer) - Generates static HTML sites from a Wagtail project.
+
+### Settings management
+
+- [Wagtail-Constance](https://github.com/MechanisM/wagtail-constance) - django-constance integration for Wagtail CMS.
+- [Wagtail-Waffle](https://github.com/TheCodingSheikh/wagtail-waffle) - Manage Django Waffle in Wagtail.
+
+### E-commerce
+
+- [django-oscar-wagtail](https://github.com/LabD/django-oscar-wagtail) - Wagtail integration for Oscar Commerce (or Oscar Commerce integration for Wagtail?).
+- [wagtailinvoices](https://github.com/LiamBrenner/wagtailinvoices) - A Wagtail module for creating invoices.
+- [longclaw](https://github.com/longclawshop/longclaw) - A shop template for Wagtail CMS.
+
+### SEO and SMO
+
+- [wagtail-redirect-importer](https://github.com/Frojd/wagtail-redirect-importer) - Your friendly neighborhood importer that lets you import redirects from different tabular data formats, such as .csv and .xls
+- [wagtail-metadata](https://github.com/neon-jungle/wagtail-metadata) - A tool to assist with metadata for social media and search engines.
+- [wagtail-metadata-mixin](https://github.com/bashu/wagtail-metadata-mixin) - OpenGraph, Twitter Card and Google+ snippet tags for Wagtail CMS pages.
+- [wagtail-opengraph-image-generator](https://github.com/candylabshq/wagtail-opengraph-image-generator) - Assists you in automatically creating Open Graph images for your Wagtail pages.
+
+### Analytics
+
+- [Wagtail Analytics](https://github.com/tomdyson/wagalytics) - A Google Analytics dashboard in your Wagtail admin.
+
+### Customer experience
+
+### Security
+
+- [wagtailenforcer](https://github.com/springload/wagtailenforcer) - If you need to enforce security protocols on your Wagtail site you've come to the right place.
+- [wagtail-yubikey](https://github.com/ahopkins/wagtail-yubikey) - Enable YubiKey two factor authentication on Wagtail admin panel.
+
+### Media
+
+- [Wagtail Alt Generator](https://github.com/marteinn/wagtail-alt-generator) - A module for generating image description and tags based on computer vision.
+- [Wagtail FilePreviews](https://github.com/filepreviews/wagtail-filepreviews) - Extend Wagtail's Documents with image previews and metadata from FilePreviews.io.
+- [Wagtail-Textract](https://github.com/fourdigits/wagtail_textract) - Make Wagtail search Documents contents (PDF, Excel and Word, etc.).
+- [Wagtail-Lazyimages](https://github.com/ptrck/wagtail-lazyimages) - A plugin that generates tiny blurry placeholder images for lazy loading Wagtail images medium.com style.
+- [Wagtail Image Import](https://github.com/emilytoppm/wagtail-image-import) - A plugin for importing images from Google Drive.
+- [Rent Free Media](https://github.com/RentFreeMedia/rentfreemedia) - A media distribution framework built on Django and Wagtail. Premium / subscription-based publishing like Patreon or Substack.
+- [Wagtail CLIP](https://github.com/MattSegal/wagtail-clip) - A module for searching the contents of Wagtail images with natural language queries.
+- [Wagtail Stock Images](https://github.com/vicktornl/wagtail-stock-images) - Search stock images (e.g. via Unsplash) and save them to your Wagtail image library.
+
+### Translations
+
+- [wagtailtrans](https://github.com/wagtail/wagtailtrans) - A Wagtail add-on for supporting multilingual sites.
+
+### Forms
+
+- [Wagtail Simple Captcha](https://github.com/acarasimon96/wagtail-django-simple-captcha) - A self-hosted alternative to Wagtail ReCaptcha that easily integrates a [django-simple-captcha](https://github.com/mbi/django-simple-captcha) field into the Wagtail form builder.
+- [wagtailstreamforms](https://github.com/AccentDesign/wagtailstreamforms) - Build forms in Wagtail's admin for use in streamfields.
+- [wagtail-contact-reply](https://github.com/KalobTaulien/wagtail-contact-reply) - Reply directly to form submissions from the Wagtail admin
+
+### Testing
+
+- [Wagtail Foliage](https://github.com/harrislapiroff/wagtail-foliage) - Utilities for programmatically building page trees in Wagtail.
+
+### Modeladmin
+
+### Asynchronous (tasks)
+
+- [Wagtail Celery Beat](https://github.com/Nigel2392/wagtail_celery_beat) - A way to manage your Django Celery Beat tasks inside of the Wagtail admin.
+
+### Content Management
+
+- [Wagtail Import Export](https://github.com/torchbox/wagtail-import-export) - Import/Export pages between Wagtail instances.
+- [Wagtail Live Preview](https://github.com/KalobTaulien/wagtail-livepreview) - Live page previews beside your content.
+- [Wagtail Import/Export Tool](https://github.com/berkalpyakici/wagtail-import-export-tool) - Refactor of [Wagtail Import Export](https://github.com/torchbox/wagtail-import-export). This tool supports importing/exporting images, documents, and snippets that are used on imported/exported pages.
+- [Wagtail Tag Manager](https://github.com/jberghoef/wagtail-tag-manager) - A Wagtail addon that allows for easier and GDPR compliant administration of scripts and tags.
+
+### Misc
+
+- [Wagtail Condensed Inline Panel](https://github.com/wagtail-deprecated/wagtail-condensedinlinepanel) - Drop-in replacement for Wagtail's InlinePanel suited for large number of inlines (collapsible with drag and drop support).
+- [Joyous](https://github.com/linuxsoftware/ls.joyous) - A calendar application for Wagtail.
+- [Wagtail GraphQL](https://github.com/tr11/wagtail-graphql) - App to automatically add GraphQL support to a Wagtail website.
+- [Wagtail Secret Sharing](https://github.com/vicktornl/wagtail-secret-sharing) - Keep sensitive information out of your chat logs and email via a secure sharing protocol
+- [Wagtail Word](https://github.com/Nigel2392/wagtail_word) - A way to upload your word (.docx, .doc) documents as pages to Wagtail.
+
+## Tools
+
+### Templates & Starter Kits
+
+- [Wagtail Cookiecutter Foundation](https://github.com/chrisdev/wagtail-cookiecutter-foundation) - A Cookiecutter template for Wagtail CMS using Zurb Foundation 6.
+- [Beginner Wagtail Cookiecutter](https://github.com/hminnovation/beginner-wagtail) – A super simple implementation of Wagtail CMS.
+- [Django Cookiecutter Wagtail](https://github.com/Jean-Zombie/cookiecutter-django-wagtail) – A Django Cookiecutter template with Wagtail. Based on the original 'Django Cookiecutter'. Features: Docker support using `docker-compose` for development and production (using Traefik with LetsEncrypt support), customizable PostgreSQL version, Bootstrap 4, media storage using Amazon S3 or Google Cloud Storage and many more.
+- [wagtail-webpack-dokku](https://github.com/helixsoftco/wagtail-webpack-dokku/) - A template with Wagtail, Webpack using django-webpack-loader, Bootstrap 5, production ready for Dokku.
+- [Wagtail Tailwind & Stimulus blog](https://github.com/AccordBox/wagtail-tailwind-blog) - A Wagtail blog based on Tailwind CSS, Stimulus, it supports Markdown, Latex and user comments.
+- [Wagtail Quickstart with docker](https://github.com/saevarom/wagtail-start-docker) - A template repository to get started quickly with the latest Wagtail in docker.
+
+### Templates (start command)
+
+## Resources
+
+### Getting started
+
+### Articles
+
+- [Extending The Functionality of Email Forms in Wagtail](https://lb.ee/posts/dev-wagtail-extending-the-functionality-of-email-forms-232c8469ac97/)
+- [Python CMS Framework Review: Wagtail vs Django-CMS](https://saashammer.com/blog/python-cms-framework-review-wagtail-vs-django-cms/) - Talk about the difference between Django-CMS and Wagtail, the two most popular CMS framework in Python world.
+- [Getting started with Draftail extensions](https://thib.me/getting-started-with-draftail-extensions) – Do you want to write extensions for Draftail? This is a good place to start.
+- [Amplify a Wagtail/Django site](https://parbhatpuri.com/amplify-wagtail-django-site-urls-part-1.html) - Prepare you Wagtail site for Accelerated Mobile Pages (AMP).
+
+### Recipes
+
+- [Oscar Wagtail demo project](https://github.com/LUKKIEN/oscar-wagtail-demo) - A Django recipe for integrating Oscar E-commerce into a Wagtail CMS application.
+- [Serafeim's Wagtail FAQ](https://github.com/spapas/wagtail-faq) - Answers and recipes for Wagtail
+- [Consumer Financial Protection Bureau Wagtail development guide](https://github.com/cfpb/development/blob/main/guides/unittesting-django-wagtail.md) - Unit Testing Django and Wagtail
+
+### Presentations
+
+### Podcasts
+
+### Videos
+
+### Books
+
+### Showcases
+
+### Lists
+
+## For editors
+
+## Community
+
+## Open-source sites
+
+- [OpenCanada.org](https://github.com/CIGIHub/opencanada) – The opencanada.org website source.
+- [NHS.UK Content Store](https://github.com/nhsuk-archive/nhsuk-content-store) – NHS.UK content store and editing app.
+- [dev.hel.fi](https://github.com/City-of-Helsinki/devheldev) – City of Helsinki development site with Wagtail.
+- [Digital Helsinki](https://github.com/City-of-Helsinki/digihel) – City of Helsinki Digital Helsinki Wagtail CMS.
+- [Secure the News](https://github.com/freedomofpress/securethenews) – An automated scanner and web dashboard for tracking TLS deployment across news organizations.
+- [Table Tennis Wellington Business Class](https://github.com/jordij/bctt.nz) – Website for the table tennis business league in Wellington NZ.
+- [Jordi Joan's blog](https://github.com/jordij/jordijoan.me) – Personal blog site using Wagtail CMS.
+- [Localore: Finding America](https://github.com/ghostwords/localore) – Wagtail-based CMS and Ansible playbooks for Localore: Finding America.
+- [Adventure Capitalists](https://github.com/AdventureCapitalists/website) – Wagtail powered website for the world's only investment band.
+- [RTEI](https://github.com/okfn/rtei) – Right to Education Index website (OKFN).
+- [WagtailParadise](https://github.com/abrahamrome/WagtailParadise) - demo Wagtail site showing common features and recipes
+