Skip to content

Commit cb9f761

Browse files
committed
chore: improve seo
1 parent fe0cc0a commit cb9f761

10 files changed

Lines changed: 374 additions & 4 deletions

File tree

messages/en.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
"$schema": "https://inlang.com/schema/inlang-message-format",
33
"site_title": "DevSteps",
44
"site_tagline": "Community-written free technology courses",
5+
"meta_home_title": "DevSteps | Community-written 7 & 30 day tech courses",
6+
"meta_home_description": "Join DevSteps to follow focused 7 and 30 day learning paths written by developers. Practice daily with free projects and community feedback.",
7+
"meta_browse_title": "Browse DevSteps courses | Filter 7 and 30 day learning paths",
8+
"meta_browse_description": "Search community-written DevSteps courses by duration, language and technology to craft your next 7 or 30 day learning plan.",
9+
"meta_course_title": "{title} | DevSteps course",
10+
"meta_course_description": "Learn {title} in {duration} days with {lessons} community-written lessons, projects and daily practice on DevSteps.",
11+
"meta_lesson_title": "{title} | DevSteps lesson",
12+
"meta_lesson_description": "{title} lesson from {courseTitle}. {description}",
513
"hero_badge": "✨ Community-written free technology courses",
614
"hero_title": "Learn the technology you need in 7 and 30 days",
715
"hero_description": "DevSteps is a project-focused, completely free learning platform that takes you one step further every day with short-term written courses.",

messages/tr.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
"$schema": "https://inlang.com/schema/inlang-message-format",
33
"site_title": "DevSteps",
44
"site_tagline": "Topluluk tarafından yazılan ücretsiz teknoloji kursları",
5+
"meta_home_title": "DevSteps | Topluluk yazımı 7 ve 30 günlük teknoloji kursları",
6+
"meta_home_description": "Geliştiricilerin yazdığı 7 ve 30 günlük öğrenme yollarıyla DevSteps'te ücretsiz ilerle, her gün proje ve topluluk desteğiyle pratik yap.",
7+
"meta_browse_title": "DevSteps kurslarını keşfet | 7 ve 30 günlük planları filtrele",
8+
"meta_browse_description": "Süre, dil ve teknolojiye göre topluluk yazımı DevSteps kurslarını filtreleyerek bir sonraki 7 veya 30 günlük öğrenme planını oluştur.",
9+
"meta_course_title": "{title} | DevSteps kursu",
10+
"meta_course_description": "{duration} günde {title} öğren; {lessons} topluluk yazımı ders, projeler ve günlük egzersizlerle DevSteps'te.",
11+
"meta_lesson_title": "{title} | DevSteps dersi",
12+
"meta_lesson_description": "{courseTitle} kursunun {title} dersi. {description}",
513
"hero_badge": "✨ Topluluk tarafından yazılan ücretsiz teknoloji kursları",
614
"hero_title": "7 ve 30 günde ihtiyacın olan teknolojiyi öğren",
715
"hero_description": "DevSteps, kısa süreli yazılı kurslarla seni her gün bir adım ileri taşıyan, proje odaklı ve tamamen ücretsiz bir öğrenme platformu.",

src/lib/seo.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
const DEFAULT_SITE_URL = 'https://www.devsteps.net';
2+
3+
const rawSiteUrl =
4+
typeof import.meta !== 'undefined' && import.meta.env && 'PUBLIC_SITE_URL' in import.meta.env
5+
? (import.meta.env.PUBLIC_SITE_URL as string)
6+
: DEFAULT_SITE_URL;
7+
8+
export const siteUrl = (rawSiteUrl || DEFAULT_SITE_URL).replace(/\/+$/, '');
9+
10+
export const supportedLocales = ['en', 'tr'] as const;
11+
12+
export const toCanonical = (path: string) => {
13+
const normalized = path.startsWith('/') ? path : `/${path}`;
14+
return `${siteUrl}${normalized === '/' ? '' : normalized}`;
15+
};
16+
17+
export const serializeLdJson = (schema: Record<string, unknown>) =>
18+
JSON.stringify(schema, null, 2);
19+
20+
export const baseOrganizationSchema = {
21+
'@context': 'https://schema.org',
22+
'@type': 'Organization',
23+
name: 'DevSteps',
24+
url: siteUrl,
25+
logo: `${siteUrl}/favicon.svg`,
26+
sameAs: [
27+
'https://github.com/k61b/devsteps',
28+
'https://www.reddit.com/r/devsteps/'
29+
]
30+
};
31+
32+
export const baseWebsiteSchema = {
33+
'@context': 'https://schema.org',
34+
'@type': 'WebSite',
35+
name: 'DevSteps',
36+
url: siteUrl,
37+
potentialAction: {
38+
'@type': 'SearchAction',
39+
target: `${siteUrl}/browse-courses?query={search_term_string}`,
40+
'query-input': 'required name=search_term_string'
41+
}
42+
};
43+
44+
export const buildBreadcrumbSchema = (items: Array<{ name: string; url: string }>) => ({
45+
'@context': 'https://schema.org',
46+
'@type': 'BreadcrumbList',
47+
itemListElement: items.map((item, index) => ({
48+
'@type': 'ListItem',
49+
position: index + 1,
50+
name: item.name,
51+
item: item.url
52+
}))
53+
});
54+
55+
export const buildCourseSchema = (args: {
56+
name: string;
57+
description: string;
58+
url: string;
59+
providerName: string;
60+
totalLessons: number;
61+
durationDays: number;
62+
language: string;
63+
}) => ({
64+
'@context': 'https://schema.org',
65+
'@type': 'Course',
66+
name: args.name,
67+
description: args.description,
68+
url: args.url,
69+
inLanguage: args.language,
70+
provider: {
71+
'@type': 'Organization',
72+
name: args.providerName,
73+
sameAs: siteUrl
74+
},
75+
numberOfCredits: {
76+
'@type': 'StructuredValue',
77+
value: args.totalLessons,
78+
name: 'Lessons'
79+
},
80+
timeRequired: `P${args.durationDays}D`
81+
});
82+
83+
export const buildArticleSchema = (args: {
84+
title: string;
85+
description: string;
86+
url: string;
87+
dateModified: string;
88+
datePublished?: string;
89+
author?: string;
90+
}) => ({
91+
'@context': 'https://schema.org',
92+
'@type': 'Article',
93+
headline: args.title,
94+
description: args.description,
95+
url: args.url,
96+
dateModified: args.dateModified,
97+
datePublished: args.datePublished ?? args.dateModified,
98+
mainEntityOfPage: args.url,
99+
author: {
100+
'@type': 'Organization',
101+
name: args.author ?? 'DevSteps'
102+
},
103+
publisher: {
104+
'@type': 'Organization',
105+
name: 'DevSteps',
106+
logo: {
107+
'@type': 'ImageObject',
108+
url: `${siteUrl}/favicon.svg`
109+
}
110+
}
111+
});

src/routes/+layout.svelte

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,33 @@
11
<script lang="ts">
22
import favicon from '$lib/assets/favicon.svg';
33
import '../app.css';
4+
import * as m from '$lib/paraglide/messages';
5+
import { getLocale } from '$lib/paraglide/runtime';
6+
import {
7+
baseOrganizationSchema,
8+
baseWebsiteSchema,
9+
serializeLdJson
10+
} from '$lib/seo';
411
512
let { children } = $props();
13+
const locale = getLocale();
14+
const ogLocale = locale === 'tr' ? 'tr_TR' : 'en_US';
615
</script>
716

817
<svelte:head>
918
<link rel="icon" href={favicon} />
19+
<meta name="robots" content="index, follow" />
20+
<meta name="theme-color" content="#f97316" />
21+
<meta name="color-scheme" content="light" />
22+
<meta property="og:site_name" content={m.site_title()} />
23+
<meta property="og:locale" content={ogLocale} />
24+
<meta name="twitter:card" content="summary_large_image" />
25+
<script type="application/ld+json">
26+
{serializeLdJson(baseOrganizationSchema)}
27+
</script>
28+
<script type="application/ld+json">
29+
{serializeLdJson(baseWebsiteSchema)}
30+
</script>
1031
</svelte:head>
1132

1233
{@render children()}

src/routes/+page.svelte

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import * as m from '$lib/paraglide/messages';
66
import Navigation from '$lib/components/Navigation.svelte';
77
import { courses, courseColorClasses } from '$lib/data/courses';
8+
import { serializeLdJson, toCanonical } from '$lib/seo';
89
910
// Dialog for "Get Started" modal
1011
const {
@@ -87,8 +88,31 @@
8788
const featuredCourses = courses.filter((course) => course.featured).slice(0, 3);
8889
8990
const currentYear = new Date().getFullYear();
91+
const canonicalUrl = toCanonical('/');
92+
const homepageSchema = {
93+
'@context': 'https://schema.org',
94+
'@type': 'WebPage',
95+
name: m.site_title(),
96+
description: m.meta_home_description(),
97+
url: canonicalUrl
98+
};
9099
</script>
91100

101+
<svelte:head>
102+
<title>{m.meta_home_title()}</title>
103+
<meta name="description" content={m.meta_home_description()} />
104+
<link rel="canonical" href={canonicalUrl} />
105+
<meta property="og:type" content="website" />
106+
<meta property="og:title" content={m.meta_home_title()} />
107+
<meta property="og:description" content={m.meta_home_description()} />
108+
<meta property="og:url" content={canonicalUrl} />
109+
<meta name="twitter:title" content={m.meta_home_title()} />
110+
<meta name="twitter:description" content={m.meta_home_description()} />
111+
<script type="application/ld+json">
112+
{serializeLdJson(homepageSchema)}
113+
</script>
114+
</svelte:head>
115+
92116
<!-- Hero Section -->
93117
<section class="min-h-screen bg-amber-50 relative overflow-hidden">
94118
<!-- Decorative shapes -->

src/routes/browse-courses/+page.svelte

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import Navigation from '$lib/components/Navigation.svelte';
55
import Footer from '$lib/components/Footer.svelte';
66
import { courses as allCourses, courseColorClasses } from '$lib/data/courses';
7+
import { serializeLdJson, toCanonical } from '$lib/seo';
78
89
// Filters state
910
let searchQuery = '';
@@ -59,8 +60,31 @@
5960
6061
// Color classes for cards
6162
const colorClasses = courseColorClasses;
63+
const canonicalUrl = toCanonical('/browse-courses');
64+
const browseSchema = {
65+
'@context': 'https://schema.org',
66+
'@type': 'CollectionPage',
67+
name: m.meta_browse_title(),
68+
description: m.meta_browse_description(),
69+
url: canonicalUrl
70+
};
6271
</script>
6372

73+
<svelte:head>
74+
<title>{m.meta_browse_title()}</title>
75+
<meta name="description" content={m.meta_browse_description()} />
76+
<link rel="canonical" href={canonicalUrl} />
77+
<meta property="og:type" content="website" />
78+
<meta property="og:title" content={m.meta_browse_title()} />
79+
<meta property="og:description" content={m.meta_browse_description()} />
80+
<meta property="og:url" content={canonicalUrl} />
81+
<meta name="twitter:title" content={m.meta_browse_title()} />
82+
<meta name="twitter:description" content={m.meta_browse_description()} />
83+
<script type="application/ld+json">
84+
{serializeLdJson(browseSchema)}
85+
</script>
86+
</svelte:head>
87+
6488
<!-- Hero Section with Navigation -->
6589
<section class="bg-gradient-to-br from-pink-50 via-purple-50 to-blue-50 relative overflow-hidden">
6690
<!-- Decorative shapes -->

src/routes/courses/[id]/+page.svelte

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,21 @@
66
import Footer from '$lib/components/Footer.svelte';
77
import CourseAccordion from '$lib/components/CourseAccordion.svelte';
88
import * as m from '$lib/paraglide/messages';
9+
import { getLocale } from '$lib/paraglide/runtime';
10+
import {
11+
buildBreadcrumbSchema,
12+
buildCourseSchema,
13+
serializeLdJson,
14+
siteUrl,
15+
toCanonical
16+
} from '$lib/seo';
917
1018
export let data: PageData;
1119
1220
$: course = data.course;
1321
22+
const locale = getLocale();
23+
1424
// Calculate total lessons
1525
$: totalLessons = course.curriculum.reduce((acc, day) => acc + day.lessons.length, 0);
1626
@@ -156,11 +166,50 @@
156166
};
157167
158168
$: colors = colorMap[course.color] || colorMap.purple;
169+
170+
$: canonicalUrl = toCanonical(`/courses/${course.id}`);
171+
$: metaTitle = m.meta_course_title({ title: course.title });
172+
$: metaDescription = m.meta_course_description({
173+
title: course.title,
174+
duration: course.duration,
175+
lessons: totalLessons
176+
});
177+
$: breadcrumbSchema = buildBreadcrumbSchema([
178+
{ name: m.course_breadcrumb_home(), url: toCanonical('/') },
179+
{ name: m.course_breadcrumb_courses(), url: toCanonical('/browse-courses') },
180+
{ name: course.title, url: canonicalUrl }
181+
]);
182+
$: courseSchema = buildCourseSchema({
183+
name: course.title,
184+
description: course.description,
185+
url: canonicalUrl,
186+
providerName: m.site_title(),
187+
totalLessons,
188+
durationDays: course.duration,
189+
language: locale
190+
});
191+
$: lastUpdatedIso = new Date(course.lastUpdated).toISOString();
159192
</script>
160193

161194
<svelte:head>
162-
<title>{course.title} | DevSteps</title>
163-
<meta name="description" content={course.description} />
195+
<title>{metaTitle}</title>
196+
<meta name="description" content={metaDescription} />
197+
<link rel="canonical" href={canonicalUrl} />
198+
<meta property="og:type" content="website" />
199+
<meta property="og:title" content={metaTitle} />
200+
<meta property="og:description" content={metaDescription} />
201+
<meta property="og:url" content={canonicalUrl} />
202+
<meta property="og:image" content={`${siteUrl}/favicon.svg`} />
203+
<meta property="article:modified_time" content={lastUpdatedIso} />
204+
<meta name="twitter:title" content={metaTitle} />
205+
<meta name="twitter:description" content={metaDescription} />
206+
<meta name="twitter:image" content={`${siteUrl}/favicon.svg`} />
207+
<script type="application/ld+json">
208+
{serializeLdJson(breadcrumbSchema)}
209+
</script>
210+
<script type="application/ld+json">
211+
{serializeLdJson(courseSchema)}
212+
</script>
164213
</svelte:head>
165214

166215
<!-- Hero Section -->

0 commit comments

Comments
 (0)