Objective: Develop an SEO analysis tool, for a website built in Django, that allows users to input a URL and receive an SEO report containing information about the page's optimization strengths and weaknesses.
Using: Python 3.10 and Django 4.1.1
Difficulty: Design. The code works, but unfortunately I don't know whether I've taken the most professional approach. At times, I feel I make my code too convoluted, and miss more elegant solutions. Am I using dataclasses correctly? Is the code hard to read? Given the complexity and importance of this app, I want to make sure I'm on the right track before getting too deep. So I look forward to your feedback, thanks!
seo_dataclasses.py
Store SEO reports for easy access and readability.
Future: add WebsiteReport
class that stores a list of page results for the given website. Save website reports to db.
@dataclass
class SEOCheckResult:
error: bool
check_failed_desc: Optional[str] = None
check_passed_desc: Optional[str] = None
solution: Optional[str] = None
code: Optional[str] = None
@dataclass
class SEOChecks:
category: str
category_results: List[SEOCheckResult] = field(default_factory=list)
@dataclass
class PageReport:
soup: BeautifulSoup
domain: str
url: str
keyword: str
page_results: List[SEOChecks] = field(default_factory=list)
def generate_results(self):
seo_check_functions = [
analyze_h_tag
# etc
]
for seo_func in seo_check_functions:
self.page_results.append(seo_func(self.soup, self.keyword))
seo_checks.py
Contains all functions associated with assessing a page's SEO.
def analyze_h_tag(soup, keyword):
"""
Analyze h1 tags.
Counts the number of h1 tags on a page.
Ensures proper syntax is being used.
"""
h1 = soup.find_all("h1")
h1_results = SEOChecks(category="H1 Tags")
if not h1:
h1_results.category_results.append(
SEOCheckResult(
error=True,
check_failed_desc="Missing H1 tag.",
solution="Implement an H1 tag.",
)
)
return h1_results
else:
h1_results.category_results.append(
SEOCheckResult(error=False, check_passed_desc="H1 tag found.")
)
# Check number of H1 tags in use
if len(h1) > 1:
for h1_tag in h1[1:]:
h1_results.category_results.append(
SEOCheckResult(
error=True,
check_failed_desc="Multiple H1 tags found.",
solution="Reduce to one H1 tag.",
)
)
else:
h1_results.category_results.append(
SEOCheckResult(
error=False, check_passed_desc="Single H1 tag present."
)
)
# Get the first h1 tag, all further analysis will be on it alone.
main_heading = h1[0].text
lower_heading = main_heading.lower()
# Check keyword usage
if keyword not in lower_heading:
h1_results.category_results.append(
SEOCheckResult(
error=True,
check_failed_desc="Keyword not present in H1 tag.",
solution="Add keyword.",
)
)
else:
h1_results.category_results.append(
SEOCheckResult(
error=False, check_passed_desc="Keyword present in H1 tag."
)
)
heading_split = lower_heading.split()
if (
keyword not in heading_split[: math.trunc(len(main_heading) / 2)]
): # Must be present in the first half
h1_results.category_results.append(
SEOCheckResult(
error=True,
check_failed_desc="Keyword priority.",
solution="Move keyword forward.",
)
)
else:
h1_results.category_results.append(
SEOCheckResult(
error=False, check_passed_desc="Correct keyword placement."
)
)
return h1_results
def analyze_meta_title(soup, keyword):
pass
def analyze_meta_description(soup, keyword):
pass
# etc
crawler.py
Crawl page or crawl site. Establish response. Future: Improve security. Limit user to x number of requests per day.
class Crawler:
def __init__(self, url, keyword):
self.conn_error = None
self.url = url if urlsplit(url).scheme else f"http://{url}"
self.parse = urlsplit(self.url)
self.domain = self.parse.netloc
self.request = self._get_request(self.url)
self.keyword = keyword
self.soup = (
BeautifulSoup(self.request.text, "lxml") if self.request else None
)
def _get_request(self, url):
try:
response = requests.get(url, timeout=40)
response.raise_for_status()
return response
except (
requests.exceptions.ConnectionError,
requests.exceptions.Timeout,
requests.exceptions.RequestException,
) as e:
self.conn_error = str(e)
return None
def crawl_page(self):
page = PageReport(
soup=self.soup,
domain=self.domain,
url=self.url,
keyword=self.keyword,
)
page.generate_results()
return page.page_results
def crawl_site(self):
pass
views.py
def seo_tools(request):
if request.method == "POST":
website = request.POST["website"]
keyword = request.POST["keyword"]
page = Crawler(url=website, keyword=keyword)
if page.conn_error:
messages.error(request, page.conn_error)
return render(request, "freetools/seo-tools.html")
results = page.crawl_page()
context = {"analysis": results}
return render(request, "freetools/seo-tools.html", context)
else:
return render(request, "freetools/seo-tools.html")