There are 5.6 billion Google Searches a day

63,000 queries a second (avg.)

But how high is the percentage of websites that actually

receive traffic from Google?

Guess!

Not even 10 percent!

SEO in a Vue.js World ๐ŸŒ

VueConf US 2020

About me

Alexander Lichter

Nuxt.js Core Team Member

@TheAlexLichter

What is SEO?

Search Engine Optimization

  • It's not rocket science!

  • Users first - Search engines second

  • Continuous work needed

  • Easy to get started with - Hard to master

  • Like web development, it is changing a lot

Search Engine Optimization

  • Important for pages search engines can crawl

    • Marketing pages, company & business sites

    • Forums, Help databases & FAQs

    • Blogs / Articles of any kind

  • Not relevant for

    • Content behind any kind of Authentication

    • Short-lived content

Three pillars of SEO

On-page

Off-page

Technical

Link building

...

Content

Keywords

UX

Meta tags

...

Social media

Citations

Authority

Page speed

...

Broken links

Security

Sitemap

We focus on two of them

On-page

Technical

SEO and Vue.js

  • Vue.js SPAs generate HTML through JS

Is JavaScript a problem for Search Engines?

Yes, for some...

  • No content when JS is disabled or not loaded

...but mostly eastern one's

Can Google fetch and

index Vue.js SPAs?

Yes, but...

Yes

  • ...meta tags aren't present, no preview when sharing links

  • ...mistake in your JS could lead to indexing blank pages

  • ...delayed content might not get indexed all

  • ...indexing doesn't mean a page ranks well

  • ...fragment router mode is bad for SEO

  • ...extra work for multi-page applications

Can Google fetch and

index Vue.js SPAs?

TL;DR

...using "just" the SPA mode isn't ideal for SEO. It can work but solely relying on it is not enough

Yes, but...

What else if not SPA?

Server Side Rendering

...on the fly

...at build time (JAMstack)

PS: You can also run your own setup for both, but it'll increase your owrk

Nuxt.js

Nuxt.js

Vuepress

Gridsome

Before we start with SEO

Set up your page in the Google Search Console

Before we start with SEO

Add an analysis tool to your website, e.g. Matomo or Google Analytics

Before we start with SEO

Bonus: Use a paid tool like Moz, Semrush or ahrefs to analyze your competition

Let's improve our SEO game

  • We will use Nuxt.js for most of the code examples shown

    • Can be used for both, dynamic SSR and JAMstack

    • Comes with vue-meta out of the box

  • vue-meta will make our SEO efforts enjoyable ๐Ÿ™Œ๐Ÿป

  • Examples can be easily applied to other frameworks, even to custom SSR setups

  • Examples will become more and more specific

  • Last but not least, users first!

What is vue-meta?

A Vue library to manage HTML metadata

<template>
  ...
</template>

<script>
  export default {
    metaInfo: {
      title: 'My Example App',
      titleTemplate: '%s - Yay!',
      htmlAttrs: {
        lang: 'en'
      }
    }
  }
</script>

Plain Vue

<template>
  ...
</template>

<script>
  export default {
    head() {
      return {
        title: 'My Example App',
        titleTemplate: '%s - Yay!',
        htmlAttrs: {
          lang: 'en'
        }
      }
    }
  }
</script>

Nuxt.js

Let's get it on!

SEO Category

Effort:ย  ๐Ÿ”ถ

๐Ÿ”ถ ย  ย  ย  ย  ย  ย  ย  ย  ย  is low

ย 

๐Ÿ”ถ๐Ÿ”ถ๐Ÿ”ถ๐Ÿ”ถ๐Ÿ”ถ ย ย  is very high

Basic security

Technical

Effort:ย  ๐Ÿ”ถ

  • Enable HTTPS if you haven't done that already
  • Set security headers like NOSNIFF
  • Keep your CMS/staff accounts/... in good shape

Mobile Friendliness

Technical

Effort:ย  ๐Ÿ”ถ - ๐Ÿ”ถ๐Ÿ”ถ๐Ÿ”ถ๐Ÿ”ถ๐Ÿ”ถ

  • A must have nowadays
  • A lot of traffic comes from mobile devices
  • Ranking factor
  • Effort depends on current state

Text Compression

Technical

Effort:ย  ๐Ÿ”ถ

  • Also a must have for every page out there
  • GZIP comes usually by default
  • Brotli is faster and leads to smaller files
  • Improves page speed
  • https://brotli.pro to check if your site supports it + guides

Text Compression

  • Nuxt enables GZIP out of the box with dynamic SSR
  • Brotli can be enabled as well
  • Usually the job of your web server or platform provider
  • Pre-generate compressed assets to save time

Broken links & redirects

Technical

Effort:ย  ๐Ÿ”ถ - ๐Ÿ”ถ๐Ÿ”ถ๐Ÿ”ถ

  • Make sure that you don't link to broken pages
  • Check for broken links to your page
    • Setup redirects for them
  • Avoid redirect chains (/a -> /b -> /c)
  • Effort depends on site and content size

Broken links & redirects

  • Redirects possible through web server / platform provider
  • Also realizable via Nuxt's redirect-module
/posts/how-to-load-dynamic-images-in-vue-and-nuxt-with-ea/       /posts/dynamic-images-vue-nuxt                      301
/posts/going-jamstack-with-netlify-and-nuxt/                     /posts/jamstack-nuxt-netlify                        301
# ...

Netlify redirects

Canonical Links

On-page

Effort:ย  ๐Ÿ”ถ

  • Set a canonical link for every page
  • It represents the preferred link/version of the page
  • Use the same URL for pages with duplicate content
  • Especially important in e-commerce
  • Great for trailing slash enforcement

https://abc.com/shoes/nike-air-max/

https://abc.com/specials/nike-air-max/

<link rel="canonical" href="https://abc.com/shoes/...">

Canonical Links

http://www.example.com

http://example.com

http://example.com/

http://www.example.com/

https://www.example.com/

https://www.example.com

https://example.com

https://example.com/

https://example.com/?foo

https://example.com/#ab

https://example.com/

Web Server's duty

Canonical Links

<script>
export default {
  components: {
    /* ... */
  },
  head () {
    const baseUrl = process.env.baseUrl // retrieve URL from env
    const { path } = this.$route // get current route
    const pathWithSlash = path.endsWith('/') ? path : `${path}/` // append trailing slash
    return {
      link: [
        { rel: 'canonical', href: `${baseUrl}${pathWithSlash}` } // Set canonical
      ]
    }
  }
}
</script>
/layouts/default.vue

Implementation in Nuxt layout

Sitemap

Technical

Effort:ย  ๐Ÿ”ถ - ๐Ÿ”ถ๐Ÿ”ถ

  • Create a sitemap.xml for all relevant URLs
  • Can be done automatically most of the time
  • If you have many URLs (>10k), split them up
  • You can also create sitemaps for images and videos

Sitemap

  sitemap: {
    hostname: 'https://example.com',
    gzip: true,
    exclude: [
      '/secret',
      '/admin/**'
    ],
    routes: [
      '/page/1',
      '/page/2',
      {
        url: '/page/3',
        changefreq: 'daily',
        priority: 1,
        lastmod: '2017-06-30T13:30:00.000Z'
      }
    ]
  }
  • Catches all static URLs, dynamic ones can be provided
  • e.g. via API or file system
nuxt.config.js

Sitemap

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
    <url>
        <loc>https://blog.lichter.io/posts/dynamic-images-vue-nuxt/</loc> <!-- The correct url -->
        <lastmod>2019-12-05T00:03:26.000Z</lastmod> <!-- might be considered -->
        <priority>1.0</priority> <!-- can be neglected -->
    </url>
    <!-- ofc there are more urls here in prod, lol -->
</urlset>

Robots.txt

Technical

Effort:ย  ๐Ÿ”ถ

  • Add your sitemap there
  • Deny crawling of some pages
  • Don't try to hide admin pages that way
  • Don't block your page from being crawled
Sitemap: https://blog.lichter.io/sitemap.xml

User-agent: *
Disallow: /legal
Disallow: /privacy
User-agent: *
Disallow: /en

Optimize your assets

Technical

Effort:ย  ๐Ÿ”ถ - ๐Ÿ”ถ๐Ÿ”ถ

  • Optimized responsive images via <picture>

    • Mind the format and size

    • Remove metadata, compress lossy

    • Load lazily

  • Improve JS via code-splitting, lazy-loading & treeshaking
  • Cache all static assets, use hashes/fingerprinting
  • Use descriptive file names for images!

Page Structure

Technical

Effort:ย  ๐Ÿ”ถ๐Ÿ”ถ - ๐Ÿ”ถ๐Ÿ”ถ๐Ÿ”ถ๐Ÿ”ถ

  • Create a logical and hierarchical structure
  • Don't nest pages too deeply (> 3 clicks)
  • Make category pages worth indexing
  • Link topic pages internally where possible

Home

Cat 1

Cat 2

Page 1

Page 2

Page 3

Page 4

Cat 1

Cat 2

Akryum's cat (Oreo)

URL Structure

Technical

Effort:ย  ๐Ÿ”ถ๐Ÿ”ถ - ๐Ÿ”ถ๐Ÿ”ถ๐Ÿ”ถ๐Ÿ”ถ

  • Use short and descriptive URLs
  • Remove stop words (and, the, a, ...)
  • Enforce trailing slashes or remove them
  • Place keywords inside
  • Avoid query params
  • Use hyphens as delimiters
  • non-www or www

https://abc.com/129310231.html

https://abc.com/blog_post-about_benefits_of-nuxt.html

https://abc.com/?id=129310231

https://abc.com/blog/post-about-benefits-of-nuxt.html

https://abc.com/blog/benefits-of-nuxt/

๐Ÿคฎ

๐Ÿ˜ข

๐Ÿ™

๐Ÿ™‚

๐Ÿคฉ

URL Structure

export default {
  /* ... */
  router: {
    trailingSlash: true // Will enforce slashes
  }
}

Consistent trailing slashes with Nuxt

nuxt.config.js

Attention: Set up redirects for non-slash URLs

Semantic HTML

Technical / On-page

Effort:ย  ๐Ÿ”ถ

  • Use semantic HTML. For real!
    • <a> when you change pages on click
    • <button> when it's actionable and no link
  • Use nav, footer, main, section where applicable
  • Use headings appropriately & in hierarchy
<template>
  <div>
    <nuxt-link
      v-for="(project, index) in projects"
      tag="div"
      @click.native="doSomethingElse">
      To project
    </nuxt-link>
  </div>
</template>

Alt Tags

Technical / On-page

Effort:ย  ๐Ÿ”ถ

  • Provide alt tags for your images
  • Don't just stuff keywords in there
  • Use real descriptions of the image!
  • Bonus: Can be retrieved via AI nowadays
<template>
  <div>
    <img src="@assets/img/cute-dog">
  </div>
</template>

Meta tags

On-page

Effort: ๐Ÿ”ถ - ๐Ÿ”ถ๐Ÿ”ถ๐Ÿ”ถ๐Ÿ”ถ๐Ÿ”ถ

  • Set utf-8 charset as well as the initial viewport
  • Find the ideal title and meta description for each page
  • Testing will be necessary but is worth it
  • Use OG tags to improve link previews
  • Effort depends on website size

Meta tags - Charset & Viewport

<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

<meta name="viewport" content="width=device-width, initial-scale=1">

Can be set in nuxt.config.js or via @nuxtjs/pwa module

Using the latter will provide it by default...

...and also comes with other neat shortcuts

Meta tags - Title

Preferred format: ย  ย  ย  ย  Primary Keyword - Secondary Keyword | Brand Name

ย Going JAMstack with Netlify and Nuxt | blog.Lichter.ioย  ย 

export default {
  head: {
    titleTemplate: c => c ? `${c} | blog.Lichter.io` : 'blog.Lichter.io - Alex\'s blog about things'
  }
}

Suggested length: 50 - 60 characters

Leverage vue-meta's titleTemplate

<script>
export default {
  head () {
    return {
      title: 'Going JAMstack with Netlify and Nuxt'
    }
  }
}
</script>
nuxt.config.js

Define fragment in the page components

Meta tags - Meta Description

  • Suggested length: 155 - 160 characters
  • Your chance to advertise the content

  • Good description -> high CTR

  • Define description in the page components

<script>
export default {
  head () {
    return {
      title: 'Speaking'
      meta: [
        {
          hid: 'description',
          name: 'description',
          content: 'Here comes the long meta description'
        }
      ]
    }
  }
}
</script>

hreflang

anchor text

structured data

local seo

What to do after changing things

  • Monitor, monitor, monitor!
  • Resubmit changed pages via Google Search Console for quicker updates

  • Be patient - SEO changes often take time until results show up

Conclusion

  • SEO is no rocket science
  • Vue.js and SEO can be a love story

  • Always monitor changes to catch mistakes, find chances to improve, etc.

  • The only two constants in SEO are

    • It's not a one-time thing

    • As in web dev, things change quickly and often

Thank you!

@TheAlexLichter

Bonus - Structured Data

  • Help search engines to understand your content even better...
  • By providing an easily machine-readable format of your content

  • Best way: JSON-LD

<script type="application/ld+json">
{
    "@context": "http://schema.org",
    "@type": "Blog",
    "name": "blog.Lichter.io",
    "url": "https://blog.lichter.io/",
    "description": "A technical blog about Nuxt.js, Vue.js, Javascript, best practices, clean code and more!",
    "publisher": {
        "@type": "Organization",
        "name": "Alexander Lichter",
        "logo": {
            "@type": "imageObject",
            "url": "https://lichter.io/img/me@2x.jpg"
        }
    },
    "sameAs": ["https://nuxt.xyz"],
    "blogPosts": [/* ... */]
}
</script>

On-page

Effort: ๐Ÿ”ถ๐Ÿ”ถ๐Ÿ”ถ - ๐Ÿ”ถ๐Ÿ”ถ๐Ÿ”ถ๐Ÿ”ถ