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 type { Metadata } from 'next';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { ContactForm } from '@/components/contact';
|
import { ContactForm } from '@/components/contact';
|
||||||
import { BreadcrumbJsonLd, WebPageJsonLd } from '@/components/seo';
|
import { BreadcrumbJsonLd, WebPageJsonLd, ContactPageJsonLd } from '@/components/seo';
|
||||||
import { type Locale } from '@/i18n/config';
|
import { type Locale } from '@/i18n/config';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -80,6 +80,7 @@ export default async function ContactPage({ params }: Props) {
|
|||||||
url={`/${locale}/contact`}
|
url={`/${locale}/contact`}
|
||||||
locale={locale as Locale}
|
locale={locale as Locale}
|
||||||
/>
|
/>
|
||||||
|
<ContactPageJsonLd locale={locale as Locale} />
|
||||||
<ContactForm />
|
<ContactForm />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { setRequestLocale } from 'next-intl/server';
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { PortfolioGrid } from '@/components/portfolio';
|
import { PortfolioGrid } from '@/components/portfolio';
|
||||||
|
import { BreadcrumbJsonLd, CollectionPageJsonLd } from '@/components/seo';
|
||||||
|
import { type Locale } from '@/i18n/config';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
params: Promise<{ locale: string }>;
|
params: Promise<{ locale: string }>;
|
||||||
@@ -165,8 +167,25 @@ export default async function PortfolioPage({ params }: Props) {
|
|||||||
|
|
||||||
const t = await getTranslations('portfolio');
|
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 (
|
return (
|
||||||
<div className="relative min-h-screen">
|
<div className="relative min-h-screen">
|
||||||
|
<BreadcrumbJsonLd items={breadcrumbItems} locale={locale as Locale} />
|
||||||
|
<CollectionPageJsonLd locale={locale as Locale} projects={portfolioProjects} />
|
||||||
|
|
||||||
<section className="py-24">
|
<section className="py-24">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="text-center mb-16">
|
<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.'
|
? '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.',
|
: '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,
|
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',
|
email: 'info@damjan-savic.com',
|
||||||
telephone: '+491756950979',
|
telephone: '+491756950979',
|
||||||
address: {
|
address: {
|
||||||
@@ -34,6 +39,33 @@ export function PersonJsonLd({ locale }: JsonLdProps) {
|
|||||||
addressRegion: 'NRW',
|
addressRegion: 'NRW',
|
||||||
addressCountry: 'DE',
|
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: [
|
sameAs: [
|
||||||
'https://www.linkedin.com/in/damjansavic/',
|
'https://www.linkedin.com/in/damjansavic/',
|
||||||
'https://github.com/damjansavic',
|
'https://github.com/damjansavic',
|
||||||
@@ -50,6 +82,9 @@ export function PersonJsonLd({ locale }: JsonLdProps) {
|
|||||||
'Next.js',
|
'Next.js',
|
||||||
'n8n',
|
'n8n',
|
||||||
'SaaS Development',
|
'SaaS Development',
|
||||||
|
'Claude AI',
|
||||||
|
'GPT-4',
|
||||||
|
'LangChain',
|
||||||
],
|
],
|
||||||
alumniOf: [
|
alumniOf: [
|
||||||
{
|
{
|
||||||
@@ -69,6 +104,11 @@ export function PersonJsonLd({ locale }: JsonLdProps) {
|
|||||||
credentialCategory: 'degree',
|
credentialCategory: 'degree',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
// Nationality for geo-targeting
|
||||||
|
nationality: {
|
||||||
|
'@type': 'Country',
|
||||||
|
name: 'Germany',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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) {
|
export function WebSiteJsonLd({ locale }: JsonLdProps) {
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
|
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: 'English', alternateName: 'en' },
|
||||||
{ '@type': 'Language', name: 'Serbian', alternateName: 'sr' },
|
{ '@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 (
|
return (
|
||||||
|
|||||||
@@ -12,4 +12,7 @@ export {
|
|||||||
ProfilePageJsonLd,
|
ProfilePageJsonLd,
|
||||||
VideoJsonLd,
|
VideoJsonLd,
|
||||||
SoftwareAppJsonLd,
|
SoftwareAppJsonLd,
|
||||||
|
ContactPageJsonLd,
|
||||||
|
CollectionPageJsonLd,
|
||||||
|
ItemListJsonLd,
|
||||||
} from './JsonLd';
|
} from './JsonLd';
|
||||||
|
|||||||
Reference in New Issue
Block a user