-
-
Notifications
You must be signed in to change notification settings - Fork 57
ci: add automated SEO & crawl audit workflow for landing page #229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e71297c
94696bd
5ba0b1d
6de9783
72ed64e
da3182f
17826cb
5cf7c69
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| requests>=2.31.0 | ||
| beautifulsoup4>=4.12.0 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| name: Landing Page Audit Pipeline | ||
|
|
||
| on: | ||
| push: | ||
| branches: [ main, develop ] | ||
| pull_request: | ||
| branches: [ main ] | ||
| workflow_dispatch: | ||
|
|
||
| jobs: | ||
| seo-and-crawl-audit: | ||
| runs-on: ubuntu-latest | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| steps: | ||
| - name: Checkout Code | ||
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: Set up Node.js | ||
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | ||
| with: | ||
| node-version: '20' | ||
|
|
||
| # Decide which URL to audit: local build for PRs, production for push/manual | ||
| - name: Set audit URL | ||
| id: set-url | ||
| run: | | ||
| if [ "${{ github.event_name }}" == "pull_request" ]; then | ||
| echo "AUDIT_URL=http://localhost:3000" >> $GITHUB_ENV | ||
| else | ||
| echo "AUDIT_URL=https://social-share-button.aossie.org/" >> $GITHUB_ENV | ||
| fi | ||
|
|
||
| # Build and serve the landing page locally so PR audits test the PR's own code | ||
| - name: Build and serve landing page (PR only) | ||
| if: github.event_name == 'pull_request' | ||
| run: | | ||
| cd landing-page | ||
| npm ci | ||
| npm run build | ||
| npx serve -s out -l 3000 & | ||
| sleep 5 | ||
|
|
||
| # 1. SCAN FOR BROKEN LINKS & ASSETS | ||
| # Docker-based actions cannot reach localhost on the runner. | ||
| # linkchecker runs directly on the runner, so it works for both | ||
| # localhost (PR builds) and the production URL (push/manual). | ||
| - name: Install Link Checker | ||
| run: pip install linkchecker | ||
|
|
||
| - name: Audit Broken Links and Images | ||
| run: linkchecker "${{ env.AUDIT_URL }}" --check-extern --no-warnings | ||
|
|
||
| # 2. RUN LIGHTHOUSE SEO + PERFORMANCE + ACCESSIBILITY AUDIT (no API key needed) | ||
| - name: Run Lighthouse Audit | ||
| uses: treosh/lighthouse-ci-action@0d716cfc9995b090bb89fe8f531fc3c133ec89ef # v11 | ||
| with: | ||
| urls: | | ||
| ${{ env.AUDIT_URL }} | ||
| configPath: ./.lighthouserc.json | ||
| uploadArtifacts: true | ||
| temporaryPublicStorage: true | ||
|
Comment on lines
+55
to
+62
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π― Functional Correctness | π‘ Minor | β‘ Quick win π§© Analysis chainπ Script executed: #!/bin/bash
set -euo pipefail
rg -n --hidden --glob '!**/.git/**' \
'treosh/lighthouse-ci-action|lighthouserc|configPath|assertions|budgetPath' .Repository: AOSSIE-Org/SocialShareButton Length of output: 249 π Web query:
π‘ Result: To configure treosh/lighthouse-ci-action v11 (or later) to perform CI assertions and manage artifacts, you should use the configPath and uploadArtifacts inputs in your workflow [1][2][3]. When you define assertions in a lighthouserc file (referenced by configPath), the action will automatically evaluate these assertions, and the CI workflow will fail if any assertion set to "error" is not met [1][4][5]. Key configuration details: 1. configPath: Use this input to specify the path to your lighthouserc file (e.g.,./lighthouserc.json) [4][5]. This file provides full control over LHCI assertions [4]. Example lighthouserc.json: { "ci": { "assert": { "assertions": { "categories:performance": ["error", { "minScore": 0.6 }] } } } } [4] 2. uploadArtifacts: Set this to true to save the Lighthouse reports as GitHub Actions artifacts [1][4]. This is helpful for debugging or persistent storage of results [4]. Example workflow step: - name: Audit URLs uses: treosh/lighthouse-ci-action@v11 with: urls: 'https://example.com/' configPath: './lighthouserc.json' uploadArtifacts: true [4][5] Troubleshooting workflow failures: If your workflow fails unexpectedly, check if you are using includePassedAssertions: true in your lighthouserc. In some older versions of this action, the presence of passed assertions in the results could cause the action to incorrectly report a failure [6]. Additionally, ensure that your assertions are correctly categorized as "error" if you intend for them to break the build; "warn" will typically report the issue without failing the workflow [4]. Citations:
Fail CI on Lighthouse score thresholds. This step uploads artifacts but does not provide π§° Toolsπͺ zizmor (1.29.0)[error] 25-25: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy) (unpinned-uses) π€ Prompt for AI Agents |
||
|
|
||
| # 3. ADVANCED VERIFICATIONS (Canonical, Schema, Robots.txt) | ||
| - name: Set up Python for Advanced Custom Scans | ||
| uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 | ||
| with: | ||
| python-version: '3.10' | ||
|
|
||
| - name: Install Advanced Audit Dependencies | ||
| run: | | ||
| python -m pip install --upgrade pip | ||
| python -m pip install -r .github/requirements-audit.txt | ||
|
|
||
| - name: Run Advanced Structural Validation | ||
| env: | ||
| TARGET_URL: ${{ env.AUDIT_URL }} | ||
| run: | | ||
| python - <<EOF | ||
| import os | ||
| import json | ||
| import requests | ||
| from bs4 import BeautifulSoup | ||
|
|
||
| url = os.environ["TARGET_URL"] | ||
| response = requests.get(url, headers={"User-Agent": "Googlebot"}, timeout=(10, 30)) | ||
|
|
||
| # Test 1: Server Status and Speed | ||
| print(f"HTTP Status: {response.status_code}") | ||
| assert response.status_code == 200, "Landing page is down!" | ||
| ttfb = response.elapsed.total_seconds() | ||
| ttfb_budget = 2.5 # TTFB must be under 2.5 seconds | ||
| print(f"Time to First Byte (TTFB): {ttfb}s (Budget: {ttfb_budget}s)") | ||
| assert ttfb <= ttfb_budget, f"Performance Error: TTFB of {ttfb}s exceeds budget of {ttfb_budget}s" | ||
|
|
||
| soup = BeautifulSoup(response.text, 'html.parser') | ||
|
|
||
| # Test 2: Canonical Tag Check | ||
| canonicals = soup.find_all('link', rel='canonical') | ||
| assert len(canonicals) > 0, "CRITICAL: Missing rel='canonical' tag!" | ||
| assert len(canonicals) == 1, f"CRITICAL: Duplicate canonical tags found ({len(canonicals)}). Expected exactly 1!" | ||
|
|
||
| canonical_href = canonicals[0].get('href', '').strip() | ||
| print(f"Canonical URL found: {canonical_href if canonical_href else 'Empty'}") | ||
|
|
||
| assert canonical_href, "CRITICAL: The rel='canonical' tag has an empty or missing 'href' attribute!" | ||
|
|
||
| normalized_href = canonical_href.rstrip('/') | ||
| normalized_url = "https://social-share-button.aossie.org" | ||
| assert normalized_href == normalized_url, f"CRITICAL: Canonical URL '{canonical_href}' does not match the expected base URL '{normalized_url}/'!" | ||
|
|
||
| # Test 3: Heading Hierarchy Check | ||
| h1s = soup.find_all('h1') | ||
| print(f"Found {len(h1s)} H1 tags.") | ||
| assert len(h1s) == 1, "SEO Error: Landing page must have exactly ONE <h1> tag." | ||
|
|
||
| # Test 4: Schema Markup Verification (no extruct β uses built-in json) | ||
| script_tags = soup.find_all('script', type='application/ld+json') | ||
| json_ld = [] | ||
| for index, tag in enumerate(script_tags, start=1): | ||
| raw = tag.get_text(strip=True) | ||
| assert raw, f"CRITICAL: JSON-LD block {index} is empty." | ||
| try: | ||
| data = json.loads(raw) | ||
| json_ld.append(data) | ||
| except json.JSONDecodeError as error: | ||
| raise AssertionError( | ||
| f"CRITICAL: JSON-LD block {index} is invalid: {error}" | ||
| ) from error | ||
|
|
||
| print(f"Structured Data: Found {len(json_ld)} JSON-LD blocks.") | ||
| assert len(json_ld) > 0, "Warning: No Structured Schema found on page." | ||
|
|
||
| print("β All advanced landing page validations passed successfully!") | ||
| EOF | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| { | ||
| "ci": { | ||
| "assert": { | ||
| "assertions": { | ||
| "categories:performance": ["error", { "minScore": 0.8 }], | ||
| "categories:accessibility": ["error", { "minScore": 0.9 }], | ||
| "categories:seo": ["error", { "minScore": 0.9 }], | ||
| "categories:best-practices": ["error", { "minScore": 0.9 }] | ||
| } | ||
| }, | ||
| "upload": { | ||
| "target": "temporary-public-storage" | ||
| } | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.