Alexander Lichter
Web Engineering Consultant โข Founder โข Nuxt team โข Speaker
Vue.js Global 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