Skip to content

Commit f7cb93b

Browse files
author
Frank Goossen
committed
Add SEO
1 parent ac9be27 commit f7cb93b

12 files changed

Lines changed: 1110 additions & 35 deletions

File tree

apps/dotnet/public/robots.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
User-agent: *
2+
Allow: /
3+
4+
# Sitemap
5+
Sitemap: https://xprtz.net/sitemap-index.xml
6+
Sitemap: https://xprtz.net/sitemap-0.xml
7+
8+
# Crawl delay (optional - adjust based on your server capacity)
9+
Crawl-delay: 1
10+
11+
# Disallow admin or private areas (if any)
12+
# Disallow: /admin/
13+
# Disallow: /api/
14+
15+
# Allow important pages
16+
Allow: /artikelen/
17+
Allow: /images/
18+
Allow: /fonts/

apps/dotnet/src/layouts/layout.astro

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,80 @@
11
---
2-
import { BaseHead, Footer, Header } from '@xprtz/ui'
3-
import Logo from '../images/logo_met_tekst.svg'
4-
const { title, description } = Astro.props;
2+
import {
3+
BaseHead,
4+
Footer,
5+
Header,
6+
type SEOData,
7+
generateSEOData,
8+
generateWebsiteStructuredData,
9+
} from "@xprtz/ui";
10+
import Logo from "../images/logo_met_tekst.svg";
11+
12+
interface Props {
13+
title: string;
14+
description: string;
15+
image?: string;
16+
imageAlt?: string;
17+
type?: "website" | "article";
18+
author?: string;
19+
publishedTime?: string;
20+
tags?: string[];
21+
seoData?: Partial<SEOData>;
22+
}
23+
24+
const {
25+
title,
26+
description,
27+
image,
28+
imageAlt,
29+
type = "website",
30+
author,
31+
publishedTime,
32+
tags,
33+
seoData,
34+
} = Astro.props;
35+
36+
// Generate base structured data for organization/website
37+
const baseStructuredData = generateWebsiteStructuredData({
38+
name: "XPRTZ",
39+
url: Astro.site?.toString() || "https://xprtz.net",
40+
description:
41+
"Expert consultancy in .NET, Azure, and modern software development",
42+
logo: `${Astro.site}${Logo.src}`,
43+
sameAs: [
44+
"https://www.linkedin.com/company/xprtz",
45+
"https://github.com/xprtz",
46+
"https://instagram.com/workwithxprtz",
47+
"https://www.youtube.com/channel/UCFUV8Q5RgFMwN-XM_vkwT3g",
48+
],
49+
});
50+
51+
// Generate SEO data with all the props
52+
const finalSEOData = generateSEOData({
53+
title: title,
54+
description: description,
55+
image: image,
56+
imageAlt: imageAlt,
57+
type: type,
58+
siteName: "XPRTZ",
59+
structuredData: baseStructuredData,
60+
additionalKeywords: tags,
61+
...seoData,
62+
});
63+
64+
// Override specific properties if provided
65+
if (author) finalSEOData.author = author;
66+
if (publishedTime) finalSEOData.publishedTime = publishedTime;
67+
if (tags) finalSEOData.tags = tags;
568
---
6-
<html lang="en">
69+
70+
<html lang="nl">
771
<head>
8-
<meta charset="utf-8">
9-
<meta name="viewport" content="width=device-width, initial-scale=1">
10-
<BaseHead title={title} description={description} />
72+
<meta charset="utf-8" />
73+
<meta name="viewport" content="width=device-width, initial-scale=1" />
74+
<BaseHead {...finalSEOData} />
1175
</head>
1276
<body>
13-
<Header Logo={Logo.src} client:load/>
77+
<Header Logo={Logo.src} client:load />
1478
<main class="isolate">
1579
<slot />
1680
</main>

apps/dotnet/src/pages/artikelen/[article].astro

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
---
22
import Layout from "../../layouts/layout.astro";
33
import { fetchData, type Article } from "@xprtz/cms";
4-
import { ContentAstro as Content } from "@xprtz/ui";
5-
4+
import {
5+
ContentAstro as Content,
6+
generateArticleSEOData,
7+
generateArticleStructuredData,
8+
} from "@xprtz/ui";
69
710
export async function getStaticPaths() {
811
const site = import.meta.env.PUBLIC_SITE || "no-site-found";
@@ -16,7 +19,7 @@ export async function getStaticPaths() {
1619
"populate[authors][fields]": "*",
1720
"populate[authors][populate][avatar][fields][0]=url": "url",
1821
"populate[image][fields][0]": "url",
19-
"populate[tags][fields][0]":"title",
22+
"populate[tags][fields][0]": "title",
2023
status: "published",
2124
},
2225
});
@@ -28,8 +31,47 @@ export async function getStaticPaths() {
2831
}
2932
3033
const article: Article = Astro.props;
34+
35+
// Environment variables
36+
const imagesUrl = import.meta.env.PUBLIC_IMAGES_URL || "";
37+
const siteUrl = Astro.site?.toString() || "https://xprtz.net";
38+
39+
// Generate structured data for the article
40+
const structuredData = generateArticleStructuredData({
41+
article,
42+
siteUrl,
43+
siteName: "XPRTZ",
44+
imagesUrl,
45+
});
46+
47+
// Generate comprehensive SEO data
48+
const seoData = generateArticleSEOData({
49+
article,
50+
imagesUrl,
51+
siteName: "XPRTZ",
52+
structuredData,
53+
});
54+
55+
// Extract primary author info
56+
const primaryAuthor = article.authors?.[0];
57+
const authorName = primaryAuthor
58+
? `${primaryAuthor.firstname} ${primaryAuthor.lastname}`
59+
: undefined;
60+
61+
// Extract tag names for keywords
62+
const tagNames = article.tags?.map((tag) => tag.title) || [];
3163
---
3264

33-
<Layout title={article.title} description={article.title}>
65+
<Layout
66+
title={article.title}
67+
description={article.title}
68+
image={article.image ? `${imagesUrl}${article.image.url}` : undefined}
69+
imageAlt={article.image?.alternateText || article.title}
70+
type="article"
71+
author={authorName}
72+
publishedTime={article.date}
73+
tags={tagNames}
74+
seoData={seoData}
75+
>
3476
<Content article={article} />
3577
</Layout>

apps/dotnet/src/pages/artikelen/page/[page].astro

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
import type { GetStaticPaths } from "astro";
33
import { fetchData, type Article, type Page as PageType } from "@xprtz/cms";
44
import Layout from "../../../layouts/layout.astro";
5-
import { Container, Blogs } from "@xprtz/ui";
5+
import {
6+
Container,
7+
Blogs,
8+
SEOTools,
9+
generateWebsiteStructuredData,
10+
} from "@xprtz/ui";
611
712
interface PageData {
813
data: Article[];
@@ -44,6 +49,8 @@ export const getStaticPaths: GetStaticPaths = async ({ paginate }) => {
4449
};
4550
4651
const site = import.meta.env.PUBLIC_SITE || "no-site-found";
52+
const siteUrl = Astro.site?.toString() || "https://xprtz.net";
53+
4754
const articlesPages = await fetchData<Array<PageType>>({
4855
endpoint: "pages",
4956
wrappedByKey: "data",
@@ -55,14 +62,66 @@ const articlesPages = await fetchData<Array<PageType>>({
5562
});
5663
5764
const { page } = Astro.props as { page: PageData };
58-
5965
const articlesPage = articlesPages[0];
66+
67+
// Generate pagination URLs
68+
const prevPageUrl = page.url.prev ? `${siteUrl}${page.url.prev}` : undefined;
69+
const nextPageUrl = page.url.next ? `${siteUrl}${page.url.next}` : undefined;
70+
71+
// Generate structured data for the blog listing page
72+
const structuredData = generateWebsiteStructuredData({
73+
name: `${articlesPage.title_website} - Pagina ${page.currentPage}`,
74+
url: `${siteUrl}${page.url.current}`,
75+
description: `${articlesPage.description} - Pagina ${page.currentPage} van ${page.lastPage}`,
76+
});
77+
78+
// Generate breadcrumbs
79+
const breadcrumbs = [
80+
{ name: "Home", url: siteUrl },
81+
{ name: articlesPage.title_website, url: `${siteUrl}/artikelen/` },
82+
...(Number(page.currentPage) > 1
83+
? [
84+
{
85+
name: `Pagina ${page.currentPage}`,
86+
url: `${siteUrl}${page.url.current}`,
87+
},
88+
]
89+
: []),
90+
];
91+
92+
// Create page-specific title and description
93+
const pageTitle =
94+
Number(page.currentPage) > 1
95+
? `${articlesPage.title_website} - Pagina ${page.currentPage}`
96+
: articlesPage.title_website;
97+
98+
const pageDescription =
99+
Number(page.currentPage) > 1
100+
? `${articlesPage.description} - Pagina ${page.currentPage} van ${page.lastPage}`
101+
: articlesPage.description;
60102
---
61103

62104
<Layout
63-
title={articlesPage.title_website}
64-
description={articlesPage.description}
105+
title={pageTitle}
106+
description={pageDescription}
107+
type="website"
108+
seoData={{
109+
structuredData,
110+
keywords: [
111+
"articles",
112+
"blog posts",
113+
".NET development",
114+
"software engineering",
115+
"programming tutorials",
116+
],
117+
}}
65118
>
119+
<SEOTools
120+
prevPage={prevPageUrl}
121+
nextPage={nextPageUrl}
122+
breadcrumbs={breadcrumbs}
123+
/>
124+
66125
<Container>
67126
<svg
68127
class="absolute inset-x-0 top-0 -z-10 h-[64rem] w-full stroke-gray-200 [mask-image:radial-gradient(32rem_32rem_at_center,white,transparent)]"
@@ -113,11 +172,11 @@ const articlesPage = articlesPages[0];
113172
<h1
114173
class="mt-2 text-pretty text-4xl font-semibold tracking-tight text-primary-800 sm:text-5xl"
115174
>
116-
{articlesPage.title_website}
175+
{pageTitle}
117176
</h1>
118-
<p class="mt-6 text-xl/8">{articlesPage.description}</p>
177+
<p class="mt-6 text-xl/8">{pageDescription}</p>
119178
<div class="mt-10 max-w-7xl text-base/7">
120-
<Blogs page={page}/>
179+
<Blogs page={page} />
121180
</div>
122181
</div>
123182
</div>

apps/dotnet/src/pages/index.astro

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
---
22
import { fetchData, type HomePage } from "@xprtz/cms";
3-
import { ComponentRenderer } from "@xprtz/ui";
3+
import {
4+
ComponentRenderer,
5+
generateWebsiteStructuredData,
6+
generateOrganizationStructuredData,
7+
} from "@xprtz/ui";
48
59
import Layout from "../layouts/layout.astro";
610
711
const site = import.meta.env.PUBLIC_SITE || "no-site-found";
12+
const siteUrl = Astro.site?.toString() || "https://xprtz.net";
813
914
const homepages = await fetchData<Array<HomePage>>({
1015
endpoint: "homepages",
@@ -19,7 +24,54 @@ const homepage = homepages[0];
1924
2025
const title = homepage.title;
2126
const description = homepage.description;
27+
28+
// Generate structured data for the homepage
29+
const websiteStructuredData = generateWebsiteStructuredData({
30+
name: "XPRTZ",
31+
url: siteUrl,
32+
description: description,
33+
sameAs: [
34+
"https://www.linkedin.com/company/xprtz",
35+
"https://github.com/xprtz",
36+
"https://instagram.com/workwithxprtz",
37+
"https://www.youtube.com/channel/UCFUV8Q5RgFMwN-XM_vkwT3g",
38+
],
39+
});
40+
41+
// Generate Organization structured data for rich results
42+
const organizationStructuredData = generateOrganizationStructuredData({
43+
name: "XPRTZ",
44+
url: siteUrl,
45+
logo: `${siteUrl}/images/logo.svg`,
46+
description:
47+
"XPRTZ is a leading .NET and Azure consulting company specializing in enterprise software development, cloud solutions, and Microsoft technology stack implementations.",
48+
sameAs: [
49+
"https://www.linkedin.com/company/xprtz",
50+
"https://github.com/xprtz",
51+
"https://instagram.com/workwithxprtz",
52+
"https://www.youtube.com/channel/UCFUV8Q5RgFMwN-XM_vkwT3g",
53+
],
54+
});
55+
56+
// Add keywords related to your business
57+
const keywords = [
58+
".NET development",
59+
"Azure consulting",
60+
"Software development",
61+
"Cloud solutions",
62+
"Microsoft technology",
63+
"Enterprise applications",
64+
];
2265
---
23-
<Layout title={title} description={description}>
66+
67+
<Layout
68+
title={title}
69+
description={description}
70+
type="website"
71+
seoData={{
72+
structuredData: [websiteStructuredData, organizationStructuredData],
73+
keywords,
74+
}}
75+
>
2476
<ComponentRenderer components={homepage.components} />
2577
</Layout>

0 commit comments

Comments
 (0)