Making Client-Side Rendered Vue Apps Search Engine Friendly

Preramble: I have a lot of client-side rendered vue apps that I've built. I wanted to make them search engine friendly, but I didn't want to invest in a server-side rendered solution. I found a few articles on the subject, but they were either outdated or didn't cover all the bases. I thought I'd share my experience and what I learned.

So, You Built a Client-Side Rendered App...

Client-side rendered (CSR) apps are fantastic. They’re snappy, cheap to host, and a joy to build with frameworks like Vue 3. If you’re deploying to a static host like GitHub Pages, Netlify, or an S3 bucket, you probably chose CSR to keep things simple and affordable.

But then you check your analytics and realise Google isn't sending you much love. That's because, out of the box, search engine crawlers often see a blank page when they visit a CSR app. They don't always wait around for your JavaScript to load and render the content. The result? Poor visibility, low organic traffic, and a lot of wasted effort.

Don't worry, you don't need to scrap everything and switch to server-side rendering (SSR). You can absolutely make a CSR Vue site SEO-friendly. You just need to be a little strategic.

This guide walks through the essentials: dynamic metadata, sitemaps, semantic HTML, and more, with practical examples for Vue 3.


Keywords

Generally, when optimizing for search engines (ignoring client side rendering for a moment), one of the most foundational steps is making sure you are choosing the right keywords to optimize for. Search engines are getting better at understanding the content of a page, but it's still a good idea to make sure you are using the right keywords - and making sure your content is relevant to those keywords.

User Intent

Figuring out what your target audience is searching for; Keyword intent generally falls into categories like navigational, informational, transactional, and commercial. For example, if you are selling a product, you want to use keywords that are transactional or commercial in nature. If you are providing information, you want to use keywords that are informational in nature.

Keyword Research

You can use tools to understand your users better like Google Analytics or Google Search Console. You want to find a balance between what your users are searching for and what you are trying to optimize for. Search volume, Keyword difficulty, and relevance are all important factors to consider when choosing keywords.

Long-tail keywords

Long-tail keywords are a great way to target specific audiences. For example, if you are selling a product, you want to use keywords that are transactional in nature. If you are providing information, you want to use keywords that are informational in nature. For example, if you are selling a coffee machine, you want to use keywords like "buy coffee machine" or "coffee machine for sale." If you are providing information, you want to use keywords like "how to make coffee."

Competition

Google (or whatever search engine you are tackling) the keyword you are targeting and find out what your competition is doing. Answer questions such as:

  • What are the top 10 results for this keyword?
  • Are they using structured data?
  • Can you offer something unique or more value than your competition?
  • Are they forums, articles, or products?

Keyword Parity

I already touched on this but keywords are not just metadata, they are also used in the content of your page. Make sure you are using your target keywords in the content of your page. This is called keyword parity.

  • Titles
  • Meta Description
  • Headings (h1, h2, etc)
  • URL slugs
  • Alt text
  • Body text

This is super important, but it's a bit of an art. You want to avoid keyword stuffing. You want to use your target keywords naturally; don't repeat them too often.

Dynamic Metadata is Non-Negotiable

If every page on your site has the same <title> and <meta name="description">, search engines get confused. They might index your homepage title for every single URL, which is a recipe for SEO disaster. You need to update these tags dynamically as the user navigates your app. We'll be doing this in a dynamic way using javascript.

The best tool for the job is @vueuse/head.

Get it Installed

npm install @vueuse/head

Wire it up in main.js

import { createApp } from 'vue'
import { createHead } from '@vueuse/head'
import App from './App.vue'

const app = createApp(App)
const head = createHead()

app.use(head)
app.mount('#app')

Use It in Your Components

Now you can control the <head> of any component.

<script setup>
import { useHead } from '@vueuse/head'

// This can be driven by props, a store, or API data
useHead({
  title: 'The Best Coffee Shops in London',
  meta: [
    { name: 'description', content: 'A guide to the best coffee in London, handpicked by locals.' },
    { property: 'og:title', content: 'Top Coffee in London' },
    { property: 'og:type', content: 'article' }
  ]
})
</script>

Pro Tip: For cleaner code, manage metadata from a single place using Vue Router's navigation guards. This lets you define titles and descriptions right alongside your routes.


Give Search Engines a Map

A sitemap.xml file is your way of handing Google a list of all the pages on your site you want it to crawl. It’s a simple but crucial step.

Option 1: The Simple, Manual Sitemap

For a site with just a handful of static pages, you can write this file by hand or with a simple build script.

const fs = require('fs')

const urls = [
  '/', '/about', '/contact',
  '/blog/london-coffee', '/blog/bristol-coffee'
]

const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls.map(url => `
  <url>
    <loc>https://yourdomain.com${url}</loc>
    <changefreq>weekly</changefreq>
  </url>
`).join('')}
</urlset>`

fs.writeFileSync('./dist/sitemap.xml', sitemap)

Option 2: The Dynamic Approach

If your blog posts or products come from a headless CMS, your build script should fetch all the slugs from the API to generate the sitemap automatically.

const fs = require('fs')
const axios = require('axios')

async function generateSitemap() {
  const response = await axios.get('https://yourheadlesscms.com/api/posts')
  const posts = response.data

  const urls = posts.map(post => `/blog/${post.slug}`)

  const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls.map(url => `
  <url>
    <loc>https://yourdomain.com${url}</loc>
    <changefreq>weekly</changefreq>
  </url>
`).join('')}
</urlset>`

  fs.writeFileSync('./dist/sitemap.xml', sitemap)
}

generateSitemap()

Don't Forget: Add Sitemap: https://yourdomain.com/sitemap.xml to your robots.txt file and submit the sitemap directly to Google Search Console.


Write HTML for Machines, Not Just Humans

Think of it this way: clean, semantic HTML gives search bots a clear blueprint of your page. It helps them understand the hierarchy and importance of your content.

A Good Vue Template Structure

<template>
  <main>
    <article>
      <header>
        <h1>{{ post.title }}</h1>
        <p>Published on: {{ post.date }}</p>
      </header>

      <section v-html="post.content" />

      <footer>
        <p>Written by {{ post.author }}</p>
      </footer>
    </article>
  </main>
</template>

Quick Wins for Semantics:

  • Use <main>, <nav>, <article>, and <section> to define the layout.
  • Stick to one <h1> per page. This is your main headline.
  • Don't use a <div> when a more specific tag exists.
  • Make sure your site is navigable with a keyboard.
  • Use aria-label on navigation elements to give them unique, descriptive names.

Add Rich Results with JSON-LD

Want those fancy snippets, FAQs, and article carousels in Google search results? JSON-LD is how you get them. It's a small block of code that explicitly tells search engines what your content is about.

You can inject this script using @vueuse/head, just like your meta tags.

Example for a Blog Post

<script setup>
import { useHead } from '@vueuse/head'

const post = {
  title: 'The Best Coffee in Bristol',
  date: '2025-07-20',
  author: 'Alex Green'
}

useHead({
  script: [
    {
      type: 'application/ld+json',
      children: JSON.stringify({
        "@context": "https://schema.org",
        "@type": "BlogPosting",
        "headline": post.title,
        "datePublished": post.date,
        "author": {
          "@type": "Person",
          "name": post.author
        }
      })
    }
  ]
})
</script>

This tells Google: "This page is a blog post, here's the headline, the publication date, and the author."

Test It! Use Google’s Rich Results Test to make sure your markup is valid.


Links That Crawlers Can Actually Follow

This one is a classic mistake. Search bots follow href attributes on <a> tags. If your navigation is built entirely on buttons with @click events, crawlers will hit a dead end.

Don't Do This:

<!-- Bots can't see this as a link -->
<button @click="router.push('/about')">About</button>

Do This Instead:

<!-- This is perfect! It's a real link. -->
<router-link to="/about">About</router-link>

<!-- A standard anchor tag also works. -->
<a href="/about">About</a>

Vue Router is smart enough to intercept clicks on these links, so you get the speed of a single-page app while keeping your site crawlable.

Also: Use descriptive anchor text. Instead of "click here," write "read our guide to Bristol coffee." It provides context for both users and search engines.


Avoid Duplicate Content with Canonicals

Sometimes you might have multiple URLs that show the same content (e.g., with and without a trailing slash, or with tracking parameters). A canonical tag tells search engines which version is the "official" one to index.

useHead({
  link: [
    { rel: 'canonical', href: 'https://yourdomain.com/blog/london-coffee' }
  ]
})

This prevents duplicate content issues and consolidates your page authority to a single URL.


You Can't Fix What You Can't See

Once you've done the work, you need to monitor it. These tools are essential.

Tool What It's For
Google Search Console The source of truth for indexing and errors.
Lighthouse (Chrome DevTools) Quick audits for SEO, performance, and accessibility.
Screaming Frog SEO Spider Crawl your site like a bot to find issues.
PageSpeed Insights Performance directly impacts SEO rankings.

Check Google Search Console regularly, especially after a big update. It will tell you exactly how Google sees your site.


Your Quick SEO Checklist

  • Are your <title> and <meta> tags unique for every page?
  • Do you have a sitemap.xml submitted to search engines?
  • Is your HTML structured with semantic tags (<main>, <article>, etc.)?
  • Are you using <router-link> or <a> tags for all internal navigation?
  • Does important content have JSON-LD structured data?
  • Have you set canonical URLs for your main pages?
  • Is your site registered with Google Search Console?

It's All About Clarity

Getting a client-side rendered Vue app to rank well isn't magic. It's about giving search engines clear, unambiguous signals about your content. You don't always need a complex SSR setup to succeed.

Focus on these fundamentals:

  • Clean, semantic markup.
  • Dynamic and descriptive metadata.
  • A crawlable link structure.
  • A sitemap for easy discovery.

Make your content legible to machines, and you'll make it discoverable by humans.

Further Reading

  • Search Engine Optimization (SEO) Starter Guide
  • Understand the JavaScript SEO