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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ jobs:
run: terraform -chdir=projects/api_platform/infra/terraform init -backend=false -input=false
- name: Terraform validate
run: terraform -chdir=projects/api_platform/infra/terraform validate
- name: Verify SEO Metadata & Artifacts
run: python3 scripts/test_seo.py
security:
runs-on: ubuntu-latest
steps:
Expand Down
78 changes: 74 additions & 4 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,41 @@
<meta name="theme-color" content="#0b1020">
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
<meta name="description" content="DevOps Learn by Doing is an AI-assisted CLI for learning DevOps through a security-gated Azure deployment lifecycle and a production-style Python API project.">
<link rel="canonical" href="https://howlcipher.github.io/DevOps-Learn-by-Doing/">

<!-- Open Graph Metadata -->
<meta property="og:type" content="website">
<meta property="og:site_name" content="DevOps Learn by Doing">
<meta property="og:title" content="DevOps Learn by Doing | Security-Gated Azure DevOps &amp; Platform Engineering CLI">
<meta property="og:description" content="AI-assisted CLI platform for mastering DevOps through real containerization, security scanning, Terraform validation, and Azure deployment workflows.">
<meta property="og:url" content="https://howlcipher.github.io/DevOps-Learn-by-Doing/">
<meta property="og:image" content="https://howlcipher.github.io/DevOps-Learn-by-Doing/assets/social-preview.png">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="https://howlcipher.github.io/DevOps-Learn-by-Doing/assets/social-preview.png">
<title>DevOps Learn by Doing</title>

<!-- Twitter Card Metadata -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="DevOps Learn by Doing | Security-Gated Azure DevOps &amp; Platform Engineering CLI">
<meta name="twitter:description" content="AI-assisted CLI platform for mastering DevOps through real containerization, security scanning, Terraform validation, and Azure deployment workflows.">
<meta name="twitter:image" content="https://howlcipher.github.io/DevOps-Learn-by-Doing/assets/social-preview.png">

<!-- JSON-LD Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "DevOps Learn by Doing",
"applicationCategory": "EducationalApplication",
"operatingSystem": "Linux, macOS, Windows",
"description": "AI-assisted CLI learning and platform engineering platform built around real containerization, Terraform validation, security gating, and Azure deployment workflows.",
"url": "https://howlcipher.github.io/DevOps-Learn-by-Doing/",
"author": {
"@type": "Person",
"name": "William Elias",
"url": "https://howlcipher.github.io/william_elias/"
}
}
</script>

<title>DevOps Learn by Doing | Security-Gated Azure DevOps &amp; Platform Engineering CLI</title>
<style>
:root {
--ink: #0b1020;
Expand Down Expand Up @@ -470,6 +501,7 @@
</a>
<nav class="nav-links" aria-label="Primary navigation">
<a href="#model">Learning model</a>
<a href="#devops-qa">Platform Q&amp;A</a>
<a href="#status">Status</a>
<a href="#roadmap">Roadmap</a>
<a class="repo-link" href="https://github.com/howlcipher/DevOps-Learn-by-Doing">GitHub ↗</a>
Expand All @@ -483,7 +515,7 @@
<div class="eyebrow">AI-Assisted DevOps Mastery // V1 Skeleton</div>
<h1>Learn DevOps by doing the work.</h1>
<p>
A CLI learning and engineering platform built around a real production-style Python API project.
Created by <a href="https://howlcipher.github.io/william_elias/" style="color: var(--cyan); text-decoration: underline;">William Elias</a>, DevOps Learn by Doing is a CLI learning and engineering platform built around a real production-style Python API project.
The AI <strong>works with you, explains what matters, and never pretends to know</strong>
what it has not actually done. Progress means you can direct, review, troubleshoot,
and improve AI-assisted infrastructure—not type every command from memory.
Expand Down Expand Up @@ -674,6 +706,42 @@ <h3>Azure lifecycle acceptance run</h3>
</div>
</section>

<section id="devops-qa">
<div class="shell">
<div class="section-head">
<div class="label">05 // Technical Q&amp;A</div>
<h2>AI-assisted platform engineering &amp; safe automation.</h2>
<p>Key architectural questions on operating infrastructure, security gates, and autonomy boundaries alongside AI agents.</p>
</div>
<div class="grid" style="grid-template-columns: repeat(2, 1fr);">
<article class="card">
<h3 style="color: var(--cyan);">How does AI-assisted DevOps training bridge the gap to production engineering?</h3>
<p>
Traditional courses rely on contrived browser sandboxes that abstract away real tooling friction. DevOps Learn by Doing grounds learners in a real production-grade Python API codebase, real Docker build contexts, and real Terraform manifests. Instead of memorizing commands, engineers learn how to direct, verify, troubleshoot, and safely govern AI-assisted infrastructure operations.
</p>
</article>
<article class="card">
<h3 style="color: var(--amber);">Why use security gates and sandbox modes for infrastructure automation?</h3>
<p>
Unconstrained agent execution against cloud providers risks runaway resource provisioning, accidental data exposure, or credential leakage. DevOps Learn by Doing enforces security-gated verification using Trivy vulnerability scanning, Conftest policy-as-code assertions, and read-only dry-run simulation modes before any state-mutating deployment can execute.
</p>
</article>
<article class="card">
<h3 style="color: var(--green);">How does the four-level autonomy model prevent dangerous AI mutations?</h3>
<p>
By categorizing operational tasks into Observe, Guided, Collaborative, and Autonomous tiers, the system ensures that irreversible actions (such as Terraform applies, database migrations, or remote cloud destruction) require cryptographic human sign-off. AI agents assist with drafting and analysis, while humans retain authority over production state.
</p>
</article>
<article class="card">
<h3 style="color: var(--blue);">What core technical competencies are required for platform engineers working with AI?</h3>
<p>
As generative models increasingly write deployment scripts and configuration files, the vital platform engineering skill shifts from syntax memorization to rigorous validation: evaluating diff blast radius, writing declarative policies, inspecting container isolation, interpreting structured audit logs, and defining deterministic rollback protocols.
</p>
</article>
</div>
</div>
</section>

<section class="quickstart">
<div class="shell">
<div class="window">
Expand Down Expand Up @@ -709,6 +777,8 @@ <h3>Azure lifecycle acceptance run</h3>
<div class="shell footer-row">
<span>DEVOPS LEARN BY DOING // AI-ASSISTED MASTERY V1</span>
<span>
<a href="https://howlcipher.github.io/william_elias/">William Elias Portfolio</a>
&nbsp;·&nbsp;
<a href="https://github.com/howlcipher/DevOps-Learn-by-Doing">Repository</a>
&nbsp;·&nbsp;
<a href="https://github.com/howlcipher/DevOps-Learn-by-Doing/tree/main/docs">Documentation</a>
Expand Down
4 changes: 4 additions & 0 deletions docs/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
User-agent: *
Allow: /

Sitemap: https://howlcipher.github.io/DevOps-Learn-by-Doing/sitemap.xml
9 changes: 9 additions & 0 deletions docs/sitemap.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://howlcipher.github.io/DevOps-Learn-by-Doing/</loc>
<lastmod>2026-09-07</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
</urlset>
135 changes: 135 additions & 0 deletions scripts/test_seo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
"""
SEO & Discoverability Validation Suite for DevOps Learn by Doing.
Validates metadata, canonical tags, sitemap, robots.txt, and author entity linking.
"""

import os
import re
import sys
import json
import xml.etree.ElementTree as ET

REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DOCS_DIR = os.path.join(REPO_ROOT, "docs")
INDEX_HTML = os.path.join(DOCS_DIR, "index.html")
ROBOTS_TXT = os.path.join(DOCS_DIR, "robots.txt")
SITEMAP_XML = os.path.join(DOCS_DIR, "sitemap.xml")

def test_seo():
print("=== Running DevOps-Learn-by-Doing SEO Verification ===")

# 1. Check docs/index.html
assert os.path.exists(INDEX_HTML), f"Missing {INDEX_HTML}"
with open(INDEX_HTML, "r", encoding="utf-8") as f:
html = f.read()

# Title check
title_match = re.search(r"<title>(.*?)</title>", html, re.IGNORECASE | re.DOTALL)
assert title_match, "Missing <title> tag in index.html"
title = title_match.group(1).strip()
assert "DevOps Learn by Doing" in title, f"Title does not mention DevOps Learn by Doing: {title}"
print(f" [PASS] HTML <title> exists: {title}")

# Meta description check
meta_desc_match = re.search(
r'<meta\s+name=["\']description["\']\s+content=["\'](.*?)["\']',
html,
re.IGNORECASE,
)
assert meta_desc_match, "Missing meta description in index.html"
meta_desc = meta_desc_match.group(1).strip()
assert len(meta_desc) > 30, "Meta description too short"
assert len(meta_desc) <= 170, f"Meta description exceeds 170 chars ({len(meta_desc)}): {meta_desc}"
print(f" [PASS] meta description exists: {meta_desc[:60]}... ({len(meta_desc)} chars)")

# Canonical URL check
canonical_match = re.search(
r'<link\s+rel=["\']canonical["\']\s+href=["\'](.*?)["\']',
html,
re.IGNORECASE,
)
assert canonical_match, "Missing link rel=canonical in index.html"
canonical_url = canonical_match.group(1).strip()
expected_canonical = "https://howlcipher.github.io/DevOps-Learn-by-Doing/"
assert canonical_url == expected_canonical, (
f"Canonical URL mismatch: expected {expected_canonical}, got {canonical_url}"
)
print(f" [PASS] Canonical URL matches: {canonical_url}")

# Heading hierarchy check
h1_matches = re.findall(r"<h1[^>]*>(.*?)</h1>", html, re.IGNORECASE | re.DOTALL)
assert len(h1_matches) == 1, f"Expected exactly 1 <h1> tag, found {len(h1_matches)}"
clean_h1 = re.sub(r"<[^>]+>", "", h1_matches[0]).strip()
print(f" [PASS] Exactly 1 H1 present: {clean_h1}")

# Open Graph & Twitter Cards
assert 'property="og:title"' in html or "property='og:title'" in html, "Missing og:title"
assert 'property="og:description"' in html or "property='og:description'" in html, "Missing og:description"
assert 'property="og:url"' in html or "property='og:url'" in html, "Missing og:url"
assert 'name="twitter:card"' in html or "name='twitter:card'" in html, "Missing twitter:card"
print(" [PASS] Open Graph and Twitter card tags verified")

# JSON-LD Schema.org Structured Data
json_ld_matches = re.findall(
r'<script\s+type=["\']application/ld\+json["\']\s*>(.*?)</script>',
html,
re.IGNORECASE | re.DOTALL,
)
assert len(json_ld_matches) >= 1, "Missing JSON-LD structured data block"
ld = json.loads(json_ld_matches[0].strip())
assert ld.get("@context") in ("https://schema.org", "http://schema.org"), "Invalid @context"
ld_type = ld.get("@type")
assert ld_type == "SoftwareApplication", (
f"Expected SoftwareApplication, got {ld_type}"
)
author = ld.get("author", {})
expected_author = "William" + " " + "Elias"
assert author.get("name") == expected_author, (
f"Author name must be {expected_author}"
)
author_url = author.get("url")
expected_author_url = "https://howlcipher.github.io/william_elias/"
assert author_url == expected_author_url, (
"Author URL must point to william_elias portfolio"
)
print(f" [PASS] JSON-LD valid and correctly attributes {expected_author}")

# Internal entity links
william_url = "https://howlcipher.github.io/william_elias/"
assert william_url in html, f"Missing link to William Elias portfolio ({william_url})"
print(" [PASS] Internal entity link to William Elias verified")

# 2. Check docs/robots.txt
assert os.path.exists(ROBOTS_TXT), f"Missing {ROBOTS_TXT}"
with open(ROBOTS_TXT, "r", encoding="utf-8") as f:
robots = f.read()
assert "User-agent: *" in robots, "robots.txt missing User-agent: *"
assert "Allow: /" in robots, "robots.txt missing Allow: /"
assert "Sitemap: https://howlcipher.github.io/DevOps-Learn-by-Doing/sitemap.xml" in robots, (
"robots.txt missing correct Sitemap directive"
)
print(" [PASS] docs/robots.txt valid and references sitemap")

# 3. Check docs/sitemap.xml
assert os.path.exists(SITEMAP_XML), f"Missing {SITEMAP_XML}"
tree = ET.parse(SITEMAP_XML)
root = tree.getroot()
namespace = {"sm": "http://www.sitemaps.org/schemas/sitemap/0.9"}
locs = [elem.text.strip() for elem in root.findall(".//sm:loc", namespace)]
assert expected_canonical in locs, (
f"sitemap.xml does not contain canonical URL {expected_canonical}"
)
print(f" [PASS] docs/sitemap.xml valid and contains canonical URL")

print("\nAll SEO validations PASSED successfully!\n")

if __name__ == "__main__":
try:
test_seo()
except AssertionError as e:
print(f"FAIL: {e}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"ERROR: {e}", file=sys.stderr)
sys.exit(1)
Loading