""" HTML Report Generator Produces the interactive accordion-based light-theme audit report. """ import html as html_module from urllib.parse import urlparse from datetime import datetime def esc(text): return html_module.escape(str(text)) if text else "" def short_url(url, domain="edstellar.com"): parsed = urlparse(str(url)) if domain in parsed.netloc: return parsed.path + (('?' + parsed.query) if parsed.query else '') host = parsed.netloc.replace('www.', '') path = parsed.path if len(path) > 50: path = path[:25] + '...' + path[-20:] return f"{host}{path}" def badge(text, cls): return f'{esc(text)}' def render_link_entry(link, domain="edstellar.com", show_flags=True): has_issues = link['link_status'] in ('Broken',) or link.get('flags') is_redirect = link['link_status'] == 'Redirect' cls = 's-issue' if (has_issues or is_redirect) else 's-ok' if link['link_status'] == 'Broken': status_tag = f'{esc(link["status_code"])} BROKEN' elif link['link_status'] == 'Redirect': status_tag = f'{esc(link["status_code"])} Redirect' else: status_tag = f'{esc(link["status_code"])}' has_follow_flag = any('Dofollow' in f or 'Nofollow' in f for f in link.get('flags', [])) if has_follow_flag: follow_tag = f'{link["follow"]} โš ' else: follow_tag = f'{link["follow"]} โœ“' out = f'
' out += f'
{esc(short_url(link["url"], domain))}
' out += f'
{status_tag}{follow_tag}
' out += f'
Anchor: {esc(link["anchor"])}
' if link.get('redirect_url'): out += f'
โ†’ {esc(short_url(link["redirect_url"], domain))}
' out += f'
๐Ÿ“ {esc(link["location"])}
' if show_flags: for flag in link.get('flags', []): out += f'
โš  {esc(flag)}
' out += '
' return out def render_accordion(collapsed_html, details_html): return f'''
โ–ถ
{collapsed_html}
{details_html}
''' def generate_report(results, orphan_pages, domain="edstellar.com"): now = datetime.now().strftime("%b %d, %Y %H:%M") total_pages = len(results) total_int = sum(r['int_count'] for r in results) total_ext = sum(r['ext_count'] for r in results) total_broken = sum(r['broken_int_count'] + r['broken_ext_count'] for r in results) total_redirects = sum(r['redirect_int_count'] + r['redirect_ext_count'] for r in results) total_flags = sum(r['follow_flag_count'] for r in results) total_dups = sum(r['duplicate_count'] for r in results) total_sug = sum(len(r['suggestions']) for r in results) total_orphan = len(orphan_pages) rows_html = "" for idx, r in enumerate(results, 1): if r['error']: rows_html += f'''
#{idx}
{esc(short_url(r["url"], domain))} โŒ Error: {esc(r["error"])}''' continue is_orphan = r['url'].rstrip('/').split('?')[0] in orphan_pages # Internal Links int_badges = "" if r['int_count'] == 0: int_badges = badge('0 Links โš ', 'issue') else: ok_count = r['int_count'] - r['broken_int_count'] - r['redirect_int_count'] if ok_count > 0: int_badges += badge(f'{ok_count} Active', 'ok') if r['broken_int_count'] > 0: int_badges += badge(f'{r["broken_int_count"]} Broken', 'issue') if r['redirect_int_count'] > 0: int_badges += badge(f'{r["redirect_int_count"]} Redirect', 'issue') int_details = "".join(render_link_entry(l, domain) for l in r['internal_links']) if not int_details: int_details = '
No internal links found in body content.
' # External Links ext_badges = "" if r['ext_count'] == 0: ext_badges = badge('0 Links', 'neutral') else: if r['broken_ext_count'] > 0: ext_badges += badge(f'{r["broken_ext_count"]} Broken', 'issue') if r['redirect_ext_count'] > 0: ext_badges += badge(f'{r["redirect_ext_count"]} Redirect', 'issue') ext_df_count = sum(1 for l in r['external_links'] if l['follow'] == 'Dofollow') if ext_df_count > 0: ext_badges += badge(f'{ext_df_count} Dofollow โš ', 'issue') ok_ext = r['ext_count'] - r['broken_ext_count'] - r['redirect_ext_count'] if ok_ext > 0 and not r['broken_ext_count'] and not r['redirect_ext_count'] and not ext_df_count: ext_badges += badge(f'{ok_ext} Active', 'ok') ext_details = "".join(render_link_entry(l, domain) for l in r['external_links']) if not ext_details: ext_details = '
No external links in body content.
' # Follow Flags int_nf_flags = [l for l in r['internal_links'] if l['follow'] == 'Nofollow'] ext_df_flags_list = [l for l in r['external_links'] if l['follow'] == 'Dofollow'] flag_badges = "" if int_nf_flags: flag_badges += badge(f'{len(int_nf_flags)} Int. Nofollow โš ', 'issue') if ext_df_flags_list: flag_badges += badge(f'{len(ext_df_flags_list)} Ext. Dofollow โš ', 'issue') if not flag_badges: flag_badges = badge('โœ“ No Flags', 'ok') flag_details = "".join(render_link_entry(l, domain, show_flags=True) for l in int_nf_flags + ext_df_flags_list) if not flag_details: flag_details = '
All internal=Dofollow โœ“ and external=Nofollow โœ“
' # Broken / Redirect bi_badges = badge(f'{r["broken_int_count"]} Broken', 'issue') if r['broken_int_count'] > 0 else badge('โœ“ None', 'ok') bi_details = "".join(render_link_entry(l, domain) for l in r['broken_internal']) or '
No broken internal links.
' be_badges = badge(f'{r["broken_ext_count"]} Broken', 'issue') if r['broken_ext_count'] > 0 else badge('โœ“ None', 'ok') be_details = "".join(render_link_entry(l, domain) for l in r['broken_external']) or '
No broken external links.
' ri_badges = badge(f'{r["redirect_int_count"]} Redirects', 'issue') if r['redirect_int_count'] > 0 else badge('โœ“ None', 'ok') ri_details = "".join(render_link_entry(l, domain) for l in r['redirect_internal']) or '
No internal redirects.
' re_badges = badge(f'{r["redirect_ext_count"]} Redirects', 'issue') if r['redirect_ext_count'] > 0 else badge('โœ“ None', 'ok') re_details = "".join(render_link_entry(l, domain) for l in r['redirect_external']) or '
No external redirects.
' # Duplicates dup_badges = badge(f'{r["duplicate_count"]} Duplicates', 'issue') if r['duplicate_count'] > 0 else badge('โœ“ None', 'ok') dup_details = "" for d in r['duplicates']: locs = ", ".join(esc(l) for l in d['locations']) dup_details += f'
{esc(short_url(d["url"], domain))}
' dup_details += f'
โš  Appears {d["count"]}x in body content
' dup_details += f'
๐Ÿ“ Locations: {locs}
' if not dup_details: dup_details = '
No duplicate links in body content.
' # Suggestions sug_list = r['suggestions'] high_count = sum(1 for s in sug_list if s['priority'] == 'High') sug_badges = "" if sug_list: sug_badges = badge(f'{len(sug_list)} Suggestions', 'sug') if high_count: sug_badges += badge(f'{high_count} High', 'issue') else: sug_badges = badge('0', 'neutral') sug_details = "" for s in sug_list: pri_cls = 'high' if s['priority'] == 'High' else 'med' sug_details += f'''
{esc(s["section"])} {s["priority"]}
{esc(s["target"])}
โ†’ "{esc(s["anchor"])}"
''' if not sug_details: sug_details = '
No keyword matches for suggestions.
' # Notes issues = [] if r['int_count'] < 3: issues.append(f"Only {r['int_count']} internal links โ€” very low for article length") if r['broken_int_count'] + r['broken_ext_count'] > 0: issues.append(f"{r['broken_int_count']+r['broken_ext_count']} broken link(s) need fixing") if ext_df_flags_list: issues.append(f"{len(ext_df_flags_list)} external links are Dofollow โ€” add nofollow") if int_nf_flags: issues.append(f"{len(int_nf_flags)} internal links are Nofollow โ€” should be Dofollow") if r['redirect_int_count'] + r['redirect_ext_count'] > 0: issues.append(f"{r['redirect_int_count']+r['redirect_ext_count']} redirect(s) โ€” update href") if r['duplicate_count'] > 0: issues.append(f"{r['duplicate_count']} duplicate link(s) in body") if is_orphan: issues.append("ORPHAN PAGE โ€” no incoming internal links from other pages") note_badges = badge(f'{len(issues)} Issues', 'issue') if issues else badge('โœ“ Clean', 'ok') note_details = "".join(f'
โš  {esc(issue)}
' for issue in issues) if not note_details: note_details = '
No issues detected.
' orphan_cell = 'Yes โš ' if is_orphan else 'No' rows_html += f'''
#{idx}
{esc(short_url(r['url'], domain))} {r['int_count']} {r['ext_count']} {render_accordion(int_badges, int_details)} {render_accordion(ext_badges, ext_details)} {r['int_df']} / {r['int_nf']} {r['ext_df']} / {r['ext_nf']} {render_accordion(flag_badges, flag_details)} {render_accordion(bi_badges, bi_details)} {render_accordion(be_badges, be_details)} {render_accordion(ri_badges, ri_details)} {render_accordion(re_badges, re_details)} {render_accordion(dup_badges, dup_details)} {orphan_cell} {render_accordion(sug_badges, sug_details)} {render_accordion(note_badges, note_details)} ''' report = f''' Bulk Link Audit Report
Bulk Link Audit Report

Blog โ€” Body Content Link Analysis

Scope: Body content only ยท {now} ยท Pages: {total_pages} ยท Domain: {esc(domain)}
Actions:
Filter:
No Issues
Issue (Broken / Flag / Redirect / Duplicate)
Info
{total_pages}
Pages
{total_int}
Internal
{total_ext}
External
{total_broken}
Broken
{total_redirects}
Redirects
{total_flags}
Follow Flags
{total_dups}
Duplicates
{total_sug}
Suggestions
{total_orphan}
Orphans
{rows_html}
URLInt.Ext.Internal LinksExternal LinksInt DF/NFExt DF/NFFollow FlagsBroken Int.Broken Ext.Redirect Int.Redirect Ext.DuplicatesOrphanSuggestionsNotes
''' return report