What each tag does
| Tag | What 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:image | Link preview in Telegram, Slack, iMessage, Twitter |
<link rel="canonical"> | Tells Google which URL is the "real" one to index |
robots.txt | Controls 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
- OG tags: paste your URL at opengraph.xyz to preview how it looks when shared
- Title and description rendering: Google Rich Results Test
- Indexing status: Google Search Console -> URL Inspection
Then run vibeblame again — the SEO score should go up.