Skip to content

Commit f8c7ee5

Browse files
brabojclaude
andcommitted
feat: add SEO optimization — meta tags, sitemap, structured data (#123)
- Add page-specific meta descriptions via frontmatter - Add Open Graph and Twitter Card meta tags - Add canonical URLs - Install @astrojs/sitemap integration - Add robots.txt - Add JSON-LD structured data (Course on home, Article on chapters) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 79e718e commit f8c7ee5

17 files changed

Lines changed: 165 additions & 6 deletions

astro-site/astro.config.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
// @ts-check
22
import { defineConfig } from 'astro/config';
3+
import sitemap from '@astrojs/sitemap';
34
import { remarkRewriteLinks } from './src/plugins/remark-rewrite-links.ts';
45

56
// https://astro.build/config
67
export default defineConfig({
78
site: 'https://braboj.github.io',
89
base: '/tutorial-git/',
910
trailingSlash: 'always',
11+
integrations: [sitemap()],
1012
markdown: {
1113
syntaxHighlight: false,
1214
remarkPlugins: [remarkRewriteLinks],

astro-site/package-lock.json

Lines changed: 67 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

astro-site/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"astro": "astro"
1313
},
1414
"dependencies": {
15+
"@astrojs/sitemap": "^3.7.2",
1516
"astro": "^6.1.8"
1617
}
17-
}
18+
}

astro-site/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://braboj.github.io/tutorial-git/sitemap-index.xml

astro-site/src/content.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const docs = defineCollection({
99
}),
1010
schema: z.object({
1111
title: z.string(),
12+
description: z.string().optional(),
1213
section: z.string(),
1314
order: z.number(),
1415
}),

astro-site/src/layouts/DocLayout.astro

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,52 @@ import site from "../data/site.json";
88
99
interface Props {
1010
title: string;
11+
description?: string;
1112
headings?: { depth: number; slug: string; text: string }[];
1213
currentPath?: string;
14+
jsonLd?: Record<string, unknown>;
1315
}
1416
15-
const { title, headings = [], currentPath = "/" } = Astro.props;
17+
const { title, description, headings = [], currentPath = "/", jsonLd } = Astro.props;
18+
const pageDescription = description || site.description;
19+
const pageTitle = `${title} — ${site.title}`;
20+
const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
21+
const ogImage = new URL("/tutorial-git/images/og-banner.png", Astro.site);
1622
---
1723

1824
<!doctype html>
1925
<html lang="en">
2026
<head>
2127
<meta charset="utf-8" />
2228
<meta name="viewport" content="width=device-width, initial-scale=1" />
23-
<title>{title}{site.title}</title>
24-
<meta name="description" content={site.description} />
29+
<title>{pageTitle}</title>
30+
<meta name="description" content={pageDescription} />
31+
<link rel="canonical" href={canonicalUrl} />
32+
33+
<!-- Open Graph -->
34+
<meta property="og:type" content="article" />
35+
<meta property="og:title" content={pageTitle} />
36+
<meta property="og:description" content={pageDescription} />
37+
<meta property="og:url" content={canonicalUrl} />
38+
<meta property="og:image" content={ogImage} />
39+
<meta property="og:site_name" content={site.title} />
40+
41+
<!-- Twitter Card -->
42+
<meta name="twitter:card" content="summary_large_image" />
43+
<meta name="twitter:title" content={pageTitle} />
44+
<meta name="twitter:description" content={pageDescription} />
45+
<meta name="twitter:image" content={ogImage} />
46+
2547
<link rel="preconnect" href="https://fonts.googleapis.com" />
2648
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
2749
<link
2850
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;600;700&family=Roboto+Mono:wght@400;500&display=swap"
2951
rel="stylesheet"
3052
/>
53+
54+
{jsonLd && (
55+
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
56+
)}
3157
</head>
3258
<body>
3359
<a href="#main-content" class="skip-link">Skip to main content</a>

astro-site/src/pages/[...slug].astro

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,35 @@ export async function getStaticPaths() {
1919
const { page, entry } = Astro.props;
2020
const { Content, headings } = await render(entry);
2121
const sectionPath = `/${page.section}/`;
22+
23+
const jsonLd = {
24+
"@context": "https://schema.org",
25+
"@type": "Article",
26+
"headline": page.title,
27+
"description": entry.data.description || "",
28+
"author": {
29+
"@type": "Person",
30+
"name": "Branimir Georgiev",
31+
"url": "https://github.com/braboj"
32+
},
33+
"publisher": {
34+
"@type": "Person",
35+
"name": "Branimir Georgiev"
36+
},
37+
"isPartOf": {
38+
"@type": "Course",
39+
"name": "Git Tutorial — Code with Branko",
40+
"url": "https://braboj.github.io/tutorial-git/"
41+
}
42+
};
2243
---
2344

24-
<DocLayout title={page.title} headings={headings} currentPath={sectionPath}>
45+
<DocLayout
46+
title={page.title}
47+
description={entry.data.description}
48+
headings={headings}
49+
currentPath={sectionPath}
50+
jsonLd={jsonLd}
51+
>
2552
<Content />
2653
</DocLayout>

astro-site/src/pages/index.astro

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,31 @@ const headings = [
1010
{ depth: 2, slug: "about", text: "About" },
1111
{ depth: 2, slug: "selected-references", text: "Selected references" },
1212
];
13+
14+
const jsonLd = {
15+
"@context": "https://schema.org",
16+
"@type": "Course",
17+
"name": "Git Tutorial — Code with Branko",
18+
"description": "A hands-on Git tutorial from first commit to confident daily use — concepts, exercises, and real-world examples.",
19+
"url": "https://braboj.github.io/tutorial-git/",
20+
"provider": {
21+
"@type": "Person",
22+
"name": "Branimir Georgiev",
23+
"url": "https://github.com/braboj"
24+
},
25+
"educationalLevel": "Beginner to Advanced",
26+
"inLanguage": "en",
27+
"isAccessibleForFree": true
28+
};
1329
---
1430

15-
<DocLayout title="Let's Learn Git!" headings={headings} currentPath="/">
31+
<DocLayout
32+
title="Let's Learn Git!"
33+
description="A hands-on Git tutorial from first commit to confident daily use — concepts, exercises, and real-world examples."
34+
headings={headings}
35+
currentPath="/"
36+
jsonLd={jsonLd}
37+
>
1638

1739
<p>
1840
A hands-on tutorial that takes you from first commit to confident daily use.

chapters/01-introduction.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
title: "Introduction"
3+
description: "What Git is, how to install it, how it works internally, and an overview of the most common commands."
34
section: "introduction"
45
order: 1
56
---

chapters/02-building-blocks.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
title: "Building Blocks"
3+
description: "Git internals — repositories, the object model (blobs, trees, commits, tags), the staging area, references, and HEAD."
34
section: "building-blocks"
45
order: 2
56
---

0 commit comments

Comments
 (0)