Internal Linking Strategy for Vue Sites · Nuxt SEO

-
-
-
-

[1.4K](https://github.com/harlan-zw/nuxt-seo)

[Nuxt SEO on GitHub](https://github.com/harlan-zw/nuxt-seo)

Learn SEO

Master search optimization

Nuxt

 Vue

-
-
-
-
-
-
-

-
-
-
-
-
-
-

-
-
-

-
-
-
-
-
-
-
-
-
-
-

-
-
-

-
-
-
-
-
-
-
-
-

1.
2.
3.
4.
5.

# Internal Linking Strategy for Vue Sites

How to build an internal linking architecture in Vue that distributes PageRank, improves crawl depth, and helps Google discover every page.

[![Harlan Wilton](https://avatars.githubusercontent.com/u/5326365?v=4)Harlan Wilton](https://x.com/harlan-zw)12 mins read Published Mar 23, 2026

What you'll learn

- Every page should be reachable within 3 clicks from the homepage; deeper pages get crawled less frequently and may never get indexed
- `<RouterLink>` renders a standard `<a>` tag and passes full PageRank, unlike JavaScript click handlers
- Hub and spoke architecture is the strongest pattern for topic clusters, connecting a pillar page to related child pages and back
- Orphan pages (zero internal links pointing to them) rarely get indexed, even when included in your sitemap
- Internal links are the [#1 discovery method](https://developers.google.com/search/podcasts/search-off-the-record/internal-linking) for Googlebot, confirmed by Google's Gary Illyes

Internal links are the single most controllable ranking factor on your site. A [study of 23 million internal links](https://zyppy.com/seo/internal-links/seo-study/) found that sites with diverse internal link structures saw a 40% increase in organic traffic. Google's Gary Illyes has called internal links the ["number one thing you can do"](https://developers.google.com/search/podcasts/search-off-the-record/internal-linking) to help Google discover and understand your site. Every link you place tells search engines "this page matters" and shares ranking power with the destination.

## [Why Internal Links Matter](#why-internal-links-matter)

Internal links serve three purposes for search engines: controlling crawl depth, distributing PageRank, and building topical authority.

### [Crawl Depth](#crawl-depth)

Googlebot follows links to discover pages. Pages linked from the homepage are crawled most frequently. Pages that require 4+ clicks to reach may be crawled weekly instead of daily, or skipped during a crawl budget cycle. [Increasing internal links to key pages can boost crawl frequency by over 20%](https://ahrefs.com/blog/internal-links-for-seo/).

One aggregator site [reduced crawl depth from 13 levels to under 3 clicks](https://www.kevin-indig.com/internal-linking-strategies-for-seo/) from the homepage and saw a 160% organic traffic lift over 12 months.

```
Homepage (depth 0)
├── /features (depth 1)         ← crawled daily
│   ├── /features/editor (depth 2)  ← crawled daily
│   │   └── /features/editor/plugins (depth 3) ← crawled weekly
│   │       └── /features/editor/plugins/markdown (depth 4) ← crawled rarely
```

_Illustrative data based on crawl depth studies. Pages beyond 3 clicks see a steep drop in indexation. Source: [Kevin Indig](https://www.kevin-indig.com/internal-linking-strategies-for-seo/), [Ahrefs](https://ahrefs.com/blog/internal-links-for-seo/)_

### [PageRank Distribution](#pagerank-distribution)

Every page on your site accumulates ranking power (PageRank). When a page links to another, it passes a portion of that power along. Pages with more internal links pointing to them accumulate more authority.

Google's [Reasonable Surfer patent](https://patents.google.com/patent/US7716216B2/en) reveals that not all links carry equal weight. Links are weighted by click probability: a link in body content is high-value, while a footer or boilerplate link is devalued. Place your most important links where users are likely to click them.

### [Topical Authority](#topical-authority)

When you link related pages together, Google understands they belong to the same topic cluster. A page about `/blog/vue-seo-guide` linking to `/blog/vue-meta-tags` and `/blog/vue-sitemaps` tells Google you have depth in Vue SEO.

Pages with [strong internal topical clustering are 40% more likely to be cited in AI Overviews](https://ahrefs.com/blog/serp-features-evolution-2025/), even as traditional Featured Snippets have declined. AI crawlers like GPTBot and ClaudeBot use internal link structures to [map site entities for better recall](https://growth-memo.com/p/internal-linking-grows-up-evolving-from-link-juice-to-entity-maps) in AI-generated answers.

**Comparison: pages with and without internal links**

| Metric | Pages With Internal Links | Orphan Pages |
| --- | --- | --- |
| Average time to first index | 4 days | 2+ weeks |
| Crawl frequency | Daily to weekly | Monthly or never |
| PageRank received | Proportional to link count | Near zero |
| Topical association | Strong cluster signals | Isolated, no context |
| Ranking potential | Full | Severely limited |

## [Hub and Spoke Architecture](#hub-and-spoke-architecture)

The hub and spoke model is the most effective internal linking pattern for content sites. A **hub page** (pillar page) serves as the central entry point for a topic. **Spoke pages** cover subtopics in depth. The hub links to every spoke, and every spoke links back to the hub and to sibling spokes.

A [SearchPilot A/B test](https://www.searchpilot.com/resources/case-studies/internal-linking-nearby-locations/) found that interlinking spoke pages (nearest neighbors) produced a statistically significant 7% traffic uplift.

Spoke

/meta-tags

Spoke

/sitemaps

Spoke

/canonicals

Spoke

/robots

Hub

/seo-guide

Hub ↔ Spoke

Spoke ↔ Spoke

_ Hub and spoke architecture. Solid lines carry the most PageRank. Dashed cross-links reinforce topical clustering. _### [Building a Hub Page in Vue](#building-a-hub-page-in-vue)

Use a composable or direct `fetch` call to load child pages from your API, then link to each with `<RouterLink>`:

pages/features/index.vue

```
<script setup lang="ts">
import { useSeoMeta } from '@unhead/vue'
import { onMounted, ref } from 'vue'

const features = ref<Array<{ path: string, title: string, description: string }>>([])

onMounted(() => {
  fetch('/api/pages?prefix=/features').then(r => r.json()).then(d => features.value = d)
})

useSeoMeta({
  title: 'Features Overview',
  description: 'Explore all features including color extraction, font detection, and performance analysis.'
})
</script>

<template>
  <div>
    <h1>Features</h1>
    <p>Everything our platform offers, organized by capability.</p>

    <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-8">
      <RouterLink
        v-for="feature in features"
        :key="feature.path"
        :to="feature.path"
        class="block p-6 border border-muted rounded-lg hover:bg-muted transition-colors"
      >
        <h2 class="text-lg font-semibold">
          {{ feature.title }}
        </h2>
        <p class="text-muted mt-2">
          {{ feature.description }}
        </p>
      </RouterLink>
    </div>
  </div>
</template>
```

Each spoke page should include a link back to the hub and to related spokes:

pages/features/color-extraction.vue

```
<script setup lang="ts">
import { useSeoMeta } from '@unhead/vue'

useSeoMeta({
  title: 'Color Extraction',
  description: 'Extract dominant colors from any image with our color extraction engine.'
})
</script>

<template>
  <article>
    <RouterLink to="/features" class="text-primary mb-4 inline-block">
      ← Back to all features
    </RouterLink>

    <h1>Color Extraction</h1>
    <p>Our color extraction engine identifies dominant colors...</p>

    <!-- Contextual links to sibling spokes -->
    <p>
      Color extraction pairs well with
      <RouterLink to="/features/font-detection">
        font detection
      </RouterLink>
      for complete design analysis. See how it integrates with our
      <RouterLink to="/features/performance-analysis">
        performance analysis
      </RouterLink>
      pipeline.
    </p>
  </article>
</template>
```

This pattern ensures every spoke page is reachable within 2 clicks from the homepage (homepage → hub → spoke), and the cross-links between spokes create a tightly connected cluster.

## [Implementing with RouterLink](#implementing-with-routerlink)

`<RouterLink>` is the correct way to create internal links in a Vue app. It renders a standard `<a href="...">` tag that search engines can follow, while also enabling client-side navigation for users.

### [Anchor Text Matters](#anchor-text-matters)

The clickable text inside your link (anchor text) tells Google what the destination page is about. The [Zyppy study](https://zyppy.com/seo/internal-links/seo-study/) found that anchor text diversity, including both keyword-rich and natural variations, correlated strongly with ranking improvements.

✅ Correct

❌ Wrong

```
<template>
  <p>
    See our guide on
    <RouterLink to="/blog/vue-meta-tags">
      configuring meta tags in Vue
    </RouterLink>
    for detailed implementation steps.
  </p>

  <p>
    The
    <RouterLink to="/features/color-extraction">
      color extraction feature
    </RouterLink>
    supports PNG, JPEG, and WebP formats.
  </p>
</template>
```

```
<template>
  <p>
    For meta tags in Vue,
    <RouterLink to="/blog/vue-meta-tags">
      this link
    </RouterLink>.
  </p>

  <p>
    Learn more about this feature
    <RouterLink to="/features/color-extraction">
      on this page
    </RouterLink>.
  </p>
</template>
```

"Click here" and "here" tell Google nothing about the target page. "Configuring meta tags in Vue" is a strong relevance signal.

### [Breadcrumb Navigation](#breadcrumb-navigation)

Breadcrumbs add internal links to every page in your hierarchy and provide structured data that Google displays in search results. Use `useHead` from `@unhead/vue` to inject the JSON-LD manually, alongside visible `<RouterLink>` breadcrumbs:

pages/features/color-extraction.vue

```
<script setup lang="ts">
import { useHead } from '@unhead/vue'

useHead({
  script: [
    {
      type: 'application/ld+json',
      innerHTML: JSON.stringify({
        '@context': 'https://schema.org',
        '@type': 'BreadcrumbList',
        'itemListElement': [
          { '@type': 'ListItem', 'position': 1, 'name': 'Home', 'item': 'https://example.com/' },
          { '@type': 'ListItem', 'position': 2, 'name': 'Features', 'item': 'https://example.com/features' },
          { '@type': 'ListItem', 'position': 3, 'name': 'Color Extraction' },
        ]
      })
    }
  ]
})
</script>

<template>
  <nav aria-label="Breadcrumb" class="text-sm text-muted mb-6">
    <ol class="flex gap-2">
      <li>
        <RouterLink to="/">
          Home
        </RouterLink>
      </li>
      <li>/</li>
      <li>
        <RouterLink to="/features">
          Features
        </RouterLink>
      </li>
      <li>/</li>
      <li class="text-dimmed">
        Color Extraction
      </li>
    </ol>
  </nav>
</template>
```

This generates both visible breadcrumb links and JSON-LD structured data that Google uses to render breadcrumb trails in search results.

## [Contextual Links in Content](#contextual-links-in-content)

Links placed within body content carry more weight than links in navigation or sidebars. Google's [Reasonable Surfer patent](https://patents.google.com/patent/US7716216B2/en) confirms that a link surrounded by relevant text is treated as an editorial endorsement, not a template element.

### [Related Articles Component](#related-articles-component)

Build a reusable component that renders related links from page data:

components/RelatedArticles.vue

```
<script setup lang="ts">
const { relatedPages = [] } = defineProps<{
  relatedPages?: Array<{ path: string, title: string }>
}>()
</script>

<template>
  <aside v-if="relatedPages.length" class="mt-12 p-6 bg-muted rounded-lg">
    <h2 class="text-lg font-semibold mb-4">
      Related Articles
    </h2>
    <ul class="space-y-2">
      <li v-for="page in relatedPages" :key="page.path">
        <RouterLink :to="page.path" class="text-primary hover:underline">
          {{ page.title }}
        </RouterLink>
      </li>
    </ul>
  </aside>
</template>
```

Use it in any page by passing the related pages data from your API response or static definition:

pages/blog/[slug].vue

```
<script setup lang="ts">
import { useSeoMeta } from '@unhead/vue'
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()
const post = ref<{ title: string, description: string, relatedPages: Array<{ path: string, title: string }> } | null>(null)

onMounted(() => {
  fetch(\`/api/blog/${route.params.slug}\`).then(r => r.json()).then(d => post.value = d)
})

useSeoMeta({
  title: () => post.value?.title,
  description: () => post.value?.description
})
</script>

<template>
  <article v-if="post">
    <div v-html="post.content" />
    <RelatedArticles :related-pages="post.relatedPages || []" />
  </article>
</template>
```

### [Markdown Content Links](#markdown-content-links)

When you render markdown content in Vue (using libraries like `marked`, `markdown-it`, or a CMS), standard markdown links become `<a>` tags in the rendered HTML output. These are crawlable by Googlebot like any other anchor tag.

content/blog/vue-seo-guide.md

```
## Setting Up Meta Tags

The foundation of any SEO strategy starts with proper meta tags.
See [configuring page titles](/learn-seo/vue/mastering-meta/titles) for
the complete guide.

If you're using dynamic routes, you'll also want to read about
[canonical URLs](/learn-seo/vue/controlling-crawlers/canonical-urls)
to prevent duplicate content.

## Sitemaps

Once your pages have proper meta tags, make sure Google can discover
them all through your [sitemap](/learn-seo/vue/controlling-crawlers/sitemaps).
```

Each of these markdown links becomes an `<a href="...">` tag in the rendered output, passing PageRank and providing crawl paths.

## [Navigation and Footer Links](#navigation-and-footer-links)

### [Site Navigation](#site-navigation)

Links in your main navigation appear on every page. This makes navigation links powerful for PageRank distribution: a page linked from the nav receives an internal link from every page on the site.

A [SearchPilot A/B test on footer links](https://www.searchpilot.com/resources/case-studies/internal-links-in-footer-seo/) showed that adding categorical footer links led to a 5% organic uplift for the recipient subcategory pages.

layouts/DefaultLayout.vue

```
<script setup lang="ts">
const navLinks = [
  { to: '/features', label: 'Features' },
  { to: '/pricing', label: 'Pricing' },
  { to: '/blog', label: 'Blog' },
  { to: '/docs', label: 'Documentation' },
]
</script>

<template>
  <div class="min-h-screen flex flex-col">
    <header class="border-b border-muted">
      <nav class="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
        <RouterLink to="/" class="text-xl font-bold">
          MySite
        </RouterLink>
        <div class="flex gap-6">
          <RouterLink
            v-for="link in navLinks"
            :key="link.to"
            :to="link.to"
            class="text-muted hover:text-default transition-colors"
          >
            {{ link.label }}
          </RouterLink>
        </div>
      </nav>
    </header>

    <main class="flex-1 max-w-7xl mx-auto px-4 py-8">
      <slot />
    </main>
  </div>
</template>
```

Keep your main navigation focused on 5 to 7 top-level links. Every additional link in the nav dilutes the PageRank each destination receives.

### [Navigation vs Footer Weight](#navigation-vs-footer-weight)

Both navigation and footer links appear on every page, but Google's [Reasonable Surfer patent](https://patents.google.com/patent/US7716216B2/en) assigns more weight to links that are prominent and likely to be clicked. Your main navigation links carry more authority per link than footer links. Use the nav for pages you want to rank highest, and the footer for completeness.

## [Internal Links and AI Search](#internal-links-and-ai-search)

AI crawlers (GPTBot, ClaudeBot, Google's AI systems) use internal links differently than traditional crawlers. Rather than counting links, they [build entity maps and semantic graphs](https://growth-memo.com/p/internal-linking-grows-up-evolving-from-link-juice-to-entity-maps) from your internal link structure. Your internal linking architecture determines how well AI systems understand the relationships between your content topics.

Google's [AI systems use internal link structures](https://developers.google.com/search/docs/crawling-indexing/google-extended) to build knowledge graphs for LLM-based responses. Pages with strong topical clustering are [40% more likely to be cited in AI Overviews](https://ahrefs.com/blog/serp-features-evolution-2025/).

What this means in practice: link related content together with descriptive anchor text. If you have a guide on meta tags, a guide on sitemaps, and a guide on canonical URLs, they should all link to each other. The anchor text should describe what the destination covers, not just say the page name.

## [Auditing Your Link Graph](#auditing-your-link-graph)

Vue apps do not have a built-in link checker module. The most reliable approaches are external crawl tools and CI-based scripts.

[Screaming Frog](https://www.screamingfrog.co.uk/seo-spider/) can crawl your deployed site and report every internal link, broken URL, and orphan page in a single audit. For automated CI checks, the [`broken-link-checker`](https://www.npmjs.com/package/broken-link-checker) [npm](https://npmjs.com) package can crawl a local dev server and fail the build on any broken links:

scripts/check-links.ts

```
import { SiteChecker } from 'broken-link-checker'

const checker = new SiteChecker(
  { excludeLinksMatchingFilter: /^mailto:/ },
  {
    link: (result) => {
      if (result.broken) {
        console.error(\`Broken: ${result.url.original} on ${result.base.resolved}\`)
        process.exitCode = 1
      }
    }
  }
)

checker.enqueue('http://localhost:4173')
```

Run this against your [Vite](https://vite.dev) preview server (`vite preview`) as part of your CI pipeline to catch broken links before deployment.

### [Checking Crawl Depth in Search Console](#checking-crawl-depth-in-search-console)

Google Search Console's Coverage report shows how Google discovers your pages. Pages that are "Discovered, currently not indexed" often have crawl depth issues. To check:

1. Open Google Search Console → Pages
2. Filter by "Not indexed" → "Discovered, currently not indexed"
3. Inspect individual URLs to see how Google found them
4. If Google found a page only through the sitemap (not through links), that page needs more internal links

### [Identifying Orphan Pages](#identifying-orphan-pages)

An orphan page has zero internal links pointing to it. The only way Google can find it is through the sitemap or external links. To identify orphan pages:

scripts/find-orphans.ts

```
import { readdir } from 'node:fs/promises'
import { resolve } from 'node:path'

// Collect all page paths from pages/ directory
async function getPagePaths(dir: string, prefix = ''): Promise<string[]> {
  const entries = await readdir(dir, { withFileTypes: true })
  const paths: string[] = []

  for (const entry of entries) {
    if (entry.isDirectory()) {
      paths.push(...await getPagePaths(resolve(dir, entry.name), \`${prefix}/${entry.name}\`))
    }
    else if (entry.name.endsWith('.vue')) {
      const route = \`${prefix}/${entry.name.replace('.vue', '').replace('index', '')}\`
      paths.push(route.replace(/\/+$/, '') || '/')
    }
  }

  return paths
}
```

For a thorough audit, crawl your site with [Screaming Frog](https://www.screamingfrog.co.uk/seo-spider/) or cross-reference your `broken-link-checker` report against all known routes.

## [Common Mistakes](#common-mistakes)

### [Orphan Pages](#orphan-pages)

The most common internal linking mistake is creating pages that no other page links to. This happens frequently with programmatic routes that generate hundreds of pages without corresponding navigation.

✅ Correct

❌ Wrong

```
<script setup lang="ts">
import { onMounted, ref } from 'vue'

// Hub page that links to all generated pages
const products = ref<Array<{ path: string, title: string }>>([])

onMounted(() => {
  fetch('/api/products').then(r => r.json()).then(d => products.value = d)
})
</script>

<template>
  <div>
    <h1>All Products</h1>
    <!-- Every product page is reachable -->
    <RouterLink
      v-for="product in products"
      :key="product.path"
      :to="product.path"
    >
      {{ product.title }}
    </RouterLink>
  </div>
</template>
```

```
<!-- No hub page exists -->
<!-- Product pages only reachable via sitemap.xml -->
<!-- pages/products/[slug].vue exists but nothing links to it -->
<template>
  <div>
    <h1>{{ product.title }}</h1>
    <!-- No link back to parent, no links to siblings -->
  </div>
</template>
```

### [JavaScript Navigation Without Links](#javascript-navigation-without-links)

Using `@click` handlers with `router.push()` instead of `<RouterLink>` creates links that Googlebot cannot follow. Googlebot parses HTML to find `<a href>` tags. It does not execute arbitrary click handlers.

✅ Correct

❌ Wrong

```
<template>
  <RouterLink to="/features/color-extraction" class="btn">
    View Color Extraction
  </RouterLink>
</template>
```

```
<template>
  <!-- Googlebot cannot follow this -->
  <button @click="router.push('/features/color-extraction')">
    View Color Extraction
  </button>
</template>
```

### [Excessive nofollow on Internal Links](#excessive-nofollow-on-internal-links)

Adding `rel="nofollow"` to internal links tells Google not to pass PageRank through that link. This wastes your own link equity. Only use nofollow on internal links in rare cases, such as linking to login pages or user-generated content that you do not want to endorse.

### [Linking to Noindexed Pages](#linking-to-noindexed-pages)

If you link to a page that has a `noindex` meta tag, the PageRank you send to that page is wasted. It accumulates on a page that will never appear in search results. Audit your internal links to ensure they point to indexable destinations.

### [Broken Links After URL Changes](#broken-links-after-url-changes)

When you rename or move pages, existing internal links break. A 404 response wastes the PageRank that the linking page was trying to pass. Set up server-side redirects when changing URLs. In an Express server:

server.ts

```
import express from 'express'

const app = express()

// 301 redirects for moved pages
app.get('/old-features/color', (req, res) => {
  res.redirect(301, '/features/color-extraction')
})
app.get('/blog/old-slug', (req, res) => {
  res.redirect(301, '/blog/new-slug')
})
```

In a Vite SSR setup, configure redirects in your server entry or via a `vite.config.ts` proxy for development. For production static hosts (Netlify, [Vercel](https://vercel.com), Cloudflare Pages), use their respective redirect configuration files (`_redirects`, `vercel.json`, `_routes.json`).

Better yet, run `broken-link-checker` in CI to catch broken links during development before they ship.

---

On this page

- [Why Internal Links Matter](#why-internal-links-matter)
- [Hub and Spoke Architecture](#hub-and-spoke-architecture)
- [Implementing with RouterLink](#implementing-with-routerlink)
- [Contextual Links in Content](#contextual-links-in-content)
- [Navigation and Footer Links](#navigation-and-footer-links)
- [Internal Links and AI Search](#internal-links-and-ai-search)
- [Auditing Your Link Graph](#auditing-your-link-graph)
- [Common Mistakes](#common-mistakes)

[GitHub](https://github.com/harlan-zw/nuxt-seo) [ Discord](https://discord.com/invite/275MBUBvgP)

###

-
-

Modules

-
-
-
-
-
-
-
-
-

###

-
-
-

###

Nuxt

-
-
-
-
-

Vue

-
-
-
-
-
-
-
-

###

-
-
-
-
-
-
-
-
-
-

Copyright © 2023-2026 Harlan Wilton - [MIT License](https://github.com/harlan-zw/nuxt-seo/blob/main/license) · [mdream](https://mdream.dev)