feat: enhance SEO and structured data across multiple components; update site description and improve sitemap and RSS feed

This commit is contained in:
cojocaru-david
2025-07-16 17:46:19 +03:00
parent cf4090da79
commit 0a8ceb09ad
15 changed files with 643 additions and 492 deletions

View File

@@ -8,6 +8,7 @@ import {
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb'
import { Icon } from 'astro-icon/components'
import { SITE } from '@/consts'
export interface BreadcrumbItem {
href?: string
@@ -20,8 +21,30 @@ interface Props {
}
const { items } = Astro.props
const breadcrumbStructuredData = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": SITE.href
},
...items.map((item, index) => ({
"@type": "ListItem",
"position": index + 2,
"name": item.label,
...(item.href && { "item": new URL(item.href, SITE.href).toString() })
}))
]
}
---
<!-- Breadcrumb Structured Data -->
<script type="application/ld+json" is:inline set:html={JSON.stringify(breadcrumbStructuredData)} />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>

View File

@@ -11,19 +11,29 @@ import Favicons from './Favicons.astro'
content="width=device-width, initial-scale=1.0, user-scalable=yes"
/>
<meta name="generator" content={Astro.generator} />
<meta name="robots" content="index, follow" />
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1" />
<meta name="googlebot" content="index, follow" />
<meta name="google" content="notranslate" />
<meta name="revisit-after" content="1 days" />
<!-- SEO Enhancement Tags -->
<meta name="author" content={SITE.author} />
<meta name="publisher" content={SITE.title} />
<meta name="language" content={SITE.locale} />
<meta name="geo.region" content="RO" />
<meta name="geo.placename" content={SITE.location} />
<!-- Mobile and PWA Tags -->
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content={SITE.title} />
<meta
name="format-detection"
content="telephone=no,date=no,address=no,email=no,url=no"
/>
<!-- Theme Colors -->
<meta
name="theme-color"
content="#121212"
@@ -31,16 +41,22 @@ import Favicons from './Favicons.astro'
/>
<meta
name="theme-color"
content="#121212"
content="#ffffff"
media="(prefers-color-scheme: light)"
/>
<meta name="msapplication-TileColor" content="#121212" />
<!-- Prefetch and Preconnect for Performance -->
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
<!-- Sitemap and Feed Links -->
<link rel="sitemap" href="/sitemap-index.xml" />
<link rel="manifest" href="/site.webmanifest" />
<link
rel="alternate"
type="application/rss+xml"
title={SITE.title}
title={`${SITE.title} RSS Feed`}
href={new URL('rss.xml', Astro.site)}
/>

View File

@@ -9,60 +9,91 @@ interface Props {
const { title = SITE.title, description = SITE.description } = Astro.props
const image = new URL('/ogImage.png', Astro.site).toString()
const posts = await getAllPosts()
// Optimize description for SEO
const optimizedDescription = description.length > 160
? description.substring(0, 157) + '...'
: description
// Create proper page title
const pageTitle = title === SITE.title ? SITE.title : `${title} | ${SITE.title}`
---
<title>{`${SITE.title} - ${title}`}</title>
<meta name="description" content={description} />
<title>{pageTitle}</title>
<meta name="description" content={optimizedDescription} />
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1" />
<meta name="language" content={SITE.locale} />
<link rel="canonical" href={Astro.url} />
<!-- Open Graph Meta Tags -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:description" content={optimizedDescription} />
<meta property="og:image" content={image} />
<meta property="og:image:alt" content={title} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:type" content="website" />
<meta property="og:locale" content={SITE.locale} />
<meta property="og:site_name" content={SITE.title} />
<meta property="og:url" content={Astro.url} />
<!-- Twitter Card Meta Tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:description" content={optimizedDescription} />
<meta name="twitter:image" content={image} />
<meta name="twitter:image:alt" content={title} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:creator" content={SITE.author} />
<meta name="twitter:site" content={SITE.href} />
<meta name="twitter:domain" content={SITE.href} />
<meta name="twitter:creator" content={`@${SITE.author.replace(' ', '').toLowerCase()}`} />
<meta name="twitter:site" content={`@${SITE.author.replace(' ', '').toLowerCase()}`} />
<meta name="twitter:domain" content={new URL(SITE.href).hostname} />
<!-- Enhanced JSON-LD Structured Data -->
<script
type="application/ld+json"
is:inline
set:html={`{
set:html={JSON.stringify({
"@context": "https://schema.org",
"@type": "WebSite",
"@id": "${SITE.href}#website",
"url": "${SITE.href}",
"name": "${SITE.title}",
"description": "${description}",
"@id": `${SITE.href}#website`,
"url": SITE.href,
"name": SITE.title,
"description": optimizedDescription,
"inLanguage": SITE.locale,
"publisher": {
"@type": "Person",
"name": SITE.author,
"url": SITE.href
},
"potentialAction": {
"@type": "SearchAction",
"target": "${SITE.href}/search?q={search_term_string}",
"target": {
"@type": "EntryPoint",
"urlTemplate": `${SITE.href}/blog?q={search_term_string}`
},
"query-input": "required name=search_term_string"
},
"blogPosts": [
${posts
.map(
(post) => `{
"mainEntity": {
"@type": "Blog",
"@id": `${SITE.href}/blog#blog`,
"name": `${SITE.title} Blog`,
"description": "Technology blog covering web development, programming, and software engineering topics",
"url": `${SITE.href}/blog`,
"author": {
"@type": "Person",
"name": SITE.author
},
"blogPost": posts.slice(0, 10).map(post => ({
"@type": "BlogPosting",
"headline": "${post.data.title}",
"description": "${post.data.description}",
"url": "${SITE.href}/blog/${post.id}",
"datePublished": "/blog/${post.id}",
"headline": post.data.title,
"description": post.data.description,
"url": `${SITE.href}/blog/${post.id}/`,
"datePublished": post.data.date.toISOString(),
"author": {
"@type": "Person",
"name": "${SITE.author}"
}
}`
)
.join(',')}
]
}`}
"name": post.data.authors ? post.data.authors[0] : SITE.author
},
"keywords": post.data.tags ? post.data.tags.join(', ') : ''
}))
}
})}
/>

View File

@@ -10,36 +10,56 @@ const { post } = Astro.props
const title = post.data.title || SITE.title
const description = post.data.description || SITE.description
const postUrl = new URL(post.id, SITE.href).toString()
const image = SITE.href + '/image/' + post.id + '.png';
const postUrl = new URL(`/blog/${post.id}/`, SITE.href).toString()
const image = new URL(`/image/${post.id}.png`, SITE.href).toString()
const author = post.data.authors ? post.data.authors.join(', ') : SITE.author
const optimizedDescription = description.length > 160
? description.substring(0, 157) + '...'
: description
const seoTitle = `${title} - ${SITE.title}`
const postUrlWithoutTrailingSlash = postUrl.endsWith('/') ? postUrl.slice(0, -1) : postUrl
const postCanonicalUrl = new URL(postUrlWithoutTrailingSlash, SITE.href).toString()
---
<title>{`${title} - ${SITE.title}`}</title>
<meta name="description" content={description} />
<title>{seoTitle}</title>
<meta name="description" content={optimizedDescription} />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="index, follow" />
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1" />
<meta name="author" content={author} />
<meta name="publisher" content={SITE.title} />
<meta name="language" content={SITE.locale} />
<link rel="canonical" href={postCanonicalUrl} />
{post?.data.tags && <meta name="keywords" content={post.data.tags.join(', ')} />}
<!-- Open Graph Meta Tags -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:description" content={optimizedDescription} />
<meta property="og:image" content={image} />
<meta property="og:image:alt" content={title} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:type" content="article" />
<meta property="og:locale" content={SITE.locale} />
<meta property="og:site_name" content={SITE.title} />
<meta property="og:url" content={postUrl} />
<meta property="og:author" content={author} />
<!-- Article specific Open Graph -->
<meta property="article:published_time" content={post.data.date.toISOString()} />
<meta property="article:modified_time" content={post.data.date.toISOString()} />
<meta property="article:published" content={post.data.date.toISOString()} />
<meta property="article:author" content={author} />
<meta property="article:publisher" content={SITE.title} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta property="twitter:image" content={image} />
<meta name="twitter:image:alt" content={title} />
<meta property="article:section" content="Technology" />
<!-- Twitter Card Meta Tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:creator" content={author} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={optimizedDescription} />
<meta name="twitter:image" content={image} />
<meta name="twitter:image:alt" content={title} />
<meta name="twitter:creator" content={`@${SITE.author.replace(' ', '').toLowerCase()}`} />
<meta name="twitter:site" content={`@${SITE.author.replace(' ', '').toLowerCase()}`} />
{
post?.data.tags &&
@@ -48,22 +68,32 @@ const author = post.data.authors ? post.data.authors.join(', ') : SITE.author
})
}
<!-- Enhanced JSON-LD Structured Data -->
<script type="application/ld+json" is:inline set:html={JSON.stringify({
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": title,
"description": description,
"image": image,
"description": optimizedDescription,
"image": {
"@type": "ImageObject",
"url": image,
"width": 1200,
"height": 630
},
"author": {
"@type": "Person",
"name": author
"name": author,
"url": SITE.href
},
"publisher": {
"@type": "Organization",
"name": SITE.title,
"url": SITE.href,
"logo": {
"@type": "ImageObject",
"url": new URL("/favicon.ico", SITE.href).toString()
"url": new URL("/favicon.ico", SITE.href).toString(),
"width": 32,
"height": 32
}
},
"datePublished": post.data.date.toISOString(),
@@ -73,5 +103,15 @@ const author = post.data.authors ? post.data.authors.join(', ') : SITE.author
"@id": postUrl
},
"keywords": post?.data.tags ? post.data.tags.join(', ') : '',
"url": postUrl
"url": postUrl,
"inLanguage": SITE.locale,
"isPartOf": {
"@type": "Blog",
"@id": new URL("/blog/", SITE.href).toString(),
"name": `${SITE.title} Blog`
},
"about": post?.data.tags ? post.data.tags.map(tag => ({
"@type": "Thing",
"name": tag
})) : []
})} />

View File

@@ -80,25 +80,6 @@ const Navbar = () => {
4: { width: '50%' },
}
const menuVariants = {
closed: {
opacity: 0,
y: "-100%",
transition: {
duration: 0.5,
ease: [0.22, 1, 0.36, 1],
}
},
open: {
opacity: 1,
y: "0%",
transition: {
duration: 0.5,
ease: [0.22, 1, 0.36, 1],
}
}
}
return (
<>
<motion.header
@@ -201,7 +182,6 @@ const Navbar = () => {
initial="closed"
animate="open"
exit="closed"
variants={menuVariants}
className="fixed inset-0 z-20 flex flex-col items-center justify-start bg-background border-0 shadow-none"
>
<div className="flex flex-col items-center justify-start h-full pt-24 w-full p-6">

View File

@@ -3,7 +3,7 @@ import type { IconMap, SocialLink, Site } from '@/types'
export const SITE: Site = {
title: 'Cojocaru David',
description:
"I'm a Junior Full Stack Developer with a passion for creating web applications. I have experience in both front-end and back-end development, and I'm always eager to learn new technologies and improve my skills. I enjoy collaborating with teams and contributing to projects that make a difference.",
"Junior Full Stack Developer specializing in modern web technologies. Expert in React, Node.js, TypeScript, and cloud development. Read my latest tech tutorials, project insights, and programming tips on web development, DevOps, and software engineering best practices.",
href: 'https://cojocarudavid.me',
author: 'Cojocaru David',
locale: 'en-US',

View File

@@ -10,8 +10,7 @@ import { cn } from '@/lib/utils'
import Posthog from '@/components/Posthog.astro'
const {
isWide = false,
canonicalUrl = SITE.href
isWide = false
} = Astro.props
---
@@ -19,8 +18,16 @@ const {
<html lang={SITE.locale}>
<Head>
<slot name="head" />
<script src="https://analytics.ahrefs.com/analytics.js" data-key="+FHMgRP7/Duxaq5D0gZtJw" async></script>
<script is:inline src="https://analytics.ahrefs.com/analytics.js" data-key="+FHMgRP7/Duxaq5D0gZtJw" async></script>
<link rel="sitemap" href="/sitemap.xml" />
<!-- Preload critical resources -->
<link rel="preload" href="/fonts/GeistVF.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/fonts/GeistMonoVF.woff2" as="font" type="font/woff2" crossorigin />
<!-- DNS prefetch for external resources -->
<link rel="dns-prefetch" href="//analytics.ahrefs.com" />
<script is:inline data-astro-rerun>
(function() {
try {
@@ -43,7 +50,6 @@ const {
}
})();
</script>
<link rel="canonical" href={canonicalUrl} />
<link rel="alternate" type="application/rss+xml" title="RSS Feed" href="/rss.xml" />
<Posthog />
</Head>

View File

@@ -9,21 +9,48 @@ const currentUrl = Astro.url;
---
<Layout canonicalUrl={currentUrl}>
<PageHead slot="head" title="404" />
<PageHead
slot="head"
title="404 - Page Not Found"
description="The page you're looking for couldn't be found. Return to the homepage to explore Cojocaru David's latest tech articles, projects, and programming insights."
/>
<!-- Add 404 structured data -->
<script type="application/ld+json" is:inline set:html={JSON.stringify({
"@context": "https://schema.org",
"@type": "WebPage",
"name": "404 - Page Not Found",
"description": "The requested page could not be found on this website.",
"url": currentUrl.toString(),
"mainEntity": {
"@type": "Thing",
"name": "404 Error",
"description": "Page not found error"
}
})} />
<section
class="flex w-full flex-col items-center justify-center gap-y-4 text-center"
>
<div class="max-w-md">
<h1 class="mb-4 text-3xl font-medium">404: Page not found</h1>
<p class="prose">Oops! The page you're looking for doesn't exist.</p>
<h1 class="mb-4 text-3xl font-medium">404: Page Not Found</h1>
<p class="prose mb-4">Oops! The page you're looking for doesn't exist. It might have been moved, deleted, or you entered the wrong URL.</p>
<p class="prose text-sm text-muted-foreground">Looking for something specific? Try searching our blog or browse our latest articles.</p>
</div>
<div class="flex flex-col sm:flex-row gap-3">
<Link
href="/"
class={cn(buttonVariants({ variant: 'default' }), 'flex gap-x-1.5 group')}
>
<span class="transition-transform group-hover:-translate-x-1">&larr;</span>
Go to Home
</Link>
<Link
href="/blog"
class={cn(buttonVariants({ variant: 'outline' }), 'flex gap-x-1.5')}
>
Browse Blog
</Link>
</div>
<Link
href="/"
class={cn(buttonVariants({ variant: 'outline' }), 'flex gap-x-1.5 group')}
>
<span class="transition-transform group-hover:-translate-x-1">&larr;</span
> Go to home page
</Link>
</section>
</Layout>

View File

@@ -41,10 +41,10 @@ const structuredData = {
keywords: post.data.tags ? post.data.tags.join(', ') : '',
description: post.data.description || '',
}
const currentUrl = Astro.url;
const canonicalUrl = new URL(`/blog/${post.id}/`, Astro.site).toString();
---
<Layout canonicalUrl={currentUrl} isWide={true}>
<Layout canonicalUrl={canonicalUrl} isWide={true}>
<PostHead slot="head" post={post} />
<script

View File

@@ -1,10 +1,24 @@
import type { APIRoute } from 'astro'
import { SITE } from '@/consts'
const getRobotsTxt = (sitemapURL: URL) => `
User-agent: *
Allow: /
# Block access to admin or private directories (if any exist)
Disallow: /api/
Disallow: /_astro/
Disallow: /temp/
# Crawl delay for better server performance
Crawl-delay: 1
# Sitemap location
Sitemap: ${sitemapURL.href}
Sitemap: ${new URL('sitemap-index.xml', SITE.href).href}
# Additional information
# Host: ${SITE.href}
`
export const GET: APIRoute = ({ site }) => {

View File

@@ -8,15 +8,33 @@ export async function GET(context: APIContext) {
const posts = await getAllPosts()
return rss({
title: SITE.title,
title: `${SITE.title} - Tech Blog`,
description: SITE.description,
site: context.site ?? SITE.href,
trailingSlash: false,
items: posts.map((post) => ({
title: post.data.title,
description: post.data.description,
pubDate: post.data.date,
link: `/blog/${post.id}/`,
categories: post.data.tags || [],
author: post.data.authors ? post.data.authors.join(', ') : SITE.author,
customData: `
<language>${SITE.locale}</language>
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
${post.data.tags ? `<category>${post.data.tags.join(', ')}</category>` : ''}
`.trim(),
})),
customData: `
<language>${SITE.locale}</language>
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
<ttl>60</ttl>
<image>
<url>${new URL('/ogImage.png', SITE.href).toString()}</url>
<title>${SITE.title}</title>
<link>${SITE.href}</link>
</image>
`.trim(),
})
} catch (error) {
console.error('Error generating RSS feed:', error)

View File

@@ -18,16 +18,22 @@ export async function GET(context: APIContext) {
priority: '1.0'
},
{
url: `${baseUrl}/projects`,
url: `${baseUrl}/projects/`,
lastmod: new Date().toISOString(),
changefreq: 'weekly',
priority: '0.8'
},
{
url: `${baseUrl}/blog`,
url: `${baseUrl}/blog/`,
lastmod: posts.length > 0 ? posts[0].data.date.toISOString() : new Date().toISOString(),
changefreq: 'daily',
priority: '0.9'
},
{
url: `${baseUrl}/tags/`,
lastmod: new Date().toISOString(),
changefreq: 'weekly',
priority: '0.8'
priority: '0.6'
}
]
@@ -35,7 +41,7 @@ export async function GET(context: APIContext) {
url: `${baseUrl}/blog/${post.id}/`,
lastmod: post.data.date.toISOString(),
changefreq: 'monthly',
priority: '0.6'
priority: '0.7'
}))
const projectPosts = projects.map(project => ({
@@ -45,7 +51,7 @@ export async function GET(context: APIContext) {
priority: '0.6'
}))
const tagUrls = Array.from(tags, ([tag]) => ({
const tagUrls = Array.from(tags, ([tag, _]) => ({
url: `${baseUrl}/tags/${tag}/`,
lastmod: new Date().toISOString(),
changefreq: 'weekly',