Skip to content

Commit cc9648c

Browse files
committed
feat(seo): add structured data and multilingual SEO improvements
- Add robots.txt with sitemap reference - Add JSON-LD schemas (WebSite, BlogPosting, BreadcrumbList) - Add hreflang alternate links for ko/en - Add English RSS feed (/en/rss.xml)
1 parent 5fe071a commit cc9648c

File tree

5 files changed

+149
-2
lines changed

5 files changed

+149
-2
lines changed

public/robots.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
User-agent: *
2+
Allow: /
3+
4+
Sitemap: https://ppippi.dev/sitemap-index.xml

src/components/BaseHead.astro

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ interface Props {
1515
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
1616
1717
const { title, description, image = FallbackImage } = Astro.props;
18+
19+
// Generate hreflang URLs for multilingual SEO
20+
const pathname = Astro.url.pathname;
21+
const isEnglish = pathname.startsWith('/en/');
22+
const koURL = isEnglish
23+
? new URL(pathname.replace('/en/', '/'), Astro.site).href
24+
: canonicalURL.href;
25+
const enURL = isEnglish
26+
? canonicalURL.href
27+
: new URL(pathname === '/' ? '/en/' : `/en${pathname}`, Astro.site).href;
1828
---
1929

2030
<!-- Global Metadata -->
@@ -26,8 +36,8 @@ const { title, description, image = FallbackImage } = Astro.props;
2636
<link
2737
rel="alternate"
2838
type="application/rss+xml"
29-
title={SITE_TITLE}
30-
href={new URL('rss.xml', Astro.site)}
39+
title={isEnglish ? `${SITE_TITLE} (English)` : SITE_TITLE}
40+
href={new URL(isEnglish ? 'en/rss.xml' : 'rss.xml', Astro.site)}
3141
/>
3242
<meta name="generator" content={Astro.generator} />
3343

@@ -41,6 +51,11 @@ const { title, description, image = FallbackImage } = Astro.props;
4151
<!-- Canonical URL -->
4252
<link rel="canonical" href={canonicalURL} />
4353

54+
<!-- Hreflang for multilingual SEO -->
55+
<link rel="alternate" hreflang="ko" href={koURL} />
56+
<link rel="alternate" hreflang="en" href={enURL} />
57+
<link rel="alternate" hreflang="x-default" href={koURL} />
58+
4459
<!-- Primary Meta Tags -->
4560
<title>{title}</title>
4661
<meta name="title" content={title} />
@@ -68,3 +83,18 @@ const { title, description, image = FallbackImage } = Astro.props;
6883
gtag('js', new Date());
6984
gtag('config', 'G-KQYDN19HGC');
7085
</script>
86+
87+
<!-- JSON-LD for WebSite -->
88+
<script type="application/ld+json" set:html={JSON.stringify({
89+
"@context": "https://schema.org",
90+
"@type": "WebSite",
91+
"name": "ppippi-dev Blog",
92+
"url": "https://ppippi.dev",
93+
"description": "My personal blog for organizing learnings and experiences in MLOps, AI, and Cloud.",
94+
"inLanguage": ["ko", "en"],
95+
"author": {
96+
"@type": "Person",
97+
"name": "ppippi",
98+
"url": "https://ppippi.dev"
99+
}
100+
})} />

src/layouts/BlogPost.astro

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,55 @@ const { title, description, pubDate, updatedDate, heroImage, tags, slug } = Astr
1414
<html lang="ko">
1515
<head>
1616
<BaseHead title={title} description={description} />
17+
<!-- JSON-LD for BlogPosting -->
18+
<script type="application/ld+json" set:html={JSON.stringify({
19+
"@context": "https://schema.org",
20+
"@type": "BlogPosting",
21+
"headline": title,
22+
"description": description || title,
23+
"image": heroImage ? new URL(heroImage, Astro.site).href : undefined,
24+
"datePublished": pubDate.toISOString(),
25+
"dateModified": updatedDate ? updatedDate.toISOString() : pubDate.toISOString(),
26+
"author": {
27+
"@type": "Person",
28+
"name": "ppippi",
29+
"url": "https://ppippi.dev"
30+
},
31+
"publisher": {
32+
"@type": "Organization",
33+
"name": "ppippi-dev Blog",
34+
"url": "https://ppippi.dev"
35+
},
36+
"mainEntityOfPage": {
37+
"@type": "WebPage",
38+
"@id": Astro.url.href
39+
},
40+
"keywords": tags ? tags.join(", ") : undefined
41+
})} />
42+
<!-- JSON-LD for BreadcrumbList -->
43+
<script type="application/ld+json" set:html={JSON.stringify({
44+
"@context": "https://schema.org",
45+
"@type": "BreadcrumbList",
46+
"itemListElement": [
47+
{
48+
"@type": "ListItem",
49+
"position": 1,
50+
"name": "",
51+
"item": "https://ppippi.dev"
52+
},
53+
{
54+
"@type": "ListItem",
55+
"position": 2,
56+
"name": "블로그",
57+
"item": "https://ppippi.dev/blog/"
58+
},
59+
{
60+
"@type": "ListItem",
61+
"position": 3,
62+
"name": title
63+
}
64+
]
65+
})} />
1766
<style>
1867
main {
1968
width: 100%;

src/layouts/BlogPostEn.astro

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,55 @@ const { title, description, pubDate, updatedDate, heroImage, tags, slug } = Astr
1414
<html lang="en">
1515
<head>
1616
<BaseHead title={title} description={description} />
17+
<!-- JSON-LD for BlogPosting -->
18+
<script type="application/ld+json" set:html={JSON.stringify({
19+
"@context": "https://schema.org",
20+
"@type": "BlogPosting",
21+
"headline": title,
22+
"description": description || title,
23+
"image": heroImage ? new URL(heroImage, Astro.site).href : undefined,
24+
"datePublished": pubDate.toISOString(),
25+
"dateModified": updatedDate ? updatedDate.toISOString() : pubDate.toISOString(),
26+
"author": {
27+
"@type": "Person",
28+
"name": "ppippi",
29+
"url": "https://ppippi.dev"
30+
},
31+
"publisher": {
32+
"@type": "Organization",
33+
"name": "ppippi-dev Blog",
34+
"url": "https://ppippi.dev"
35+
},
36+
"mainEntityOfPage": {
37+
"@type": "WebPage",
38+
"@id": Astro.url.href
39+
},
40+
"keywords": tags ? tags.join(", ") : undefined
41+
})} />
42+
<!-- JSON-LD for BreadcrumbList -->
43+
<script type="application/ld+json" set:html={JSON.stringify({
44+
"@context": "https://schema.org",
45+
"@type": "BreadcrumbList",
46+
"itemListElement": [
47+
{
48+
"@type": "ListItem",
49+
"position": 1,
50+
"name": "Home",
51+
"item": "https://ppippi.dev/en/"
52+
},
53+
{
54+
"@type": "ListItem",
55+
"position": 2,
56+
"name": "Blog",
57+
"item": "https://ppippi.dev/en/blog/"
58+
},
59+
{
60+
"@type": "ListItem",
61+
"position": 3,
62+
"name": title
63+
}
64+
]
65+
})} />
1766
<style>
1867
main {
1968
width: 100%;

src/pages/en/rss.xml.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { getCollection } from 'astro:content';
2+
import rss from '@astrojs/rss';
3+
4+
export async function GET(context) {
5+
const posts = await getCollection('blog-en');
6+
return rss({
7+
title: 'ppippi-dev Blog (English)',
8+
description: 'My personal blog for organizing learnings and experiences in MLOps, AI, and Cloud.',
9+
site: context.site,
10+
items: posts.map((post) => ({
11+
...post.data,
12+
link: `/en/blog/${post.id}/`,
13+
})),
14+
});
15+
}

0 commit comments

Comments
 (0)