auto-claude: subtask-3-1 - Audit existing JsonLd components and enhance struc
Enhanced JSON-LD structured data coverage with the following improvements: ## PersonJsonLd Enhancements: - Added hasOccupation with skills and occupationLocation - Added knowsLanguage (German, English, Serbian) - Added worksFor reference to business - Added nationality for geo-targeting - Enhanced knowsAbout with Claude AI, GPT-4, LangChain - Upgraded image to ImageObject with dimensions ## New Components: - ContactPageJsonLd: Contact page with ContactPoint entity - CollectionPageJsonLd: Portfolio page with ItemList for projects - ItemListJsonLd: Generic list schema for any items ## WebSiteJsonLd Enhancement: - Added SearchAction for sitelinks search box ## Page Updates: - Contact page now uses ContactPageJsonLd - Portfolio page now uses CollectionPageJsonLd and BreadcrumbJsonLd All schemas use @id cross-references for proper entity linking. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import { setRequestLocale } from 'next-intl/server';
|
||||
import type { Metadata } from 'next';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { ContactForm } from '@/components/contact';
|
||||
import { BreadcrumbJsonLd, WebPageJsonLd } from '@/components/seo';
|
||||
import { BreadcrumbJsonLd, WebPageJsonLd, ContactPageJsonLd } from '@/components/seo';
|
||||
import { type Locale } from '@/i18n/config';
|
||||
|
||||
type Props = {
|
||||
@@ -80,6 +80,7 @@ export default async function ContactPage({ params }: Props) {
|
||||
url={`/${locale}/contact`}
|
||||
locale={locale as Locale}
|
||||
/>
|
||||
<ContactPageJsonLd locale={locale as Locale} />
|
||||
<ContactForm />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,8 @@ import { setRequestLocale } from 'next-intl/server';
|
||||
import type { Metadata } from 'next';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { PortfolioGrid } from '@/components/portfolio';
|
||||
import { BreadcrumbJsonLd, CollectionPageJsonLd } from '@/components/seo';
|
||||
import { type Locale } from '@/i18n/config';
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ locale: string }>;
|
||||
@@ -165,8 +167,25 @@ export default async function PortfolioPage({ params }: Props) {
|
||||
|
||||
const t = await getTranslations('portfolio');
|
||||
|
||||
const breadcrumbItems = [
|
||||
{ name: locale === 'de' ? 'Start' : locale === 'sr' ? 'Početna' : 'Home', url: `/${locale}` },
|
||||
{ name: 'Portfolio', url: `/${locale}/portfolio` },
|
||||
];
|
||||
|
||||
// Transform projects for JSON-LD schema
|
||||
const portfolioProjects = projects.map((project) => ({
|
||||
name: project.title,
|
||||
description: project.description,
|
||||
url: `/${locale}/portfolio/${project.slug}`,
|
||||
dateCreated: project.date,
|
||||
technologies: project.technologies,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen">
|
||||
<BreadcrumbJsonLd items={breadcrumbItems} locale={locale as Locale} />
|
||||
<CollectionPageJsonLd locale={locale as Locale} projects={portfolioProjects} />
|
||||
|
||||
<section className="py-24">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
|
||||
@@ -25,7 +25,12 @@ export function PersonJsonLd({ locale }: JsonLdProps) {
|
||||
? 'Razvoj AI agenata i rešenja za automatizaciju. Od voice AI platformi do autonomnih web agenata.'
|
||||
: 'Remote AI developer building AI agents and automation solutions for clients in USA, UK and Europe. From voice AI platforms to autonomous web agents.',
|
||||
url: baseUrl,
|
||||
image: `${baseUrl}/images/og-image.jpg`,
|
||||
image: {
|
||||
'@type': 'ImageObject',
|
||||
url: `${baseUrl}/images/og-image.jpg`,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
email: 'info@damjan-savic.com',
|
||||
telephone: '+491756950979',
|
||||
address: {
|
||||
@@ -34,6 +39,33 @@ export function PersonJsonLd({ locale }: JsonLdProps) {
|
||||
addressRegion: 'NRW',
|
||||
addressCountry: 'DE',
|
||||
},
|
||||
// Languages known
|
||||
knowsLanguage: [
|
||||
{ '@type': 'Language', name: 'German', alternateName: 'de' },
|
||||
{ '@type': 'Language', name: 'English', alternateName: 'en' },
|
||||
{ '@type': 'Language', name: 'Serbian', alternateName: 'sr' },
|
||||
],
|
||||
// Current occupation
|
||||
hasOccupation: {
|
||||
'@type': 'Occupation',
|
||||
name: locale === 'de' ? 'Freelance KI-Entwickler' : locale === 'sr' ? 'Freelance AI Developer' : 'Freelance AI Developer',
|
||||
occupationLocation: {
|
||||
'@type': 'City',
|
||||
name: locale === 'sr' ? 'Keln' : locale === 'en' ? 'Cologne' : 'Köln',
|
||||
},
|
||||
skills: [
|
||||
'AI Development',
|
||||
'Process Automation',
|
||||
'Voice AI',
|
||||
'n8n Workflows',
|
||||
'SaaS Development',
|
||||
'Full-Stack Development',
|
||||
],
|
||||
},
|
||||
// Works for self (freelancer)
|
||||
worksFor: {
|
||||
'@id': `${baseUrl}/#business`,
|
||||
},
|
||||
sameAs: [
|
||||
'https://www.linkedin.com/in/damjansavic/',
|
||||
'https://github.com/damjansavic',
|
||||
@@ -50,6 +82,9 @@ export function PersonJsonLd({ locale }: JsonLdProps) {
|
||||
'Next.js',
|
||||
'n8n',
|
||||
'SaaS Development',
|
||||
'Claude AI',
|
||||
'GPT-4',
|
||||
'LangChain',
|
||||
],
|
||||
alumniOf: [
|
||||
{
|
||||
@@ -69,6 +104,11 @@ export function PersonJsonLd({ locale }: JsonLdProps) {
|
||||
credentialCategory: 'degree',
|
||||
},
|
||||
],
|
||||
// Nationality for geo-targeting
|
||||
nationality: {
|
||||
'@type': 'Country',
|
||||
name: 'Germany',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -719,7 +759,7 @@ export function SoftwareAppJsonLd({
|
||||
);
|
||||
}
|
||||
|
||||
// WebSite Schema for Search Box
|
||||
// WebSite Schema for Search Box with SearchAction
|
||||
export function WebSiteJsonLd({ locale }: JsonLdProps) {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
|
||||
|
||||
@@ -747,6 +787,197 @@ export function WebSiteJsonLd({ locale }: JsonLdProps) {
|
||||
{ '@type': 'Language', name: 'English', alternateName: 'en' },
|
||||
{ '@type': 'Language', name: 'Serbian', alternateName: 'sr' },
|
||||
],
|
||||
// Potential site search action (for future implementation)
|
||||
potentialAction: {
|
||||
'@type': 'SearchAction',
|
||||
target: {
|
||||
'@type': 'EntryPoint',
|
||||
urlTemplate: `${baseUrl}/${locale}/portfolio?search={search_term_string}`,
|
||||
},
|
||||
'query-input': 'required name=search_term_string',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ContactPage Schema for Contact Page
|
||||
export function ContactPageJsonLd({ locale }: JsonLdProps) {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
|
||||
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'ContactPage',
|
||||
'@id': `${baseUrl}/${locale}/contact/#webpage`,
|
||||
name: locale === 'de'
|
||||
? 'Kontakt - Damjan Savić'
|
||||
: locale === 'sr'
|
||||
? 'Kontakt - Damjan Savić'
|
||||
: 'Contact - Damjan Savić',
|
||||
description: locale === 'de'
|
||||
? 'Kontaktieren Sie Damjan Savić für KI-Entwicklung, Prozessautomatisierung und SaaS-Projekte.'
|
||||
: locale === 'sr'
|
||||
? 'Kontaktirajte Damjana Savića za AI razvoj, automatizaciju procesa i SaaS projekte.'
|
||||
: 'Contact Damjan Savić for AI development, process automation, and SaaS projects.',
|
||||
url: `${baseUrl}/${locale}/contact`,
|
||||
inLanguage: locale === 'de' ? 'de-DE' : locale === 'sr' ? 'sr-RS' : 'en-US',
|
||||
isPartOf: {
|
||||
'@id': `${baseUrl}/#website`,
|
||||
},
|
||||
about: {
|
||||
'@id': `${baseUrl}/#person`,
|
||||
},
|
||||
mainEntity: {
|
||||
'@type': 'ContactPoint',
|
||||
contactType: locale === 'de' ? 'Kundenservice' : locale === 'sr' ? 'Korisnička podrška' : 'customer service',
|
||||
email: 'info@damjan-savic.com',
|
||||
telephone: '+491756950979',
|
||||
availableLanguage: [
|
||||
{ '@type': 'Language', name: 'German' },
|
||||
{ '@type': 'Language', name: 'English' },
|
||||
{ '@type': 'Language', name: 'Serbian' },
|
||||
],
|
||||
areaServed: [
|
||||
{ '@type': 'Country', name: 'Germany' },
|
||||
{ '@type': 'Country', name: 'United States' },
|
||||
{ '@type': 'Country', name: 'United Kingdom' },
|
||||
{ '@type': 'Country', name: 'Austria' },
|
||||
{ '@type': 'Country', name: 'Switzerland' },
|
||||
{ '@type': 'Country', name: 'Serbia' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// CollectionPage Schema for Portfolio with ItemList
|
||||
interface PortfolioProject {
|
||||
name: string;
|
||||
description: string;
|
||||
url: string;
|
||||
image?: string;
|
||||
dateCreated?: string;
|
||||
technologies?: string[];
|
||||
}
|
||||
|
||||
export function CollectionPageJsonLd({
|
||||
locale,
|
||||
projects,
|
||||
}: {
|
||||
locale: Locale;
|
||||
projects: PortfolioProject[];
|
||||
}) {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
|
||||
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'CollectionPage',
|
||||
'@id': `${baseUrl}/${locale}/portfolio/#webpage`,
|
||||
name: locale === 'de'
|
||||
? 'Portfolio - Damjan Savić'
|
||||
: locale === 'sr'
|
||||
? 'Portfolio - Damjan Savić'
|
||||
: 'Portfolio - Damjan Savić',
|
||||
description: locale === 'de'
|
||||
? 'Ausgewählte KI- und Automatisierungsprojekte von Damjan Savić. Von Voice AI bis n8n-Workflows.'
|
||||
: locale === 'sr'
|
||||
? 'Odabrani AI i automatizacija projekti Damjana Savića. Od Voice AI do n8n workflow-ova.'
|
||||
: 'Selected AI and automation projects by Damjan Savić. From Voice AI to n8n workflows.',
|
||||
url: `${baseUrl}/${locale}/portfolio`,
|
||||
inLanguage: locale === 'de' ? 'de-DE' : locale === 'sr' ? 'sr-RS' : 'en-US',
|
||||
isPartOf: {
|
||||
'@id': `${baseUrl}/#website`,
|
||||
},
|
||||
author: {
|
||||
'@id': `${baseUrl}/#person`,
|
||||
},
|
||||
mainEntity: {
|
||||
'@type': 'ItemList',
|
||||
itemListElement: projects.map((project, index) => ({
|
||||
'@type': 'ListItem',
|
||||
position: index + 1,
|
||||
item: {
|
||||
'@type': 'CreativeWork',
|
||||
name: project.name,
|
||||
description: project.description,
|
||||
url: project.url.startsWith('http') ? project.url : `${baseUrl}${project.url}`,
|
||||
...(project.image && {
|
||||
image: project.image.startsWith('http') ? project.image : `${baseUrl}${project.image}`,
|
||||
}),
|
||||
...(project.dateCreated && { dateCreated: project.dateCreated }),
|
||||
...(project.technologies && {
|
||||
keywords: project.technologies.join(', '),
|
||||
}),
|
||||
author: {
|
||||
'@id': `${baseUrl}/#person`,
|
||||
},
|
||||
},
|
||||
})),
|
||||
numberOfItems: projects.length,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Generic ItemList Schema for any list of items
|
||||
interface ItemListItem {
|
||||
name: string;
|
||||
description?: string;
|
||||
url?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
export function ItemListJsonLd({
|
||||
name,
|
||||
description,
|
||||
items,
|
||||
itemType = 'ListItem',
|
||||
locale,
|
||||
}: {
|
||||
name: string;
|
||||
description?: string;
|
||||
items: ItemListItem[];
|
||||
itemType?: string;
|
||||
locale: Locale;
|
||||
}) {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
|
||||
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'ItemList',
|
||||
name: name,
|
||||
...(description && { description }),
|
||||
inLanguage: locale === 'de' ? 'de-DE' : locale === 'sr' ? 'sr-RS' : 'en-US',
|
||||
itemListElement: items.map((item, index) => ({
|
||||
'@type': itemType,
|
||||
position: index + 1,
|
||||
name: item.name,
|
||||
...(item.description && { description: item.description }),
|
||||
...(item.url && {
|
||||
url: item.url.startsWith('http') ? item.url : `${baseUrl}${item.url}`,
|
||||
}),
|
||||
...(item.image && {
|
||||
image: item.image.startsWith('http') ? item.image : `${baseUrl}${item.image}`,
|
||||
}),
|
||||
})),
|
||||
numberOfItems: items.length,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -12,4 +12,7 @@ export {
|
||||
ProfilePageJsonLd,
|
||||
VideoJsonLd,
|
||||
SoftwareAppJsonLd,
|
||||
ContactPageJsonLd,
|
||||
CollectionPageJsonLd,
|
||||
ItemListJsonLd,
|
||||
} from './JsonLd';
|
||||
|
||||
Reference in New Issue
Block a user