vibeblame
Guides

How to fix missing SEO and meta tags

Title, description, Open Graph, canonical, robots.txt — what each one does and how to add them in Next.js, Astro, Vue (Nuxt), Angular, WordPress, and Tilda.

What each tag does

TagWhat it affects
<title>Browser tab, Google search result title
<meta name="description">Google snippet text (sometimes)
<h1>Main topic signal for search engines
og:title / og:description / og:imageLink preview in Telegram, Slack, iMessage, Twitter
<link rel="canonical">Tells Google which URL is the "real" one to index
robots.txtControls which pages search engines crawl

Optimal lengths: title 30–60 characters, description 120–160 characters. Google truncates anything longer.


Next.js (App Router)

Static pages:

// app/page.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Your Page Title',
  description: 'What this page is about, around 150 characters. Be specific.',
  openGraph: {
    title: 'Your Page Title',
    description: 'Same or similar to meta description',
    images: [{ url: '/og-image.png', width: 1200, height: 630, alt: 'Description' }],
    url: 'https://yoursite.com',
    type: 'website',
  },
  alternates: {
    canonical: 'https://yoursite.com',
  },
}

Dynamic pages:

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
  const post = await getPost(params.slug)
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [{ url: post.coverImage, width: 1200, height: 630 }],
    },
    alternates: { canonical: `https://yoursite.com/blog/${params.slug}` },
  }
}

Title template (in layout):

// app/layout.tsx
export const metadata: Metadata = {
  title: {
    default: 'My App',
    template: '%s | My App',
  },
}

robots.txt:

// app/robots.ts
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
  return {
    rules: { userAgent: '*', allow: '/', disallow: ['/api/', '/admin/'] },
    sitemap: 'https://yoursite.com/sitemap.xml',
  }
}

Astro

---
// src/layouts/BaseLayout.astro
const { title, description, canonicalURL, ogImage } = Astro.props
---
<html>
  <head>
    <title>{title}</title>
    <meta name="description" content={description} />
    <link rel="canonical" href={canonicalURL} />
    <meta property="og:title" content={title} />
    <meta property="og:description" content={description} />
    <meta property="og:image" content={ogImage ?? '/og-image.png'} />
    <meta property="og:url" content={canonicalURL} />
    <meta property="og:type" content="website" />
  </head>
  <body><slot /></body>
</html>

Use it on pages:

---
import BaseLayout from '../layouts/BaseLayout.astro'
---
<BaseLayout
  title="Your Page Title"
  description="Around 150 characters describing this page."
  canonicalURL="https://yoursite.com"
>
  <h1>Your Page Title</h1>
</BaseLayout>

For robots.txt, create public/robots.txt:

User-agent: *
Allow: /
Sitemap: https://yoursite.com/sitemap.xml

Vue (Nuxt)

Nuxt 3 provides useSeoMeta() and useHead() composables with full SSR support — meta tags are rendered server-side and visible to all crawlers.

Static meta in a page component:

<!-- pages/index.vue -->
<script setup>
useSeoMeta({
  title: 'Your Page Title',
  description: 'Around 150 characters describing this page.',
  ogTitle: 'Your Page Title',
  ogDescription: 'Same or similar to the description.',
  ogImage: 'https://yoursite.com/og-image.png',
  ogUrl: 'https://yoursite.com',
  ogType: 'website',
})

useHead({
  link: [{ rel: 'canonical', href: 'https://yoursite.com' }],
})
</script>

Dynamic meta from API data:

<!-- pages/blog/[slug].vue -->
<script setup>
const route = useRoute()
const { data: post } = await useFetch(`/api/posts/${route.params.slug}`)

useSeoMeta({
  title: () => post.value?.title,
  description: () => post.value?.excerpt,
  ogTitle: () => post.value?.title,
  ogDescription: () => post.value?.excerpt,
  ogImage: () => post.value?.coverImage,
})

useHead({
  link: [{ rel: 'canonical', href: `https://yoursite.com/blog/${route.params.slug}` }],
})
</script>

Global defaults in nuxt.config.ts:

// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      titleTemplate: '%s | My Site',
      meta: [
        { name: 'description', content: 'Default site description' },
      ],
    },
  },
})

robots.txt — create public/robots.txt:

User-agent: *
Allow: /
Sitemap: https://yoursite.com/sitemap.xml

Or use the @nuxtjs/robots module for dynamic control.


Angular

Angular provides Title and Meta services from @angular/platform-browser:

// src/app/home/home.component.ts
import { Component, OnInit } from '@angular/core'
import { Title, Meta } from '@angular/platform-browser'

@Component({ selector: 'app-home', templateUrl: './home.component.html' })
export class HomeComponent implements OnInit {
  constructor(private title: Title, private meta: Meta) {}

  ngOnInit() {
    this.title.setTitle('Your Page Title')
    this.meta.updateTag({ name: 'description', content: 'Around 150 characters.' })
    this.meta.updateTag({ property: 'og:title', content: 'Your Page Title' })
    this.meta.updateTag({ property: 'og:description', content: 'Around 150 characters.' })
    this.meta.updateTag({ property: 'og:image', content: 'https://yoursite.com/og-image.png' })
    this.meta.updateTag({ property: 'og:url', content: 'https://yoursite.com' })
  }
}

Canonical link (inject manually):

import { Inject } from '@angular/core'
import { DOCUMENT } from '@angular/common'

constructor(@Inject(DOCUMENT) private doc: Document) {}

setCanonical(url: string) {
  let link: HTMLLinkElement =
    this.doc.querySelector('link[rel="canonical"]') || this.doc.createElement('link')
  link.setAttribute('rel', 'canonical')
  link.setAttribute('href', url)
  this.doc.head.appendChild(link)
}

Important: meta tags set via JavaScript are not visible to most crawlers unless you use Angular Universal (SSR). Google can run JS, but social media previews (Telegram, Slack, Twitter) and many other bots cannot. For full SEO coverage, add SSR:

ng add @angular/ssr

With SSR, meta tags are rendered into the HTML response and immediately visible to all crawlers.

robots.txt — place in src/ and register in angular.json:

"assets": ["src/favicon.ico", "src/assets", "src/robots.txt"]

src/robots.txt:

User-agent: *
Allow: /
Sitemap: https://yoursite.com/sitemap.xml

WordPress

The easiest path is a plugin — it handles all required tags from the admin panel without touching code:

  • Yoast SEO — the most popular option. Adds title, description, OG tags, canonical, sitemap, and robots.txt from a dedicated settings page.
  • Rank Math — similar feature set, slightly lighter.

After installing, fill in the SEO title and description for each page and post. The plugin adds the HTML tags automatically.

Without a plugin, add to functions.php in your theme:

function add_custom_meta_tags() {
  if (is_singular()) {
    global $post;
    $description = get_post_meta($post->ID, '_meta_description', true);
    if ($description) {
      echo '<meta name="description" content="' . esc_attr($description) . '">' . PHP_EOL;
    }
    echo '<link rel="canonical" href="' . esc_url(get_permalink()) . '">' . PHP_EOL;
  }
}
add_action('wp_head', 'add_custom_meta_tags');

robots.txt in WordPress is generated automatically at /robots.txt. Edit it via Yoast SEO -> Tools -> File Editor, or directly on the server.


Tilda

In the editor: Site Settings -> SEO — fill in the global title, description, and OG image.

For individual pages: page settings (gear icon) -> SEO tab -> fill in title, description, and OG image per page.

Tilda generates all required meta tags from these fields — no code needed.

robots.txt: Site Settings -> SEO -> robots.txt.


H1 — one per page

Every page should have exactly one <h1>. It is the main topic signal for search engines.

<!-- Correct -->
<h1>Clear Page Topic</h1>
<h2>Subtopic</h2>

<!-- Wrong — multiple h1s confuse search engines -->
<h1>Main Title</h1>
<h1>Another Title</h1>

Verify

Then run vibeblame again — the SEO score should go up.

How to fix missing SEO and meta tags | vibeblame