Spaces:
Sleeping
Sleeping
| """ | |
| 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'<span class="badge {cls}">{esc(text)}</span>' | |
| 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'<span class="tag-sm issue">{esc(link["status_code"])} BROKEN</span>' | |
| elif link['link_status'] == 'Redirect': | |
| status_tag = f'<span class="tag-sm issue">{esc(link["status_code"])} Redirect</span>' | |
| else: | |
| status_tag = f'<span class="tag-sm ok">{esc(link["status_code"])}</span>' | |
| has_follow_flag = any('Dofollow' in f or 'Nofollow' in f for f in link.get('flags', [])) | |
| if has_follow_flag: | |
| follow_tag = f'<span class="tag-sm issue">{link["follow"]} β </span>' | |
| else: | |
| follow_tag = f'<span class="tag-sm ok">{link["follow"]} β</span>' | |
| out = f'<div class="le {cls}">' | |
| out += f'<div class="le-url">{esc(short_url(link["url"], domain))}</div>' | |
| out += f'<div class="le-tags">{status_tag}{follow_tag}</div>' | |
| out += f'<div class="le-anchor">Anchor: <b>{esc(link["anchor"])}</b></div>' | |
| if link.get('redirect_url'): | |
| out += f'<div class="le-redir">β {esc(short_url(link["redirect_url"], domain))}</div>' | |
| out += f'<div class="le-location">π {esc(link["location"])}</div>' | |
| if show_flags: | |
| for flag in link.get('flags', []): | |
| out += f'<div class="le-issue">β {esc(flag)}</div>' | |
| out += '</div>' | |
| return out | |
| def render_accordion(collapsed_html, details_html): | |
| return f'''<td class="acc-cell" onclick="toggleAcc(this)"> | |
| <div class="acc-collapsed"><div class="acc-chevron">βΆ</div><div class="acc-summary">{collapsed_html}</div></div> | |
| <div class="acc-details">{details_html}</div> | |
| </td>''' | |
| 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'''<tr><td class="cell-url"><div class="row-num">#{idx}</div>{esc(short_url(r["url"], domain))}</td> | |
| <td colspan="15" style="color:var(--red);vertical-align:middle;">β Error: {esc(r["error"])}</td></tr>''' | |
| 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 = '<div style="font-size:11px;color:var(--red);">No internal links found in body content.</div>' | |
| # 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 = '<div style="font-size:11px;color:var(--text-dim);">No external links in body content.</div>' | |
| # 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 = '<div style="font-size:11px;color:var(--green);">All internal=Dofollow β and external=Nofollow β</div>' | |
| # 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 '<div style="font-size:11px;color:var(--green);">No broken internal links.</div>' | |
| 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 '<div style="font-size:11px;color:var(--green);">No broken external links.</div>' | |
| 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 '<div style="font-size:11px;color:var(--green);">No internal redirects.</div>' | |
| 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 '<div style="font-size:11px;color:var(--green);">No external redirects.</div>' | |
| # 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'<div class="le s-issue"><div class="le-url">{esc(short_url(d["url"], domain))}</div>' | |
| dup_details += f'<div class="le-issue">β Appears {d["count"]}x in body content</div>' | |
| dup_details += f'<div class="le-location">π Locations: {locs}</div></div>' | |
| if not dup_details: | |
| dup_details = '<div style="font-size:11px;color:var(--green);">No duplicate links in body content.</div>' | |
| # 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'''<div class="se"><div class="se-head"><span class="se-section">{esc(s["section"])}</span> | |
| <span class="se-pri {pri_cls}">{s["priority"]}</span></div> | |
| <div class="se-url">{esc(s["target"])}</div> | |
| <div class="se-anchor">β "{esc(s["anchor"])}"</div></div>''' | |
| if not sug_details: | |
| sug_details = '<div style="font-size:11px;color:var(--text-dim);">No keyword matches for suggestions.</div>' | |
| # 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'<div class="ni critical">β {esc(issue)}</div>' for issue in issues) | |
| if not note_details: | |
| note_details = '<div style="font-size:11px;color:var(--green);">No issues detected.</div>' | |
| orphan_cell = '<span class="badge issue">Yes β </span>' if is_orphan else '<span class="badge ok">No</span>' | |
| rows_html += f'''<tr data-broken="{r['broken_int_count']+r['broken_ext_count']}" data-redirect="{r['redirect_int_count']+r['redirect_ext_count']}" data-internal="{r['int_count']}" data-follow-flag="{r['follow_flag_count']}" data-duplicates="{r['duplicate_count']}" data-orphan="{1 if is_orphan else 0}"> | |
| <td class="cell-url"><div class="row-num">#{idx}</div>{esc(short_url(r['url'], domain))}</td> | |
| <td class="cell-count {'issue' if r['int_count']<3 else 'neutral'}">{r['int_count']}</td> | |
| <td class="cell-count neutral">{r['ext_count']}</td> | |
| {render_accordion(int_badges, int_details)} | |
| {render_accordion(ext_badges, ext_details)} | |
| <td class="cell-count"><span style="color:var(--green)">{r['int_df']}</span><span style="color:var(--text-dim)"> / </span><span style="color:{'var(--red)' if r['int_nf']>0 else 'var(--text-dim)'}">{r['int_nf']}</span></td> | |
| <td class="cell-count"><span style="color:{'var(--red)' if r['ext_df']>0 else 'var(--text-dim)'}">{r['ext_df']}</span><span style="color:var(--text-dim)"> / </span><span style="color:var(--green)">{r['ext_nf']}</span></td> | |
| {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)} | |
| <td class="cell-orphan">{orphan_cell}</td> | |
| {render_accordion(sug_badges, sug_details)} | |
| {render_accordion(note_badges, note_details)} | |
| </tr>''' | |
| report = f'''<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"> | |
| <title>Bulk Link Audit Report</title> | |
| <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> | |
| <style> | |
| :root {{--bg:#f4f6f9;--surface:#fff;--surface-2:#f8f9fb;--surface-3:#eef1f5;--border:#e2e6ed;--border-light:#d0d5de;--text:#1a1f2e;--text-muted:#5c6478;--text-dim:#8892a6;--accent:#2563eb;--accent-dim:rgba(37,99,235,0.06);--accent-light:rgba(37,99,235,0.10);--green:#059669;--green-dim:rgba(5,150,105,0.06);--green-light:rgba(5,150,105,0.10);--red:#dc2626;--red-dim:rgba(220,38,38,0.05);--red-light:rgba(220,38,38,0.08);--orange:#d97706;--orange-dim:rgba(217,119,6,0.06);--purple:#7c3aed;--purple-dim:rgba(124,58,237,0.06);--pink:#db2777;--pink-dim:rgba(219,39,119,0.06);--mono:'JetBrains Mono',monospace;--font:'DM Sans',sans-serif;--radius:8px;--radius-sm:5px;}} | |
| *{{margin:0;padding:0;box-sizing:border-box;}}body{{background:var(--bg);color:var(--text);font-family:var(--font);-webkit-font-smoothing:antialiased;line-height:1.55;}} | |
| .header{{padding:32px 36px 24px;border-bottom:1px solid var(--border);background:var(--surface);}}.header .tag-line{{font-size:10px;font-weight:700;letter-spacing:1.8px;text-transform:uppercase;color:var(--accent);margin-bottom:8px;}}.header h1{{font-size:21px;font-weight:700;margin-bottom:4px;}}.header .meta{{font-size:11.5px;color:var(--text-dim);}}.header .meta span{{color:var(--text-muted);}} | |
| .toolbar{{display:flex;align-items:center;gap:10px;padding:12px 36px;background:var(--surface);border-bottom:1px solid var(--border);flex-wrap:wrap;}}.toolbar-btn{{font-family:var(--font);font-size:11px;font-weight:600;color:var(--text-muted);background:var(--surface-2);border:1px solid var(--border);border-radius:var(--radius-sm);padding:6px 14px;cursor:pointer;transition:all .15s;}}.toolbar-btn:hover{{color:var(--text);background:var(--surface-3);}}.toolbar-btn.active{{color:var(--accent);border-color:var(--accent);background:var(--accent-dim);}}.toolbar-sep{{width:1px;height:20px;background:var(--border);}}.toolbar-label{{font-size:10px;font-weight:600;letter-spacing:.5px;text-transform:uppercase;color:var(--text-dim);margin-right:4px;}} | |
| .legend{{display:flex;flex-wrap:wrap;gap:18px;padding:10px 36px;background:var(--surface-2);border-bottom:1px solid var(--border);}}.legend-item{{display:flex;align-items:center;gap:6px;font-size:10.5px;color:var(--text-muted);}}.legend-dot{{width:8px;height:8px;border-radius:50%;}}.legend-dot.green{{background:var(--green);}}.legend-dot.red{{background:var(--red);}}.legend-dot.blue{{background:var(--accent);}} | |
| .summary-bar{{display:flex;border-bottom:1px solid var(--border);background:var(--surface);flex-wrap:wrap;}}.summary-stat{{flex:1;padding:14px 16px;border-right:1px solid var(--border);text-align:center;min-width:90px;}}.summary-stat:last-child{{border-right:none;}}.summary-stat .s-val{{font-size:22px;font-weight:700;line-height:1;margin-bottom:2px;}}.summary-stat .s-label{{font-size:9px;font-weight:600;letter-spacing:.5px;text-transform:uppercase;color:var(--text-dim);}}.s-val.blue{{color:var(--accent);}}.s-val.green{{color:var(--green);}}.s-val.red{{color:var(--red);}}.s-val.pink{{color:var(--pink);}} | |
| .table-wrap{{overflow-x:auto;background:var(--surface);}}table{{width:100%;border-collapse:collapse;min-width:2400px;}} | |
| thead th{{background:var(--surface-3);color:var(--text-muted);font-size:9px;font-weight:700;letter-spacing:.7px;text-transform:uppercase;padding:12px 12px;text-align:left;border-bottom:2px solid var(--border-light);position:sticky;top:0;z-index:10;white-space:nowrap;}}thead th.center{{text-align:center;}}thead th.c-blue{{border-bottom-color:var(--accent);color:var(--accent);}}thead th.c-red{{border-bottom-color:var(--red);color:var(--red);}}thead th.c-green{{border-bottom-color:var(--green);color:var(--green);}}thead th.c-pink{{border-bottom-color:var(--pink);color:var(--pink);}}thead th:first-child{{position:sticky;left:0;z-index:15;border-right:2px solid var(--border-light);background:var(--surface-3);}} | |
| tbody tr{{border-bottom:1px solid var(--border);transition:background .12s;}}tbody tr:hover{{background:var(--accent-dim);}} | |
| tbody td{{padding:10px 12px;vertical-align:top;font-size:12px;border-right:1px solid var(--border);}}tbody td:last-child{{border-right:none;}} | |
| td.cell-url{{font-family:var(--mono);font-size:11px;color:var(--accent);word-break:break-all;line-height:1.5;background:var(--surface);position:sticky;left:0;z-index:5;border-right:2px solid var(--border-light);min-width:260px;max-width:260px;}}td.cell-url .row-num{{font-size:9px;color:var(--text-dim);margin-bottom:4px;font-weight:600;}} | |
| td.cell-count{{text-align:center;font-family:var(--mono);font-size:15px;font-weight:700;vertical-align:middle;}}td.cell-count.ok{{color:var(--green);}}td.cell-count.issue{{color:var(--red);}}td.cell-count.neutral{{color:var(--text);}} | |
| .acc-cell{{cursor:pointer;user-select:none;}}.acc-collapsed{{display:flex;align-items:center;gap:8px;min-height:26px;}}.acc-chevron{{width:20px;height:20px;border-radius:4px;background:var(--surface-3);display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:transform .2s,background .15s;font-size:10px;color:var(--text-dim);border:1px solid var(--border);}}.acc-cell.open .acc-chevron{{transform:rotate(90deg);background:var(--accent-dim);color:var(--accent);border-color:var(--accent);}}.acc-summary{{font-size:11px;color:var(--text-muted);display:flex;flex-wrap:wrap;gap:4px;align-items:center;}} | |
| .badge{{font-size:9px;font-weight:700;padding:2.5px 8px;border-radius:10px;letter-spacing:.2px;white-space:nowrap;border:1px solid transparent;}}.badge.ok{{background:var(--green-dim);color:var(--green);border-color:var(--green-light);}}.badge.issue{{background:var(--red-dim);color:var(--red);border-color:var(--red-light);}}.badge.sug{{background:var(--green-dim);color:var(--green);border-color:var(--green-light);}}.badge.neutral{{background:var(--surface-3);color:var(--text-dim);border-color:var(--border);}} | |
| .acc-details{{display:none;margin-top:10px;padding-top:10px;border-top:1px dashed var(--border);}}.acc-cell.open .acc-details{{display:block;}} | |
| .le{{padding:9px 11px;background:var(--surface-2);border-radius:var(--radius-sm);margin-bottom:6px;border:1px solid var(--border);border-left-width:3px;}}.le:last-child{{margin-bottom:0;}}.le.s-ok{{border-left-color:var(--green);}}.le.s-issue{{border-left-color:var(--red);background:var(--red-dim);}} | |
| .le .le-url{{font-family:var(--mono);font-size:10.5px;color:var(--accent);word-break:break-all;margin-bottom:4px;}}.le.s-issue .le-url{{color:var(--red);}} | |
| .le .le-tags{{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:4px;}}.tag-sm{{font-size:8.5px;font-weight:700;padding:2px 6px;border-radius:8px;}}.tag-sm.ok{{background:var(--green-light);color:var(--green);}}.tag-sm.issue{{background:var(--red-light);color:var(--red);}} | |
| .le .le-anchor{{font-size:10.5px;color:var(--text-muted);}}.le .le-anchor b{{color:var(--text);font-weight:600;}}.le .le-redir{{font-family:var(--mono);font-size:10px;color:var(--orange);margin-top:3px;}}.le .le-location{{font-size:10px;color:var(--text-dim);margin-top:4px;}}.le .le-issue{{font-size:10px;color:var(--red);margin-top:5px;padding-top:5px;border-top:1px dashed var(--border);font-weight:500;}}.le .le-fix{{font-size:10px;color:var(--green);margin-top:3px;font-weight:500;}} | |
| .se{{padding:8px 11px;background:var(--surface-2);border-radius:var(--radius-sm);margin-bottom:5px;border:1px solid var(--border);border-left:3px solid var(--green);}}.se:last-child{{margin-bottom:0;}}.se .se-head{{display:flex;justify-content:space-between;align-items:center;margin-bottom:2px;}}.se .se-section{{font-size:9.5px;font-weight:600;letter-spacing:.3px;text-transform:uppercase;color:var(--text-dim);}}.se-pri{{font-size:8.5px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;padding:2px 7px;border-radius:8px;}}.se-pri.high{{background:var(--red-light);color:var(--red);}}.se-pri.med{{background:rgba(217,119,6,.1);color:var(--orange);}}.se .se-url{{font-family:var(--mono);font-size:10px;color:var(--accent);word-break:break-all;margin-bottom:1px;}}.se .se-anchor{{font-size:10px;color:var(--green);font-weight:500;}} | |
| .ni{{font-size:11px;color:var(--text-muted);line-height:1.5;padding:5px 0;border-bottom:1px dashed var(--border);}}.ni:last-child{{border-bottom:none;}}.ni.critical{{color:var(--red);font-weight:500;}} | |
| td.cell-orphan{{text-align:center;vertical-align:middle;}} | |
| .footer{{padding:18px 36px;border-top:1px solid var(--border);font-size:11px;color:var(--text-dim);text-align:center;background:var(--surface);}} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="header"><div class="tag-line">Bulk Link Audit Report</div><h1>Blog β Body Content Link Analysis</h1><div class="meta">Scope: Body content only Β· <span>{now}</span> Β· Pages: <span>{total_pages}</span> Β· Domain: <span>{esc(domain)}</span></div></div> | |
| <div class="toolbar"><span class="toolbar-label">Actions:</span><button class="toolbar-btn" onclick="expandAll()">β Expand All</button><button class="toolbar-btn" onclick="collapseAll()">β Collapse All</button><div class="toolbar-sep"></div><span class="toolbar-label">Filter:</span><button class="toolbar-btn filter-btn active" onclick="filterRows('all',this)">All ({total_pages})</button><button class="toolbar-btn filter-btn" onclick="filterRows('broken',this)">Broken ({total_broken})</button><button class="toolbar-btn filter-btn" onclick="filterRows('redirect',this)">Redirects ({total_redirects})</button><button class="toolbar-btn filter-btn" onclick="filterRows('low-links',this)">Low Links</button><button class="toolbar-btn filter-btn" onclick="filterRows('follow-flag',this)">Follow Flags ({total_flags})</button><button class="toolbar-btn filter-btn" onclick="filterRows('duplicates',this)">Duplicates ({total_dups})</button><button class="toolbar-btn filter-btn" onclick="filterRows('orphan',this)">Orphans ({total_orphan})</button></div> | |
| <div class="legend"><div class="legend-item"><div class="legend-dot green"></div> No Issues</div><div class="legend-item"><div class="legend-dot red"></div> Issue (Broken / Flag / Redirect / Duplicate)</div><div class="legend-item"><div class="legend-dot blue"></div> Info</div></div> | |
| <div class="summary-bar"><div class="summary-stat"><div class="s-val blue">{total_pages}</div><div class="s-label">Pages</div></div><div class="summary-stat"><div class="s-val blue">{total_int}</div><div class="s-label">Internal</div></div><div class="summary-stat"><div class="s-val blue">{total_ext}</div><div class="s-label">External</div></div><div class="summary-stat"><div class="s-val red">{total_broken}</div><div class="s-label">Broken</div></div><div class="summary-stat"><div class="s-val red">{total_redirects}</div><div class="s-label">Redirects</div></div><div class="summary-stat"><div class="s-val red">{total_flags}</div><div class="s-label">Follow Flags</div></div><div class="summary-stat"><div class="s-val pink">{total_dups}</div><div class="s-label">Duplicates</div></div><div class="summary-stat"><div class="s-val green">{total_sug}</div><div class="s-label">Suggestions</div></div><div class="summary-stat"><div class="s-val red">{total_orphan}</div><div class="s-label">Orphans</div></div></div> | |
| <div class="table-wrap"><table><thead><tr><th>URL</th><th class="center c-blue">Int.</th><th class="center c-blue">Ext.</th><th class="c-blue">Internal Links</th><th class="c-blue">External Links</th><th class="center">Int DF/NF</th><th class="center">Ext DF/NF</th><th class="c-red">Follow Flags</th><th class="c-red">Broken Int.</th><th class="c-red">Broken Ext.</th><th class="c-red">Redirect Int.</th><th class="c-red">Redirect Ext.</th><th class="c-pink">Duplicates</th><th class="center">Orphan</th><th class="c-green">Suggestions</th><th>Notes</th></tr></thead> | |
| <tbody>{rows_html}</tbody></table></div> | |
| <div class="footer">Bulk Link Audit Β· Body Content Scope Β· {now} Β· Click βΆ to expand</div> | |
| <script> | |
| function toggleAcc(c){{c.classList.toggle('open');}} | |
| function expandAll(){{document.querySelectorAll('.acc-cell').forEach(c=>c.classList.add('open'));}} | |
| function collapseAll(){{document.querySelectorAll('.acc-cell').forEach(c=>c.classList.remove('open'));}} | |
| function filterRows(t,b){{document.querySelectorAll('.filter-btn').forEach(x=>x.classList.remove('active'));b.classList.add('active');document.querySelectorAll('tbody tr').forEach(r=>{{let d=r.dataset,s=true;if(t==='broken')s=parseInt(d.broken||0)>0;else if(t==='redirect')s=parseInt(d.redirect||0)>0;else if(t==='low-links')s=parseInt(d.internal||0)<5;else if(t==='follow-flag')s=parseInt(d.followFlag||0)>0;else if(t==='duplicates')s=parseInt(d.duplicates||0)>0;else if(t==='orphan')s=parseInt(d.orphan||0)>0;r.style.display=s?'':'none';}});}} | |
| </script> | |
| </body></html>''' | |
| return report | |