Alexander Lichter
Web Engineering Consultant • Founder • Nuxt team • Speaker
vueday 2020
Alexander Lichter
Nuxt.js Core Maintainer
@TheAlexLichter
Web Dev Consultant
It's not rocket science!
Users first - Search engines second
Continuous improvements
Easy to get started with - Hard to master
Frequently changing - like web development
Important for pages search engines can access
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
On-page
Off-page
Technical
Link building
...
Content
Keywords
UX
Meta tags
...
Social media
Citations
Authority
Page speed
...
Broken links
Security
Sitemap
On-page
Technical
...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
...using "just" the SPA mode isn't ideal for SEO. It can work but solely relying on it is not enough
...on the fly
...at build time (JAMstack)
PS: You can also run your own setup for both, but it'll increase your work
Nuxt.js
Nuxt.js
Vuepress
Gridsome
Set up your page in the Google Search Console
Add an analysis tool to your website, e.g. Matomo or Google Analytics
Bonus: Use a paid tool like Moz, Semrush or ahrefs to analyze your competition
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!
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
SEO Category
Effort: 🔶
🔶 is low
🔶🔶🔶🔶🔶 is very high
Technical
Effort: 🔶
Technical
Effort: 🔶 - 🔶🔶🔶🔶🔶
Technical
Effort: 🔶 - 🔶🔶🔶🔶🔶
Technical
Effort: 🔶
Technical
Effort: 🔶 - 🔶🔶🔶
/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
On-page
Effort: 🔶
https://abc.com/shoes/nike-air-max/
https://abc.com/specials/nike-air-max/
<link rel="canonical" href="https://abc.com/shoes/...">
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
<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
Technical
Effort: 🔶 - 🔶🔶
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'
}
]
}
nuxt.config.js
<?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>
Technical
Effort: 🔶 - 🔶🔶
Optimized responsive images via <picture>
Mind the format and size
Remove metadata, compress lossy
Load lazily
Technical
Effort: 🔶🔶 - 🔶🔶🔶🔶
Home
Page 1
Page 2
Page 3
Page 4
Cat 1
Cat 2
Technical
Effort: 🔶🔶 - 🔶🔶🔶🔶
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/
🤮
😢
🙁
🙂
🤩
export default {
/* ... */
router: {
trailingSlash: true // Will enforce slashes
}
}
Consistent trailing slashes with Nuxt
nuxt.config.js
Attention: Set up redirects for non-slash URLs
Technical / On-page
Effort: 🔶
<template>
<div>
<nuxt-link
v-for="(project, index) in projects"
tag="div"
@click.native="doSomethingElse">
To project
</nuxt-link>
</div>
</template>
Technical / On-page
Effort: 🔶
<template>
<div>
<img src="@assets/img/cute-dog">
</div>
</template>
On-page
Effort: 🔶 - 🔶🔶🔶🔶🔶
<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
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
Your chance to advertise the content
Good description -> high CTR
Define description in the 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
Resubmit changed pages via Google Search Console for quicker updates
Be patient - SEO changes often take time until results show up
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
@TheAlexLichter
Technical
Effort: 🔶
Sitemap: https://blog.lichter.io/sitemap.xml
User-agent: *
Disallow: /legal
Disallow: /privacy
User-agent: *
Disallow: /en
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: 🔶🔶🔶 - 🔶🔶🔶🔶
By Alexander Lichter
Web Engineering Consultant • Founder • Nuxt team • Speaker