Skip to content

Astro Config SEO Setup

Bài viết này tập trung vào cấu hình SEO trong code - từ astro.config.mjs, components, đến từng page. Setup một lần, dùng cho cả project.

astro.config.mjs:

import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
import robotsTxt from 'astro-robots-txt';
export default defineConfig({
// ⚠️ QUAN TRỌNG: Site URL cho canonical và sitemap
site: 'https://yoursite.com',
// Trailing slash config (ảnh hưởng đến URL structure)
trailingSlash: 'always', // hoặc 'never'
// Build format cho SEO-friendly URLs
build: {
format: 'directory', // Tạo /page/index.html thay vì /page.html
},
// Integrations cho SEO
integrations: [
// Auto-generate sitemap
sitemap({
filter: (page) => !page.includes('/admin') && !page.includes('/private'),
changefreq: 'weekly',
priority: 0.7,
lastmod: new Date(),
i18n: {
defaultLocale: 'vi',
locales: {
vi: 'vi-VN',
en: 'en-US',
},
},
}),
// Auto-generate robots.txt
robotsTxt({
policy: [
{
userAgent: '*',
allow: '/',
disallow: ['/admin', '/api', '/drafts'],
crawlDelay: 10,
},
],
sitemap: true,
}),
],
// Vite config cho SEO assets
vite: {
build: {
// Asset hashing cho cache busting
assetsInlineLimit: 4096,
rollupOptions: {
output: {
// Organized asset structure
assetFileNames: 'assets/[name]-[hash][extname]',
chunkFileNames: 'chunks/[name]-[hash].js',
entryFileNames: 'entries/[name]-[hash].js',
},
},
},
},
});

src/components/SEO.astro:

---
export interface Props {
title: string;
description: string;
image?: string;
type?: 'website' | 'article';
publishedTime?: string;
modifiedTime?: string;
author?: string;
keywords?: string[];
noindex?: boolean;
nofollow?: boolean;
}
const {
title,
description,
image = '/images/og-default.jpg',
type = 'website',
publishedTime,
modifiedTime,
author,
keywords,
noindex = false,
nofollow = false,
} = Astro.props;
// Site config
const siteName = 'Your Site Name';
const twitterHandle = '@yourhandle';
// Canonical URL
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
// Full image URL
const ogImage = new URL(image, Astro.site);
// Robots directive
const robots = `${noindex ? 'noindex' : 'index'},${nofollow ? 'nofollow' : 'follow'}`;
// Page title với site name
const fullTitle = title === siteName
? title
: `${title} | ${siteName}`;
---
<!-- Primary Meta Tags -->
<title>{fullTitle}</title>
<meta name="title" content={fullTitle} />
<meta name="description" content={description} />
{keywords && <meta name="keywords" content={keywords.join(', ')} />}
<meta name="robots" content={robots} />
<link rel="canonical" href={canonicalURL} />
<!-- Author -->
{author && <meta name="author" content={author} />}
<!-- Open Graph / Facebook -->
<meta property="og:type" content={type} />
<meta property="og:site_name" content={siteName} />
<meta property="og:url" content={canonicalURL} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={ogImage} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:locale" content="vi_VN" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:site" content={twitterHandle} />
<meta property="twitter:url" content={canonicalURL} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={ogImage} />
<!-- Article specific -->
{type === 'article' && (
<>
{publishedTime && (
<meta property="article:published_time" content={publishedTime} />
)}
{modifiedTime && (
<meta property="article:modified_time" content={modifiedTime} />
)}
{author && (
<meta property="article:author" content={author} />
)}
</>
)}
<!-- Alternate Languages (nếu có) -->
<link rel="alternate" hreflang="vi" href={canonicalURL} />
<link rel="alternate" hreflang="x-default" href={canonicalURL} />

src/layouts/MainLayout.astro:

---
import SEO from '../components/SEO.astro';
interface Props {
title: string;
description: string;
image?: string;
type?: 'website' | 'article';
publishedTime?: string;
modifiedTime?: string;
author?: string;
keywords?: string[];
}
const {
title,
description,
image,
type,
publishedTime,
modifiedTime,
author,
keywords,
} = Astro.props;
---
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- SEO Component -->
<SEO
title={title}
description={description}
image={image}
type={type}
publishedTime={publishedTime}
modifiedTime={modifiedTime}
author={author}
keywords={keywords}
/>
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<!-- Preconnect external domains -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<!-- Sitemap -->
<link rel="sitemap" href="/sitemap-index.xml" />
<!-- RSS -->
<link rel="alternate" type="application/rss+xml" title="RSS Feed" href="/rss.xml" />
<!-- Theme Color -->
<meta name="theme-color" content="#e11d48" />
<!-- Styles -->
<link rel="stylesheet" href="/styles/global.css" />
</head>
<body>
<header>
<nav aria-label="Main navigation">
<!-- Navigation -->
</nav>
</header>
<main>
<slot />
</main>
<footer>
<!-- Footer -->
</footer>
</body>
</html>
---
import MainLayout from '../layouts/MainLayout.astro';
---
<MainLayout
title="About Us"
description="Learn more about our company and mission"
image="/images/about-og.jpg"
>
<h1>About Us</h1>
<p>Content...</p>
</MainLayout>
---
import { getCollection } from 'astro:content';
import MainLayout from '../../layouts/MainLayout.astro';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await post.render();
// Generate OG image URL
const ogImage = post.data.image
? post.data.image
: `/og-image/${post.slug}.png`;
---
<MainLayout
title={post.data.title}
description={post.data.excerpt}
image={ogImage}
type="article"
publishedTime={post.data.date.toISOString()}
modifiedTime={post.data.updated?.toISOString()}
author={post.data.author}
keywords={post.data.tags}
>
<article>
<header>
<h1>{post.data.title}</h1>
<time datetime={post.data.date.toISOString()}>
{post.data.date.toLocaleDateString('vi-VN')}
</time>
</header>
<Content />
</article>
</MainLayout>

src/components/SchemaOrg.astro:

---
interface Props {
type: 'website' | 'article' | 'breadcrumb';
data: any;
}
const { type, data } = Astro.props;
const generateSchema = () => {
switch (type) {
case 'website':
return {
'@context': 'https://schema.org',
'@type': 'WebSite',
name: data.name,
url: data.url,
description: data.description,
potentialAction: {
'@type': 'SearchAction',
target: `${data.url}/search?q={search_term_string}`,
'query-input': 'required name=search_term_string',
},
};
case 'article':
return {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: data.title,
description: data.description,
image: data.image,
datePublished: data.date,
dateModified: data.modified || data.date,
author: {
'@type': 'Person',
name: data.author,
url: data.authorUrl,
},
publisher: {
'@type': 'Organization',
name: data.siteName,
logo: {
'@type': 'ImageObject',
url: data.logo,
},
},
mainEntityOfPage: {
'@type': 'WebPage',
'@id': data.url,
},
};
case 'breadcrumb':
return {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: data.items.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: item.name,
item: item.url,
})),
};
default:
return {};
}
};
const schema = generateSchema();
---
<script type="application/ld+json" set:html={JSON.stringify(schema)} />
---
import SchemaOrg from '../components/SchemaOrg.astro';
---
<SchemaOrg
type="website"
data={{
name: 'My Site',
url: 'https://yoursite.com',
description: 'Site description',
}}
/>
<SchemaOrg
type="article"
data={{
title: post.data.title,
description: post.data.excerpt,
image: 'https://yoursite.com' + post.data.image,
date: post.data.date,
author: post.data.author,
siteName: 'My Site',
logo: 'https://yoursite.com/logo.png',
url: 'https://yoursite.com/blog/' + post.slug,
}}
/>

src/components/Breadcrumb.astro:

---
interface Item {
name: string;
url: string;
}
interface Props {
items: Item[];
}
const { items } = Astro.props;
---
<nav aria-label="Breadcrumb">
<ol itemscope itemtype="https://schema.org/BreadcrumbList">
{items.map((item, index) => (
<li
itemprop="itemListElement"
itemscope
itemtype="https://schema.org/ListItem"
>
{index < items.length - 1 ? (
<a itemprop="item" href={item.url}>
<span itemprop="name">{item.name}</span>
</a>
) : (
<span itemprop="name" aria-current="page">{item.name}</span>
)}
<meta itemprop="position" content={index + 1} />
</li>
))}
</ol>
</nav>
Terminal window
# SEO integrations
npm install @astrojs/sitemap
npm install astro-robots-txt
# Optional: Analytics
npm install @astrojs/partytown
# Optional: Image optimization
npm install @astrojs/image
Terminal window
# Build production
npm run build
# Check meta tags trong dist/
grep -r "og:title" dist/
grep -r "description" dist/
# Preview
npm run preview