Astro Config SEO Setup
Astro Config SEO Setup
Section titled “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.
1. astro.config.mjs SEO Config
Section titled “1. astro.config.mjs SEO Config”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', }, }, }, },});2. SEO Component Reusable
Section titled “2. SEO Component Reusable”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 configconst siteName = 'Your Site Name';const twitterHandle = '@yourhandle';
// Canonical URLconst canonicalURL = new URL(Astro.url.pathname, Astro.site);
// Full image URLconst ogImage = new URL(image, Astro.site);
// Robots directiveconst robots = `${noindex ? 'noindex' : 'index'},${nofollow ? 'nofollow' : 'follow'}`;
// Page title với site nameconst 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} />3. Layout với SEO Integration
Section titled “3. Layout với SEO Integration”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>4. Sử dụng trong Page
Section titled “4. Sử dụng trong Page”Static Page
Section titled “Static Page”---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>Dynamic Page (Blog Post)
Section titled “Dynamic Page (Blog Post)”---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 URLconst 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>5. Schema.org JSON-LD Component
Section titled “5. Schema.org JSON-LD Component”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)} />Sử dụng SchemaOrg
Section titled “Sử dụng SchemaOrg”---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, }}/>6. Breadcrumb Component
Section titled “6. Breadcrumb Component”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>7. Package Installation
Section titled “7. Package Installation”# SEO integrationsnpm install @astrojs/sitemapnpm install astro-robots-txt
# Optional: Analyticsnpm install @astrojs/partytown
# Optional: Image optimizationnpm install @astrojs/image8. Checklist Cấu hình
Section titled “8. Checklist Cấu hình”9. Testing SEO Setup
Section titled “9. Testing SEO Setup”Local testing
Section titled “Local testing”# Build productionnpm run build
# Check meta tags trong dist/grep -r "og:title" dist/grep -r "description" dist/
# Previewnpm run previewOnline tools
Section titled “Online tools”- Facebook Sharing Debugger: https://developers.facebook.com/tools/debug/
- Twitter Card Validator: https://cards-dev.twitter.com/validator
- Google Rich Results Test: https://search.google.com/test/rich-results
- Schema.org Validator: https://validator.schema.org/