Find avoidable problems before release

Technical SEO is a consistency problem rather than a collection of unrelated tags. A page's URL, readable content, language version and indexing directives need to agree. Useful content can still be hidden by an accidental noindex directive or point to an unsuitable canonical URL. This tutorial builds a small local audit that catches several avoidable release errors without promising a search position, a particular visit count or automatic indexing.

A canonical URL signals a preferred version of similar content; it is not a guaranteed command to a search engine. In a bilingual site, each translated page needs an appropriate canonical and correct language counterparts. Do not indiscriminately canonicalize every English translation to its Persian page. Stable paths, translated content and reciprocal language relationships should present a coherent structure to both visitors and crawlers rather than relying on a tag to compensate for contradictory pages.

Keep canonical and language URLs consistent

The example checks for one main heading, one nonempty description, an absolute canonical and three language alternates. It also reports a noindex directive so that a test-environment setting is not accidentally carried into a public release. This is the demonstration's editorial contract, not a claim that heading count alone determines rankings. Interpret findings in the context of the actual document structure, page purpose and the site's broader indexing configuration.

Save the four files and run python -m unittest test_audit.py, followed by python run_audit.py demo.html. The supplied fixture should return an empty error list. Its example.test URLs are test data, not an assertion that those pages have been published. For your own project, inspect the actual generated HTML and compare canonical destinations with the intended final domain and paths rather than blindly copying the demonstration URLs into production metadata.

The auditor performs no network requests. It therefore cannot establish server responses, redirect chains, status codes or whether a language counterpart is reachable on the deployed server. Crawler access and sitemap configuration require separate checks. After an authorized release, inspect the final URLs and appropriate search-management tools. A clean local result does not prove that a search engine will index the page or choose exactly the canonical URL that the author supplied.

Complete executable files

The four files are complete. The article provides prerequisites, command order and verification; code is neither shortened nor hidden.

1. Complete audit_seo.py

Save this entire file beside the others using the exact filename.

from html.parser import HTMLParser
from urllib.parse import urlparse


class Audit(HTMLParser):
    def __init__(self):
        super().__init__()
        self.h1 = 0; self.canonicals = []; self.descriptions = []; self.languages = set(); self.robots = []

    def handle_starttag(self, tag, attrs):
        attrs = dict(attrs)
        if tag == 'h1': self.h1 += 1
        if tag == 'link' and attrs.get('rel') == 'canonical': self.canonicals.append(attrs.get('href', ''))
        if tag == 'link' and attrs.get('rel') == 'alternate' and attrs.get('href'): self.languages.add(attrs.get('hreflang'))
        if tag == 'meta' and attrs.get('name') == 'description': self.descriptions.append(attrs.get('content', ''))
        if tag == 'meta' and attrs.get('name') == 'robots': self.robots.append(attrs.get('content', '').lower())


def audit(source):
    parser = Audit(); parser.feed(source)
    errors = []
    if parser.h1 != 1: errors.append('expected-one-h1')
    if len(parser.descriptions) != 1 or not parser.descriptions[0].strip(): errors.append('missing-or-duplicate-description')
    if len(parser.canonicals) != 1: errors.append('expected-one-canonical')
    elif urlparse(parser.canonicals[0]).scheme != 'https' or not urlparse(parser.canonicals[0]).netloc: errors.append('canonical-must-be-absolute-https')
    if not {'fa', 'en', 'x-default'} <= parser.languages: errors.append('missing-language-alternates')
    if any('noindex' in value for value in parser.robots): errors.append('page-is-noindex')
    return errors

The execution section explains this file’s command, expected outcome and limits.

2. Complete demo.html

Save this entire file beside the others using the exact filename.

<!doctype html>
<html lang="fa" dir="rtl"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>نمونهٔ ممیزی سئوی فنی</title>
<meta name="description" content="صفحهٔ آزمایشی برای بررسی سئوی فنی؛ نشانی‌ها فقط مثال هستند.">
<meta name="robots" content="index,follow">
<link rel="canonical" href="https://example.test/article.html">
<link rel="alternate" hreflang="fa" href="https://example.test/article.html">
<link rel="alternate" hreflang="en" href="https://example.test/article-en.html">
<link rel="alternate" hreflang="x-default" href="https://example.test/article.html">
</head><body><main><h1>یک عنوان اصلی روشن</h1><p>این صفحه روی دامنهٔ نمونه منتشر نشده است.</p></main></body></html>

The execution section explains this file’s command, expected outcome and limits.

3. Complete run_audit.py

Save this entire file beside the others using the exact filename.

import json
from pathlib import Path
import sys
from audit_seo import audit

if __name__ == '__main__':
    paths = [Path(name) for name in sys.argv[1:]] or [Path(__file__).with_name('demo.html')]
    failed = False
    for path in paths:
        errors = audit(path.read_text(encoding='utf-8'))
        print(json.dumps({'file': str(path), 'errors': errors}))
        failed |= bool(errors)
    raise SystemExit(1 if failed else 0)

The execution section explains this file’s command, expected outcome and limits.

4. Complete test_audit.py

Save this entire file beside the others using the exact filename.

from pathlib import Path
import unittest
from audit_seo import audit


class AuditTests(unittest.TestCase):
    def setUp(self):
        self.page = Path(__file__).with_name('demo.html').read_text(encoding='utf-8')

    def test_valid_and_duplicate_titles(self):
        self.assertEqual(audit(self.page), [])
        self.assertIn('expected-one-h1', audit(self.page.replace('</main>', '<h1>Duplicate</h1></main>')))

    def test_accidental_noindex_and_relative_canonical(self):
        self.assertIn('page-is-noindex', audit(self.page.replace('index,follow', 'noindex,follow')))
        broken = self.page.replace('rel="canonical" href="https://example.test/article.html"', 'rel="canonical" href="article.html"')
        self.assertIn('canonical-must-be-absolute-https', audit(broken))


if __name__ == '__main__': unittest.main()

The execution section explains this file’s command, expected outcome and limits.

Run the audit and interpret its limits

For lasting value, write accurate titles and descriptions around reader needs and provide useful internal links. Repeated paragraphs, exaggerated headlines and important content hidden behind heavy loading are not repaired merely by adding more keywords. Make this audit one part of release review and evaluate changes against observed evidence. The practical objective is an understandable, accessible page whose technical declarations accurately describe the content and the routes visitors can use.

Implementation checklist

  • Match each language canonical to the right page.
  • Keep test indexing settings out of public releases.
  • Separate file checks from server behavior.
  • Write accurate unique titles and descriptions.

References

Editorial guidance: Liyan Knowledge.