Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

SEO & Digital Marketing  Module 2 – Technical SEO: crawling and indexing

Site architecture, URLs and internal links

Plan folders and URLs that stay stable, link every important page with crawlable links and clear anchor text, and find orphan and deep pages.

  • Intermediate
  • 20 minutes
  • Examples run with Node.js 24.21.0, Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Design a URL and folder structure that groups related pages and stays stable for years
  • Link every important page through crawlable a href links with descriptive anchor text
  • Find orphan pages and deep pages in a crawl, and fix them with hubs, page numbers and breadcrumbs
  • Decide which filter, sort and page addresses deserve to be crawled, and keep the rest out

Before you start

What you’ll learn · Before you start

What you will learn

  • Design a URL and folder structure that groups related pages and stays stable for years
  • Link every important page through crawlable a href links with descriptive anchor text
  • Find orphan pages and deep pages in a crawl, and fix them with hubs, page numbers and breadcrumbs
  • Decide which filter, sort and page addresses deserve to be crawled, and keep the rest out

Before you start

On this page

Of Pune Bakes’ 300 pages, 3 have no link from any other page of the site, and 6 blog posts can be reached only by pressing “Next page” five times on the blog: 7 clicks from the home page. Google finds most new pages by following links, so pages like these are the last to be found, and visitors rarely find them at all.

The way pages are grouped and linked is the site’s architecture, and it can be measured and fixed. By the end of this page you can design addresses and folders that stay stable, make every important page reachable through links Google can follow with words that say where they lead, find orphan and deep pages in a crawl and fix them, and decide which filter and page addresses deserve to be crawled at all.

Google’s starter guide says that most of the new pages Google finds each day are found through links (SEO Starter Guide). When Googlebot fetches a page it reads the addresses in its links and adds them to the queue of pages to crawl (JavaScript SEO basics), so a page’s links decide which pages are found next. Google’s link guide sets the floor: each page that matters needs a link from at least one other page of the same site (link best practices). A sitemap helps on top of links, as the sitemaps lesson showed, but it is a hint, and it gives a page none of what a link says about it.

Two measures come out of this. A page’s click depth: The fewest links a visitor or a crawler must follow from the home page to reach a page. A crawl measures it breadth first, visiting every page one click away before those two clicks away. is the fewest links that lead to it from the home page, and an orphan page: A page that no other page of the site links to. Crawlers can find it only through a sitemap or another site's link, and visitors only through search or a bookmark. is one that no page of the site links to. Both are found the way a crawler finds pages: start at the home page, visit everything one click away, then everything two clicks away, and so on. Here are ten of the bakery’s pages as letters: H the home page, C the cakes listing, B breads, O offers, M the mango mousse cake, T the eggless truffle, R the rasmalai cake, S the sourdough loaf, P the pav and G the eggless baking guide. Step through the walk from H; d is each page’s click depth:

Interactive · step through it Click depth of ten bakery pages, walked from the home page Visit nodes in order of distance from the start, with a queue.
Graph of 10 nodes and 11 edges. queue: H.BCGHd = 0MOPRSTqueue:Hvisit order:empty
  • being compared or looked at
  • being moved or written
  • in its final place
  • the key, pivot or target
  • visited or ruled out

Pseudo-code

  1. queue ← [start]; seen ← {start}; dist(start) ← 0
  2. while the queue is not empty:
  3. u ← dequeue
  4. for each neighbour v of u:
  5. if v is not seen: seen ← seen + v; dist(v) ← dist(u) + 1; enqueue v

Breadth-first search from H: visit the nodes in order of distance (in edges) from H, using a queue. d is that distance.

When it finishes: Visit order: H B C O S M R T P.

Visited 0
Edges checked 0

The walk gives the three listings depth 1, and the three cakes and the loaf depth 2. The pav is 3 clicks deep, because only the sourdough page links to it. And G is never reached: the guide links to the cakes and the truffle, but nothing links to it, so it is an orphan, which only a sitemap or another site’s link can bring to Google.

Checkpoint

Check: Discovery through links, depth and orphans

  1. Question 1 of 2 In the walk, G links to C and T, but no page links to G. What is G, and how can Google find it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: An orphan, which Google can find only through a sitemap or a link from another site

    Link a page that matters from its hub and from related pages, so that the walk from the home page reaches it.

  2. Question 2 of 2 A new guide is in the sitemap, but no page of the site links to it. What is it missing?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A link from at least one page of the site, which Google asks for and which also says what the guide is about

    The guide in the bakery's crawl got two links, from the cakes hub and the blog, and stopped being an orphan.

Addresses should be readable words, built to last

Google’s guide to URL structure asks for addresses that people can read (URL structure):

Prefer Avoid Why
Readable words: /cakes/mango-mousse-cake Long ID numbers: /p?id=48213 Google asks for “readable words rather than long ID numbers”
Hyphens between words Underscores, or words run together Hyphens help people and search engines see the separate words
One case for every address /Cakes/ and /cakes/ both answering Addresses are case-sensitive, so the two are different addresses
The words customers search with, transliterated where they use them Words only the business uses Google asks for words in the address that may be useful to people
Real paths for different content #/cakes fragments Google generally does not support fragments
Few parameters, joined with & Parameters that change nothing Each parameter can multiply the addresses of one page

Non-ASCII letters in links are percent-encoded, as the URL standard requires (RFC 3986), so मोदक in an address travels as %E0%A4%AE…; a romanised modak is what most customers type. Folders help as well: on a site with more than a few thousand addresses, grouping similar pages in folders can help Google learn how often each folder changes, and the address can show in results as a breadcrumb. The words themselves matter less than many think: Google’s starter guide says keywords in a domain or path have “hardly any effect beyond appearing in breadcrumbs” (SEO Starter Guide).

What matters most is that an address lasts. Every change costs a redirect, as the previous lesson showed, so an address should hold only what will stay true: no year in an evergreen page, no size that is really an option, no category the product may leave. The program below builds the slug: The readable last part of an address that names its page, such as mango-mousse-cake in /cakes/mango-mousse-cake: lower-case words joined by hyphens., the readable last part of each address, for 20 of the bakery’s products, with Python’s Unicode database taking the accents off (unicodedata):

Slugs for 20 product names, and the ones to decide Python · slugs/make_slugs.py
"""Turn 20 of Pune Bakes' product names into address slugs, and flag the ones that need a person's decision.

A slug is the last part of an address: /cakes/mango-mousse-cake. The rules follow Google's URL structure guide:
readable words in lower case, separated by hyphens. Letters outside a to z are dropped after accents are taken off,
so a name written only in Devanagari needs the romanised name its customers type, kept in ROMANISED.
"""

import re
import unicodedata

NAMES = [
    "Mango Mousse Cake", "Eggless Chocolate Truffle", "Rasmalai Cake", "Red Velvet Heart (1 kg)", "Crème Brûlée Tart",
    "Butterscotch Cake – Eggless", "Coffee & Walnut Cake", "Blueberry Cheesecake", "Black Forest Mini",
    "Pineapple Pastry", "Ajwain Biscuits", "Nankhatai", "Sourdough Loaf", "Pav (6 pieces)", "मोदक", "पुरणपोळी",
    "Shrikhand Cups", "Mango mousse cake!", "Diwali Hamper 2026", "Kaju_Katli Box",
]
ROMANISED = {"मोदक": "Modak", "पुरणपोळी": "Puran Poli"}  # the names customers type in Latin letters


def slug(name):
    text = unicodedata.normalize("NFKD", name.replace("&", " and ").lower())
    text = "".join(c for c in text if not unicodedata.combining(c))  # crème -> creme
    return "-".join(re.findall(r"[a-z0-9]+", text))


seen = {}
for name in NAMES:
    s, notes = slug(name), []
    if not s:
        s = slug(ROMANISED[name])
        notes.append(f"no Latin letters: built from {ROMANISED[name]!r}")
    if re.search(r"\b(19|20)\d\d\b", name):
        notes.append("a year: will it still be true next year?")
    if re.search(r"\d+\s*(kg|g|pieces)\b", name):
        notes.append("a size: is it part of the product, or an option?")
    if s in seen:
        notes.append(f"the same slug as {seen[s]!r}")
    seen.setdefault(s, name)
    print(f"{s:<30} {name}" + "".join(f"\n{'':<30}   {n}" for n in notes))
print(f"\n{len(NAMES)} names, {len(seen)} different slugs")

Output

mango-mousse-cake              Mango Mousse Cake
eggless-chocolate-truffle      Eggless Chocolate Truffle
rasmalai-cake                  Rasmalai Cake
red-velvet-heart-1-kg          Red Velvet Heart (1 kg)
                                 a size: is it part of the product, or an option?
creme-brulee-tart              Crème Brûlée Tart
butterscotch-cake-eggless      Butterscotch Cake – Eggless
coffee-and-walnut-cake         Coffee & Walnut Cake
blueberry-cheesecake           Blueberry Cheesecake
black-forest-mini              Black Forest Mini
pineapple-pastry               Pineapple Pastry
ajwain-biscuits                Ajwain Biscuits
nankhatai                      Nankhatai
sourdough-loaf                 Sourdough Loaf
pav-6-pieces                   Pav (6 pieces)
                                 a size: is it part of the product, or an option?
modak                          मोदक
                                 no Latin letters: built from 'Modak'
puran-poli                     पुरणपोळी
                                 no Latin letters: built from 'Puran Poli'
shrikhand-cups                 Shrikhand Cups
mango-mousse-cake              Mango mousse cake!
                                 the same slug as 'Mango Mousse Cake'
diwali-hamper-2026             Diwali Hamper 2026
                                 a year: will it still be true next year?
kaju-katli-box                 Kaju_Katli Box

20 names, 19 different slugs

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 make_slugs.py

Most names needed nothing, and Crème Brûlée Tart became creme-brulee-tart. The two Devanagari names have no Latin letters left, so their slugs come from the names customers type, modak and puran-poli. Two names carry a size and one a year, each a question for a person, and the second mango mousse cake would collide with the first, the duplicate of the canonical lesson waiting to happen.

Slug Generator Turn your own titles into slugs, with accents, symbols and stop words handled the same way.

Checkpoint

Check: Readable addresses that last

  1. Question 1 of 2 Which slug follows Google's advice for separating words?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: mango-mousse-cake

    The slug program also turned Kaju_Katli Box into kaju-katli-box.

  2. Question 2 of 2 A sweet is named only in Devanagari, मोदक. Its customers type modak when they search. What should its slug be?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: modak, the romanised name customers type

    A Devanagari slug is possible too, percent-encoded in every link; choose by the words your customers use.

Google generally crawls only <a> elements with an href attribute that resolves to a real address, and may skip a <span href>, an <a> with only an onclick, or an href="javascript:…" (link best practices). A link added by JavaScript is fine, as long as it is the same markup. The anchor text: The visible words of a link, or the alt text of a linked image. It tells people and Google what the target page is about, so it should name the target rather than say click here., the words inside the link, tells people and Google what the target is about, so Google asks for anchors that are “descriptive, reasonably concise, and relevant”, warns against “click here” and “read more”, uses a linked image’s alt text as its anchor, and calls keyword stuffing a violation of its spam policies (spam policies). The words around a link matter too, and links chained one after another with nothing between them say less than links in sentences. The program below reads one of the bakery’s pages and reports every element that looks like a link:

Eleven link-like elements, checked Python · links/audit_links.py
"""Which of the links on a Pune Bakes page can Google follow, and what do their words say?

Google generally crawls only <a> elements with an href that resolves to an address; it may skip the rest. The anchor
text, or the alt text of a linked image, tells people and Google what the target is about. The program reads the
page's HTML with Python's parser and reports each link-like element.
"""

from html.parser import HTMLParser

PAGE = """
<nav>
  <a href="/cakes/">Cakes</a>
  <a href="/cakes/eggless/">Eggless cakes in Pune</a>
  <span href="/breads/">Breads</span>
  <button onclick="go('/cookies/')">Cookies</button>
  <a onclick="go('/hampers/')">Hampers</a>
  <a href="javascript:openMenu('offers')">Offers</a>
</nav>
<main>
  <p>Our mango mousse sold out by noon. <a href="/cakes/mango-mousse">Click here</a> to order one.</p>
  <a href="/cakes/eggless-truffle"><img src="/img/truffle.jpg" alt="Eggless chocolate truffle cake"></a>
  <a href="/blog/bakers-notes-12"><img src="/img/oven.jpg" alt=""></a>
  <a href="/cakes/">best cake shop pune best cakes pune cheap cakes pune order cake pune</a>
  <a href="/cakes/rasmalai-cake">Rasmalai cake</a>
</main>
"""
GENERIC = {"click here", "read more", "here", "more", "this page", "link"}


class Links(HTMLParser):
    def __init__(self):
        super().__init__()
        self.found, self.current = [], None

    def handle_starttag(self, tag, attrs):
        a = dict(attrs)
        if tag == "a" or "href" in a or "onclick" in a:
            self.current = {"tag": tag, "href": a.get("href"), "onclick": "onclick" in a, "text": "", "alt": None}
            self.found.append(self.current)
        elif tag == "img" and self.current is not None:
            self.current["alt"] = a.get("alt", "")

    def handle_endtag(self, tag):
        if self.current and tag == self.current["tag"]:
            self.current = None

    def handle_data(self, data):
        if self.current:
            self.current["text"] += data


def verdict(link):
    href = link["href"] or ""
    if link["tag"] != "a" or not href or href.startswith("javascript:"):
        why = f"<{link['tag']}>" if link["tag"] != "a" else ("no href" if not href else "a javascript: href")
        return f"not a link Google can follow ({why})"
    words = " ".join(link["text"].split()) or (link["alt"] or "")
    if not words:
        return f"{href}: no anchor text and an empty alt"
    if words.lower() in GENERIC:
        return f'{href}: "{words}" says nothing about the target'
    if len(words.split()) > 8:
        return f'{href}: {len(words.split())} words of keywords, not a description'
    return f'{href}: "{words}"' + (" (the image's alt text)" if not link["text"].strip() else "")


parser = Links()
parser.feed(PAGE)
for link in parser.found:
    print(verdict(link))

Output

/cakes/: "Cakes"
/cakes/eggless/: "Eggless cakes in Pune"
not a link Google can follow (<span>)
not a link Google can follow (<button>)
not a link Google can follow (no href)
not a link Google can follow (a javascript: href)
/cakes/mango-mousse: "Click here" says nothing about the target
/cakes/eggless-truffle: "Eggless chocolate truffle cake" (the image's alt text)
/blog/bakers-notes-12: no anchor text and an empty alt
/cakes/: 13 words of keywords, not a description
/cakes/rasmalai-cake: "Rasmalai cake"

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 audit_links.py

Four of the six menu items are not links Google can follow: the breads span, the cookies button, the hampers a without an href and the offers javascript: link all work in a browser and lead nowhere for a crawler. Of the page’s own links, “Click here” names nothing, the oven photo has an empty alt, and a 13-word anchor of town and cake names is keyword stuffing; the truffle photo’s alt text and “Rasmalai cake” are what anchors should be.

A menu made of buttons

A redesign builds the menu from <button onclick="go('/cookies/')">Cookies</button>, and the cookie pages slowly drop out of Google. The button works for visitors, but it is not a link. Write <a href="/cookies/">Cookies</a>, and let a script add any effect on top of the real link.

Checkpoint

Check: Crawlable links and anchor text

  1. Question 1 of 2 Can Google follow this menu item?

    Read the code, then choose one answer.

    <span href="/breads/">Breads</span>
    Show the answer: Question 1 of 2

    Answer: No; Google generally follows only a elements with an href

    Write <a href="/breads/">Breads</a>; styling and scripts can sit on top of the real link.

  2. Question 2 of 2 Which anchor text is best for a link to the rasmalai cake page?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Rasmalai cake

    Read the anchor on its own: if it does not tell you where the link goes, it does not tell Google either.

Measure click depth and orphans, then fix them with hubs, page numbers and breadcrumbs

A crawl measures a whole site the way the walk above measured ten pages. The program below builds the links of the bakery’s 300 pages as a crawl finds them, walks them from the home page, compares the pages it reached with the sitemap, and counts the anchors used most, first as the site is today and then after three fixes:

Click depth, orphans and anchors of 300 pages, before and after Python · crawl/site_depth.py
"""Click depth, orphans and anchor text on Pune Bakes' 300 pages, before and after three fixes.

links() builds the links a crawl of the site finds: (from page, to page, anchor text). The program walks them
breadth first from the home page, so each page's depth is the fewest clicks that reach it, compares the pages it
reached with the 300 the sitemap lists, and counts the anchors used most.
"""

from collections import Counter, deque

FLAVOURS = ["mango", "chocolate", "rasmalai", "butterscotch", "pineapple", "black-forest", "red-velvet",
            "coffee-walnut", "blueberry", "strawberry", "vanilla", "pistachio"]
SECTIONS = {  # section -> its products
    "cakes": [f"{f}-{s}" for f in FLAVOURS for s in ["mousse", "truffle", "sponge", "cheesecake", "tart",
                                                       "cupcakes", "roll", "pastry", "loaf", "gateau"]],
    "breads": [f"{g}-{s}" for g in ["wheat", "multigrain", "sourdough", "ragi", "jowar", "oat", "rye", "milk"]
               for s in ["loaf", "buns", "pav", "rolls", "baguette"]],
    "cookies": [f"{f}-{s}" for f in ["ajwain", "jeera", "nankhatai", "coconut", "cashew", "almond", "oats", "choco"]
                for s in ["box", "jar", "tin", "pack", "bag"]],
    "hampers": [f"{o}-{s}" for o in ["diwali", "birthday", "wedding", "corporate"]
                for s in ["mini", "small", "medium", "large", "grand"]],
}
TOP = ["/cakes/", "/breads/", "/cookies/", "/hampers/", "/blog/", "/about/", "/contact/", "/offers/", "/delivery/",
       "/faq/", "/privacy/", "/terms/"]
POSTS = [f"/blog/bakers-notes-{n:02}" for n in range(1, 57)]  # 56 posts, newest first
ORPHANS = ["/guides/eggless-baking", "/offers/diwali-hampers", "/cakes/mango-mousse-1kg"]
PER_PAGE = {"cakes": 30, "blog": 10}  # listings longer than this are split into pages


def title(path):
    return path.rstrip("/").rsplit("/", 1)[-1].replace("-", " ").capitalize()


def listing(base, items, per_page, anchor, numbered):
    """Links of a listing split into pages of per_page items: 'Next page' only, or every page by number."""
    pages = [base] + [f"{base}?page={n}" for n in range(2, -(-len(items) // per_page) + 1)]
    for i, page in enumerate(pages):
        for item in items[i * per_page:(i + 1) * per_page]:
            yield page, item, anchor(item)
        if numbered:
            yield from ((page, other, f"Page {j + 1}") for j, other in enumerate(pages) if other != page)
        elif i + 1 < len(pages):
            yield page, pages[i + 1], "Next page"


def links(fixed):
    yield from (("/", page, title(page)) for page in TOP)
    for section, slugs in SECTIONS.items():
        items = [f"/{section}/{slug}" for slug in slugs]
        yield from listing(f"/{section}/", items, PER_PAGE.get(section, 100), title, fixed)
        for i, item in enumerate(items):  # each product links to the next two of its section
            yield from ((item, items[(i + k) % len(items)], title(items[(i + k) % len(items)])) for k in (1, 2))
    read = title if fixed else (lambda post: "Read more")
    yield from listing("/blog/", POSTS, PER_PAGE["blog"], read, fixed)
    yield from ((post, "/cakes/", "Order a cake") for post in POSTS)
    yield from ((orphan, "/cakes/", "Cakes") for orphan in ORPHANS)
    if fixed:  # the guide gets links from two hubs; the other two left the sitemap with a 301
        yield from (("/cakes/", ORPHANS[0], "Eggless baking guide"), ("/blog/", ORPHANS[0], "Eggless baking guide"))


def report(name, fixed):
    graph, anchors = {}, Counter()
    for source, target, anchor in links(fixed):
        graph.setdefault(source, []).append(target)
        anchors[anchor] += 1
    sitemap = {"/"} | set(graph) | {t for ts in graph.values() for t in ts}
    if fixed:
        sitemap -= set(ORPHANS[1:])
    depth, queue = {"/": 0}, deque(["/"])
    while queue:  # breadth first: every page gets the fewest clicks from the home page
        page = queue.popleft()
        for target in graph.get(page, []):
            if target not in depth:
                depth[target] = depth[page] + 1
                queue.append(target)
    orphans = sorted(sitemap - set(depth))
    print(f"{name}: {len(sitemap)} pages in the sitemap, {len(sitemap) - len(orphans)} reached from the home page")
    by_depth = Counter(depth.values())
    print("  clicks from home: " + ", ".join(f"{d}: {by_depth[d]}" for d in sorted(by_depth)))
    deepest = max(depth.values())
    print(f"  deepest, {deepest} clicks: {sum(1 for d in depth.values() if d == deepest)} pages, such as "
          f"{min(p for p, d in depth.items() if d == deepest)}")
    print(f"  orphans: {len(orphans)}" + "".join(f"\n    {p}" for p in orphans))
    print("  anchors used most: " + ", ".join(f'"{a}" {n}' for a, n in anchors.most_common(3)))


report("Today", fixed=False)
print()
report("After the fixes", fixed=True)

Output

Today: 300 pages in the sitemap, 297 reached from the home page
  clicks from home: 0: 1, 1: 12, 2: 142, 3: 42, 4: 42, 5: 41, 6: 11, 7: 6
  deepest, 7 clicks: 6 pages, such as /blog/bakers-notes-51
  orphans: 3
    /cakes/mango-mousse-1kg
    /guides/eggless-baking
    /offers/diwali-hampers
  anchors used most: "Read more" 56, "Order a cake" 56, "Next page" 8

After the fixes: 298 pages in the sitemap, 298 reached from the home page
  clicks from home: 0: 1, 1: 12, 2: 149, 3: 136
  deepest, 3 clicks: 136 pages, such as /blog/bakers-notes-11
  orphans: 0
  anchors used most: "Order a cake" 56, "Page 2" 8, "Page 3" 8

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 site_depth.py

Today the blog’s chain of “Next page” links puts its six oldest posts 7 clicks deep and the cakes’ chain puts the last 30 cakes 5 deep, 3 pages are orphans, and “Read more”, used 56 times, shares first place among the site’s anchors. The fixes that took every page to 3 clicks or fewer are all ordinary:

  • Numbered pages. Each listing page links every page of its listing by number, not only the next one. Google asks for <a href> links between the pages of a sequence, each page at its own address such as ?page=2 (pagination).
  • Titles as anchors. The blog links each post by its title instead of “Read more”.
  • A home for each orphan. The eggless baking guide is linked from the cakes hub and the blog; the old Diwali landing page and the 1kg variant left the sitemap with a 301 each, as the two previous lessons advised.

One long blog page

Press Edit, change the blog’s 10 in PER_PAGE to 56 and run again: every post moves up to 2 clicks, and the deepest pages are now the 30 cakes on the fourth listing page, at 5. One fix moves the problem; numbered pages solve it for both listings.

A hub page: A page that gathers the links to every page of one section or topic, such as a category listing, so that each of them is a click or two from the home page., such as a category listing, gathers the links of one section, and a breadcrumb: A short trail of links such as Home > Cakes > Mango mousse cake that shows where a page sits on a site. Marked up as BreadcrumbList, it can also show in Google's results. links each page back up the trail, Home > Cakes > Mango mousse cake. Marked up as BreadcrumbList, a breadcrumb can also show in Google’s results, which lists the rich result as available on desktop; Google asks that it represent “a typical user path to a page” rather than mirror the address, and a page may have more than one trail (breadcrumb). The cakes section after the fixes:

The home page links the cakes hub, whose four numbered pages link every cake and the guide; each cake links back by breadcrumb.Home0 clicksCakes, pages 1 to 4each links all four1 to 2 clicksMango mousse cake2 to 3 clicksTwo related cakesby nameEggless bakingguide, 2 clicksmenu: Cakesits nameits titlerelatedbreadcrumb

The cakes section after the fixes: a hub, numbered pages and breadcrumbs

Text description of the diagram

The diagram runs from top to bottom.

1. At the top is the home page, at 0 clicks. Its menu link "Cakes" leads to the cakes hub. 2. The hub is the cakes listing, split into pages 1 to 4, and each page links all four by number, so every page of the listing is 1 or 2 clicks from the home page. 3. From the hub, each cake is linked by its own name: the mango mousse cake is 2 or 3 clicks from the home page. The hub also links the eggless baking guide, which had no links before, so it is now 2 clicks from home. 4. The mango mousse cake links two related cakes by their names, and a dashed arrow, its breadcrumb, leads back to the cakes hub.

Every cake in the drawing is two or three clicks from home and the guide is two, while the dashed breadcrumb takes each cake one click back up to its hub. The crawl program leaves breadcrumbs out, and its depths would not change with them: a link to a page that is already nearer the home page changes no page’s depth.

Website SEO Crawler & Site Audit Crawl your own site from its home page and see each page's click depth, its inlinks and the sitemap addresses no page links to.

Checkpoint

Check: Fixing deep and orphan pages

  1. Question 1 of 2 The blog's oldest posts sit 7 clicks deep, behind five "Next page" links. Which change brings them within 3 clicks?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Link every listing page from every other by number, and link each post by its title

    The crawl program measured it: after the fixes no page of the 298 was more than 3 clicks from home.

  2. Question 2 of 2 The rasmalai cake is in the eggless and the birthday lists. What should its breadcrumb show?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A typical path a visitor takes to it; the markup may list both trails

    Home > Cakes > Eggless > Rasmalai cake and Home > Cakes > Birthday > Rasmalai cake can both be marked up.

Filters, sort orders and page numbers: decide which addresses deserve to exist

The bakery’s cake listing has four filters, flavour, egg, weight and occasion, and three sort orders. Each choice is a parameter in the address, and the addresses multiply. The program below counts them:

How many addresses one listing can make Python · facets/count_urls.py
"""How many addresses can Pune Bakes' cake listing make from four filters and a sort order?

Each parameter is either left out or set to one of its values, so a fixed order of parameters gives the product of
(values + 1) over the parameters. If the site accepts the parameters in any order, every order of the same choices is
another address; if flavours can be combined, each flavour becomes on or off.
"""

from itertools import combinations
from math import factorial, prod

CAKES = 120
PARAMS = {"flavour": 12, "egg": 2, "weight": 4, "occasion": 6, "sort": 3}  # values of each, besides "left out"

fixed = prod(n + 1 for n in PARAMS.values())
any_order = sum(prod(PARAMS[p] for p in chosen) * factorial(k)
                for k in range(len(PARAMS) + 1) for chosen in combinations(PARAMS, k))
combined = fixed // (PARAMS["flavour"] + 1) * 2 ** PARAMS["flavour"]

print(f"{CAKES} cakes, one listing, {len(PARAMS)} parameters")
print(f"  one value each, in a fixed order:   {fixed:>9,} addresses")
print(f"  one value each, in any order:       {any_order:>9,} addresses")
print(f"  flavours combinable, fixed order:   {combined:>9,} addresses")
print(f"  that is {combined // CAKES:,} listing addresses for every cake")

Output

120 cakes, one listing, 5 parameters
  one value each, in a fixed order:       5,460 addresses
  one value each, in any order:         270,044 addresses
  flavours combinable, fixed order:   1,720,320 addresses
  that is 14,336 listing addresses for every cake

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 count_urls.py

One listing of 120 cakes makes 5,460 addresses with one value a parameter in a fixed order, 270,044 if the parameters may come in any order, and 1,720,320 if flavours can be combined. Google’s guide to faceted navigation: Filters and sort orders on a listing, such as flavour, size and price, each choice of which makes a new address. Left unchecked, they make far more addresses than the site has products. calls the result an infinite URL space, which costs twice: crawlers fetch many addresses before they learn that they are worthless, and that work slows the discovery of new pages (faceted navigation). It offers two ways out:

Approach What it takes
Keep filter addresses out of crawling A robots.txt rule for the filter parameters, while every product and one unfiltered listing stay crawlable
Make filter addresses crawl-friendly & between parameters, one fixed order of filters, no repeated filter, and a 404 for a combination that finds nothing

Google calls rel="canonical" and nofollow on filter links less effective in the long term than either. For page numbers, the same pagination guide adds three rules: give each page its own address, do not name page 1 as the canonical of the others, since each page should have its own, and do not put page numbers in fragments, which Google ignores. Google no longer uses rel="next" and rel="prev". A sort order that only rearranges the same items is a copy of its listing: it names the listing as its canonical, as the canonical lesson advised for sorted lists, or the pagination guide’s noindex or robots.txt rule keeps it out of the index.

Key idea

Decide which addresses deserve to exist before a crawler finds them all: the products, one listing per section, and the few filtered pages people search for; everything else stays out of crawling, names its listing as canonical or answers 404.

Checkpoint

Check: Filters, sort orders and page numbers

  1. Question 1 of 2 What canonical should /cakes/?page=2 carry?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Its own address, /cakes/?page=2

    Each page of a listing is a page of its own, linked to the others with a href links.

  2. Question 2 of 2 The shop answers both ?flavour=mango&egg=no and ?egg=no&flavour=mango with the same list. What does Google's faceted navigation guide ask of crawlable filters?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: One fixed order of filters, joined with &, so that one choice has one address

    The count showed the cost: 5,460 addresses in one fixed order, 270,044 in any order.

Spice Box’s 60 pages all sit at the root of the site, and its home page links only ten of them. Write address() and link_plan() so that every page gets a clean folder address and is two or three clicks from the home page; the sample tests rebuild the link graph from your plan and check depth, orphans, broken links and anchors:

Then practise the five ideas of this lesson, the weakest first:

Practice that adapts to you

Practise this lesson

15 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 15 How does Google find most of the new pages it discovers?
    Show the answer: Question 1 of 15

    Answer: Through links on pages it already knows

    That is why the links of a site decide which of its pages are found, and how soon.

  2. Question 2 of 15 In the walk of ten pages, how many clicks from the home page H is the pav, P?

    Type a number.

    A hint

    Follow H to B, B to S, S to P.

    Show the answer: Question 2 of 15

    Answer: 3 clicks

    The only page that links to P is the sourdough page S, which is 2 clicks from H, so P is 3.

  3. Question 3 of 15 Why is click depth measured breadth first, visiting every page one click away before those two clicks away?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: Because it gives every page the fewest clicks that reach it

    The AlgoViz figure shows the queue: every page at depth 1 is visited before any page at depth 2.

  4. Question 4 of 15 What does Google's starter guide say keywords in a domain or path do?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: Hardly anything beyond appearing in breadcrumbs

    Write readable addresses for people and breadcrumbs, and keep them stable.

  5. Question 5 of 15 A hamper sold every Diwali has the slug diwali-hamper-2026. What is the problem with it?

    Choose one answer.

    Show the answer: Question 5 of 15

    Answer: Next year the address is out of date, and changing it costs a redirect

    The slug program flagged it for a person to decide, along with two sizes that may be options.

  6. Question 6 of 15 The shop's server answers /Cakes/ and /cakes/ with the same listing, and links use both. What do you change?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: Choose lower case, redirect /Cakes/ to /cakes/ and use /cakes/ in every link

    The canonical lesson treated /cakes/Mango-Mousse the same way: a 301 to the one spelling the site uses.

  7. Question 7 of 15 Which of these four elements can Google follow?

    Read the code, then choose one answer.

    <!-- A --> <a href="/cookies/" onclick="track('menu')">Cookies</a>
    <!-- B --> <a onclick="go('/cookies/')">Cookies</a>
    <!-- C --> <button onclick="go('/cookies/')">Cookies</button>
    <!-- D --> <a href="javascript:go('/cookies/')">Cookies</a>
    Show the answer: Question 7 of 15

    Answer: A

    Google's guide recommends an href with an onclick, and not an onclick without an href.

  8. Question 8 of 15 A product photo is a link with no text around it. What does Google use as its anchor?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: The image's alt text

    The truffle photo's alt text named its target; the oven photo's empty alt left its link without words.

  9. Question 9 of 15 An agency proposes anchors such as "best cake shop pune cheap cakes pune" on every internal link to the cakes listing. What do you answer?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: No; keyword stuffing violates Google's spam policies, and a short anchor such as Cakes says more

    The link checker flagged the 13-word anchor on the bakery's page for the same reason.

  10. Question 10 of 15 What is wrong with "Read more" as the anchor of the blog's 56 links to its posts?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: It says nothing about any of the 56 posts it leads to

    After the fixes the blog linked each post by its title, and "Read more" left the list of most used anchors.

  11. Question 11 of 15 The crawl found three orphans, a useful guide, an old campaign page and a 1kg variant of a cake. What does each get?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: Links for the guide; a 301 for the campaign page and the variant, which leave the sitemap

    The crawl after the fixes found 298 pages in the sitemap and no orphans.

  12. Question 12 of 15 Putting all 56 posts on one blog page brings them to 2 clicks. Why did the bakery still choose numbered pages?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: The cakes listing stayed 5 clicks deep, while numbered pages fix both listings and keep each page short

    Changing PER_PAGE moved the problem; numbered links solved it for every listing.

  13. Question 13 of 15 How many addresses can the bakery's cake listing make with one value a parameter, in a fixed order?

    Type a number.

    A hint

    Multiply (values + 1) over the five parameters.

    Show the answer: Question 13 of 15

    Answer: 5460 addresses

    13 × 3 × 5 × 7 × 4 = 5,460, for 120 cakes, before parameters in any order or combined flavours multiply it further.

  14. Question 14 of 15 The combination eggless, durian, 3 kg finds no cakes. If filter addresses are crawlable, what should it answer?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: 404, at that same address

    Keep the 404 at the address that found nothing: Google's guide asks not to redirect such combinations to a common "not found" page.

  15. Question 15 of 15 A developer adds rel="next" and rel="prev" to the listing pages and removes the visible page links to save space. What happens?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: Google no longer uses those tags, and without a href links the later pages and their items are found late or not at all

    Keep real links between the pages; other search engines may still read the tags, which do no harm alongside them.

Interview questions

Warm-up (fresher to 2–5 years): what is an orphan page, and why does it matter?

An orphan page is a page that no other page of the site links to. Google finds most new pages by following links, so an orphan can be found only through a sitemap or another site’s link, and the sitemap is a hint, not a promise. Visitors cannot reach it either, except from a search result or a bookmark. Google’s link guide asks that every page you care about have a link from at least one other page of your site. A crawl finds orphans by comparing the pages its walk from the home page reached with the pages the sitemap or the database lists. The fix depends on the page: link a page that matters from its hub and from related pages, redirect one that has a better replacement, and remove one that should not exist, along with its sitemap entry.

Two more questions follow, about a blog whose old posts are buried and a shop that wants new addresses for its keywords. Make the first decision of each before you open it:

Checkpoint

Your first decision

  1. Question 1 of 2 Interview brief: a blog of 2,000 posts lists them only in a paginated archive, 10 a page, newest first, and its old posts get no search traffic. What do you look at first?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The click depth of the old posts and the links that lead to them, from a crawl

    Then add numbered pages, topic hubs and related-post links, so that every post is a few clicks from home.

  2. Question 2 of 2 Interview brief: a shop wants to replace its addresses like /p/48213 with /cakes/mango-mousse-cake to rank better. What do you weigh first?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The gain against the cost of 301s for every address, since keywords in a path have hardly any effect on ranking

    If the change is made, it is made once, with one 301 for every address and every internal link updated.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A blog of 2,000 posts lists them only in a paginated archive, 10 a page, newest first, behind "Older posts" links. Its old posts get no search traffic, and many are "Discovered - currently not indexed". What do you change?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    A shop wants to replace its product addresses, such as /p/48213, with readable ones, such as /cakes/mango-mousse-cake, "to rank better". Is it worth it, and how would you do it?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Google finds most new pages through links, so every page that matters needs a link from another page of the site; click depth and orphans are measured by walking the links from the home page.
  • An address is readable words in one case joined by hyphens, in folders that group related pages, and holds nothing that will change, because every change costs a redirect.
  • Google follows <a> elements with an href, and their anchor text should name the target in a few words, never “click here” or a string of keywords.
  • Numbered pages, titles as anchors, hubs and breadcrumbs bring deep pages within a few clicks, and each orphan is linked, redirected or removed.
  • Filters and sort orders multiply addresses into the hundreds of thousands, so a site decides which deserve to be crawled and keeps the rest out with robots.txt or a 404.

Terms in this lesson

anchor text
The visible words of a link, or the alt text of a linked image. It tells people and Google what the target page is about, so it should name the target rather than say click here.
breadcrumb
A short trail of links such as Home > Cakes > Mango mousse cake that shows where a page sits on a site. Marked up as BreadcrumbList, it can also show in Google's results.
click depth
The fewest links a visitor or a crawler must follow from the home page to reach a page. A crawl measures it breadth first, visiting every page one click away before those two clicks away.
faceted navigation
Filters and sort orders on a listing, such as flavour, size and price, each choice of which makes a new address. Left unchecked, they make far more addresses than the site has products.
hub page
A page that gathers the links to every page of one section or topic, such as a category listing, so that each of them is a click or two from the home page.
orphan page
A page that no other page of the site links to. Crawlers can find it only through a sitemap or another site's link, and visitors only through search or a bookmark.
slug
The readable last part of an address that names its page, such as mango-mousse-cake in /cakes/mango-mousse-cake: lower-case words joined by hyphens.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.