diff --git a/.github/requirements-audit.txt b/.github/requirements-audit.txt new file mode 100644 index 0000000..f5e5eea --- /dev/null +++ b/.github/requirements-audit.txt @@ -0,0 +1,2 @@ +requests>=2.31.0 +beautifulsoup4>=4.12.0 diff --git a/.github/workflows/seo-audit.yml b/.github/workflows/seo-audit.yml new file mode 100644 index 0000000..3e41718 --- /dev/null +++ b/.github/workflows/seo-audit.yml @@ -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 + 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 + + # 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 - < 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

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 \ No newline at end of file diff --git a/.lighthouserc.json b/.lighthouserc.json new file mode 100644 index 0000000..b98917d --- /dev/null +++ b/.lighthouserc.json @@ -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" + } + } +}