Site search

Search IndexLane

Type at least two characters to search.

    Technical note

    Internal link audits: menus, footers, and contextual links

    Find important pages with weak internal links. Check their navigation paths, anchor text, redirects, and links from relevant content.

    By NikoPublished August 27, 2026Updated September 5, 2026Read 5 min

    An important page should be easy to reach from the parts of your site where visitors would expect to find it. A footer link may make a product discoverable, but links from its category and relevant guides explain much more about it.

    Audit the links that actually appear on your pages: their destinations, wording, placement, and depth from the main navigation. Google's link guidance focuses on crawlable anchors and relevant text. It does not give a formula for how much more a body link is worth than a footer link.

    Record where links appear

    For each link, record the source URL, final target URL, anchor text, DOM region, response status, canonical target, and whether the link was present in the initial or rendered HTML. Keep the individual links so you can trace an inbound count back to the pages that produced it.

    Use elements such as nav, main, and footer to identify link placement. Component markers or CSS classes can help where those elements are absent. This starter script reads the initial HTML; use browser-rendered HTML as well when JavaScript adds links:

    from bs4 import BeautifulSoup
    import requests
    from collections import defaultdict
    from urllib.parse import urljoin, urlparse
    
    def classify_link_container(link_element):
        """Classify a rendered link using the nearest meaningful ancestor."""
        parent = link_element
        depth = 0
    
        while parent and depth < 10:
            if parent.name in {'nav', 'footer', 'main', 'aside'}:
                return parent.name
            marker = parent.get('data-link-region')
            if marker:
                return marker
            for cls in parent.get('class', []):
                cls_lower = cls.lower()
                if 'nav' in cls_lower or 'menu' in cls_lower:
                    return 'nav'
                if 'footer' in cls_lower or 'foot' in cls_lower:
                    return 'footer'
                if 'sidebar' in cls_lower or 'aside' in cls_lower:
                    return 'sidebar'
                if 'content' in cls_lower or 'article' in cls_lower:
                    return 'content'
            parent = parent.parent
            depth += 1
    
        return 'unknown'
    
    def audit_internal_links(start_url, allowed_host):
        response = requests.get(start_url, timeout=10)
        response.raise_for_status()
        soup = BeautifulSoup(response.text, 'html.parser')
        link_map = defaultdict(list)
    
        for a in soup.find_all('a', href=True):
            target = urljoin(start_url, a['href'])
            if urlparse(target).hostname == allowed_host:
                container = classify_link_container(a)
                link_map[container].append(target)
    
        return link_map
    

    Run this across the crawlable site, not only a hand-picked set of prominent pages. Resolve redirects and normalise fragments and tracking parameters before grouping targets. Flag intended landing pages that have no relevant category or contextual path, but do not treat a fixed percentage of navigation links as a Google threshold.

    Make large menus easier to use

    A mega-menu can expose hundreds of destinations on every page and make the site's hierarchy difficult to read. The fix is not an arbitrary link-count limit. Keep the menu focused on real navigation needs, then add relevant contextual links where they help a reader continue. A WordPress related-content component can use an explicitly curated relationship so it links to relevant posts instead of simply the newest ones:

    add_filter('the_content', function ($content) {
        if (! is_single()) {
            return $content;
        }
    
        $related_ids = array_map('intval', (array) get_post_meta(
            get_the_ID(),
            'related_post_ids',
            true
        ));
        if ($related_ids === []) {
            return $content;
        }
    
        $related = new WP_Query([
            'post__in' => $related_ids,
            'orderby' => 'post__in',
            'posts_per_page' => count($related_ids),
        ]);
    
        if ($related->have_posts()) {
            $content .= '<div class="contextual-links">';
            while ($related->have_posts()) {
                $related->the_post();
                $content .= sprintf(
                    '<a href="%s">%s</a>',
                    esc_url(get_permalink()),
                    esc_html(get_the_title())
                );
            }
            $content .= '</div>';
            wp_reset_postdata();
        }
    
        return $content;
    });
    

    Check links in shared templates

    Laravel Blade templates often render the same navigation partial on every page. Audit the hierarchy for duplicate destinations, conflicting anchors, redirected URLs, and inconsistent canonical forms:

    {{-- layouts/app.blade.php --}}
    @include('partials.header')       {{-- logo link → homepage occurs here --}}
    @include('partials.breadcrumbs')  {{-- breadcrumb link to parent may occur before main content --}}
    @yield('content')                 {{-- contextual 'read more' links appear last --}}
    

    Keep the semantic HTML order aligned with the visible page. Do not move breadcrumbs after the content or inject hidden keyword-rich links merely to manufacture a first occurrence. Use named routes and clear anchors so repeated links point to one canonical destination.

    Find gaps in the link map

    For a large site, keep counts by page and link placement. This Go data structure is a starting point:

    type LinkGraph struct {
        nodes map[string]*Node
    }
    
    type Node struct {
        URL       string
        Inbound   int
        Outbound  int
        ByRegion  map[string]int
    }
    
    func (g *LinkGraph) AddEdge(from, to, position string) {
        g.nodes[to].Inbound++
        g.nodes[to].ByRegion[position]++
        g.nodes[from].Outbound++
    }
    

    Initialise nodes before incrementing them and store source-level edges in a production implementation so duplicates on one template do not hide the number of distinct linking pages. Pages with navigation links but no relevant category or contextual paths are candidates for review. Add links from pages where the destination is useful to the reader, then recrawl to confirm that the links work and use the final URL.

    Related notes

    Does the first link to a page get special SEO treatment?

    Check repeated links for clear labels, consistent destinations, and accessible ordering without relying on an undocumented ranking rule.

    Read article →

    Use stable @id values in Schema.org markup

    Give an author, publisher, or article one consistent identifier so your JSON-LD blocks describe the same entities across pages.

    Read article →