From 992aefcfa76c1d08e8814e55709f3410a6882cc7 Mon Sep 17 00:00:00 2001 From: John Smith Date: Wed, 24 Jun 2026 10:12:02 +0100 Subject: [PATCH 1/4] feat(seo): add OG/Twitter tags, sitemap, and Dataset enhancements - Add canonical link and sitemap link tags to - Add Open Graph meta tags (type, site_name, title, description, url) - Add Twitter Card meta tags (summary card, @solrevdev) - Add sitemap.xml covering index and agent-access pages (daily/weekly) - Add sameAs, numberOfItems, variableMeasured to Dataset JSON-LD - CI now stamps numberOfItems from jq package count and sitemap lastmod Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/github_workflows_build.yml | 13 +++++++++++-- index.html | 17 +++++++++++++++++ sitemap.xml | 15 +++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 sitemap.xml diff --git a/.github/workflows/github_workflows_build.yml b/.github/workflows/github_workflows_build.yml index e3f753a..b279146 100644 --- a/.github/workflows/github_workflows_build.yml +++ b/.github/workflows/github_workflows_build.yml @@ -89,11 +89,20 @@ jobs: # Copy files (overwrite if exists) cp index.html deploy/ - # Stamp dateModified with today's date so Dataset structured data stays current - sed -i "s/\"dateModified\": \"[^\"]*\"/\"dateModified\": \"$(date -u +%Y-%m-%d)\"/" deploy/index.html cp agent-access.html deploy/ cp llms.txt deploy/ cp packages.json deploy/ + cp sitemap.xml deploy/ + + # Stamp dateModified with today's date so Dataset structured data stays current + sed -i "s/\"dateModified\": \"[^\"]*\"/\"dateModified\": \"$(date -u +%Y-%m-%d)\"/" deploy/index.html + + # Stamp numberOfItems with the actual package count from packages.json + PACKAGE_COUNT=$(jq '.metadata.total // length' packages.json) + sed -i "s/\"numberOfItems\": [0-9]*/\"numberOfItems\": ${PACKAGE_COUNT}/" deploy/index.html + + # Stamp sitemap lastmod with today's date + sed -i "s/[^<]*<\/lastmod>/$(date -u +%Y-%m-%d)<\/lastmod>/g" deploy/sitemap.xml # Create .nojekyll to prevent Jekyll processing touch deploy/.nojekyll diff --git a/index.html b/index.html index d5c5fe6..c30f6ec 100644 --- a/index.html +++ b/index.html @@ -5,8 +5,22 @@ Winget Package Search + + + + + + + + + + + + + + )' + matches = list(re.finditer(pattern, document, re.DOTALL)) + if len(matches) != 1: + raise ValueError("expected one Dataset JSON-LD block") + dataset = json.loads(matches[0].group(2)) + if dataset.get("@type") != "Dataset": + raise ValueError("expected Dataset structured data") + for key in ("numberOfItems", "temporalCoverage", "datePublished", "sameAs"): + dataset.pop(key, None) + dataset.update({ + "description": f"Metadata for {total:,} Windows Package Manager packages extracted from microsoft/winget-pkgs.", + "url": public_base_url, + "dateModified": extracted_at.isoformat().replace("+00:00", "Z"), + "variableMeasured": ["id", "name", "version", "publisher"], + "distribution": {"@type": "DataDownload", "encodingFormat": "application/json", + "contentUrl": public_base_url + "packages.json"}, + }) + encoded = json.dumps(dataset, ensure_ascii=False, indent=2).replace("<", "\\u003c") + document = re.sub(pattern, lambda m: m.group(1) + "\n" + encoded + "\n " + m.group(3), document, flags=re.DOTALL) + summary = (dataset["description"] + " " + f'Catalog updated ' + ".") + document, count = re.subn(r'(]*\bid="catalog-summary"[^>]*>).*?(

)', + lambda m: m.group(1) + summary + m.group(2), document, flags=re.DOTALL) + if count != 1: + raise ValueError("expected one catalog-summary paragraph") + return document + + +def build_sitemap(template, public_base_url, home_modified, agent_modified): + root = ET.fromstring(template) + ns = {"s": SITEMAP_NS} + entries = root.findall("s:url", ns) + locations = {"{{PUBLIC_BASE_URL}}": (public_base_url, home_modified), + "{{AGENT_URL}}": (public_base_url + "agent-access.html", agent_modified)} + if len(entries) != len(locations): + raise ValueError("sitemap template must contain the homepage and agent page") + for entry in entries: + loc = entry.find("s:loc", ns) + if loc is None or loc.text not in locations: + raise ValueError("sitemap template contains an unknown location") + url, modified = locations.pop(loc.text) + loc.text = url + for child in list(entry): + if child is not loc: + entry.remove(child) + if modified: + ET.SubElement(entry, f"{{{SITEMAP_NS}}}lastmod").text = modified.date().isoformat() + ET.register_namespace("", SITEMAP_NS) + ET.indent(root, space=" ") + return ET.tostring(root, encoding="unicode", xml_declaration=True) + "\n" + + +def build_site(source_dir, output_dir, packages_path=None, public_base_url=None): + source_dir = Path(source_dir).resolve() + output_dir = Path(output_dir).resolve() + packages_path = Path(packages_path).resolve() if packages_path else source_dir / "packages.json" + if output_dir == source_dir or output_dir in source_dir.parents: + raise ValueError("output directory must not contain the source directory") + if public_base_url is None: + config = json.loads((source_dir / "site_config.json").read_text(encoding="utf-8")) + public_base_url = config["public_base_url"] + public_base_url = normalize_public_base_url(public_base_url) + total, extracted_at = read_catalog(packages_path) + index_source = (source_dir / "index.html").read_text(encoding="utf-8") + canonical = re.search(r']*\brel="canonical"[^>]*\bhref="([^"]+)"', index_source) + if canonical is None: + raise ValueError("expected a source canonical URL") + source_base_url = normalize_public_base_url(unescape(canonical.group(1))) + index = build_index(index_source, public_base_url, total, extracted_at) + agent = (source_dir / "agent-access.html").read_text(encoding="utf-8").replace( + escape(source_base_url, quote=True), escape(public_base_url, quote=True)) + agent = replace_attribute(agent, "link", "rel", "canonical", "href", public_base_url + "agent-access.html") + guide = (source_dir / "llms.txt").read_text(encoding="utf-8").replace(source_base_url, public_base_url) + shared = ["build_site.py", "site_config.json"] + home_modified = git_modified(source_dir, ["index.html", "search.js", "sitemap.xml", *shared]) + home_modified = max(extracted_at, home_modified) if home_modified else extracted_at + agent_modified = git_modified(source_dir, ["agent-access.html", *shared]) + sitemap = build_sitemap((source_dir / "sitemap.xml").read_text(encoding="utf-8"), + public_base_url, home_modified, agent_modified) + for asset in ASSETS: + if not (source_dir / asset).is_file(): + raise ValueError(f"missing site asset: {asset}") + if not (source_dir / "vendor").is_dir(): + raise ValueError("missing vendor directory") + output_dir.mkdir(parents=True, exist_ok=True) + for asset in ASSETS: + shutil.copy2(source_dir / asset, output_dir / asset) + shutil.copytree(source_dir / "vendor", output_dir / "vendor", dirs_exist_ok=True) + shutil.copy2(packages_path, output_dir / "packages.json") + (output_dir / "index.html").write_text(index, encoding="utf-8") + (output_dir / "agent-access.html").write_text(agent, encoding="utf-8") + (output_dir / "llms.txt").write_text(guide, encoding="utf-8") + (output_dir / "sitemap.xml").write_text(sitemap, encoding="utf-8") + (output_dir / ".nojekyll").touch() + destination = escape(public_base_url, quote=True) + (output_dir / "404.html").write_text( + '\n' + '' + f'' + 'Page not found | Winget Package Search' + f'

Page not found

' + 'Search WinGet packages

\n', encoding="utf-8") + return {"public_base_url": public_base_url, "total": total, + "extracted_at": extracted_at.isoformat().replace("+00:00", "Z")} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source-dir", type=Path, default=Path(__file__).resolve().parent) + parser.add_argument("--packages", type=Path) + parser.add_argument("--output-dir", type=Path) + parser.add_argument("--public-base-url") + args = parser.parse_args() + try: + result = build_site(args.source_dir, args.output_dir or args.source_dir / "deploy", + args.packages, args.public_base_url) + except (ValueError, KeyError, OSError) as exc: + parser.exit(1, f"Site build failed: {exc}\n") + print(json.dumps(result)) + + +if __name__ == "__main__": + main() diff --git a/index.html b/index.html index ff04168..a94e2d8 100644 --- a/index.html +++ b/index.html @@ -39,18 +39,13 @@ "isBasedOn": "https://github.com/microsoft/winget-pkgs", "keywords": ["winget", "Windows Package Manager", "package search", "software catalog", "Windows software", "package manager"], "inLanguage": "en", - "temporalCoverage": "2025-05-29/..", - "datePublished": "2025-05-29", - "dateModified": "2025-05-29", "creator": { "@type": "Person", "name": "John Smith", - "url": "https://solrevdev.com/about/" + "url": "https://github.com/solrevdev" }, "license": "https://spdx.org/licenses/MIT.html", - "sameAs": "https://github.com/solrevdev/winget-search", - "numberOfItems": 0, - "variableMeasured": ["packageId", "name", "version", "publisher"], + "variableMeasured": ["id", "name", "version", "publisher"], "distribution": { "@type": "DataDownload", "encodingFormat": "application/json", @@ -63,10 +58,10 @@ --bg-primary: #ffffff; --bg-secondary: #f8f9fa; --text-primary: #212529; - --text-secondary: #6c757d; + --text-secondary: #59636d; --border-color: #dee2e6; --accent-color: #0066cc; - --success-color: #28a745; + --success-color: #1f7a35; --hover-bg: #f0f0f0; } @@ -97,6 +92,22 @@ line-height: 1.6; } + .skip-link { + position: absolute; + top: -100px; + left: 20px; + z-index: 1; + padding: 8px 12px; + border: 2px solid var(--accent-color); + border-radius: 6px; + background: var(--bg-primary); + color: var(--accent-color); + } + + .skip-link:focus { + top: 8px; + } + h1 { margin-bottom: 10px; display: flex; @@ -107,6 +118,11 @@ .subtitle { color: var(--text-secondary); font-size: 0.9em; + margin: 0; + } + + #catalog-summary { + margin-top: 4px; margin-bottom: 20px; } @@ -332,6 +348,7 @@ } .pkg-name { + margin: 0; font-weight: 600; font-size: 1.1em; color: var(--text-primary); @@ -490,13 +507,13 @@ .copy-btn:hover { background-color: var(--accent-color); - color: white; + color: var(--bg-primary); border-color: var(--accent-color); } .copy-btn.success { background-color: var(--success-color); - color: white; + color: var(--bg-primary); border-color: var(--success-color); } @@ -508,8 +525,8 @@ border: 1px solid var(--border-color); flex: 1; min-width: 0; - overflow-x: auto; - white-space: nowrap; + white-space: pre-wrap; + overflow-wrap: anywhere; font-size: 0.9em; } @@ -530,13 +547,10 @@ .footer a { color: var(--accent-color); - text-decoration: none; - } - - .footer a:hover { text-decoration: underline; } + .keyboard-hint { font-size: 0.8em; color: var(--text-secondary); @@ -592,40 +606,46 @@ -

- - - - Winget Package Search -

-
Search and install Windows Package Manager packages
- -
- - - - - -
- -
Press / to focus search • Esc to clear
- -
-
-
- -
Loading packages...
- -
- + +
+

+ + Winget Package Search +

+

Search and install Windows Package Manager packages

+

Current package metadata extracted from microsoft/winget-pkgs.

+
+ +
+
+ + +
+ +
Press / to focus search • Esc to clear
+ +
+
+
+ +
Loading packages...
+ +
+
+