Add blog posts, cleanup unused files, update components

- Add 100 blog posts covering AI, development, and tech topics
- Add .env.example for environment configuration
- Add accessibility and lighthouse audit scripts
- Remove obsolete SEO reports and temporary files
- Remove dev-dist build artifacts and backup files
- Remove unused portrait images (moved/consolidated elsewhere)
- Update contact form and component improvements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-25 11:42:11 +01:00
co-authored by Claude Opus 4.5
parent cba6c66ccc
commit 43484c5023
164 changed files with 60969 additions and 23346 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
const descriptions: Record<string, string> = {
de: 'Impressum und rechtliche Informationen für die Portfolio-Website von Damjan Savić, KI Entwickler und Automation Specialist aus Köln.',
en: 'Legal notice and contact information for Damjan Savić portfolio website, AI Developer and Automation Specialist.',
sr: 'Pravno obavestenje i kontakt informacije za portfolio veb sajt Damjana Savica, AI Developer i Automation Specialist.',
sr: 'Pravno obavestenje i kontakt informacije za portfolio veb sajt Damjana Savića, AI Developer i Automation Specialist.',
};
const localePath = locale === 'de' ? '/imprint' : `/${locale}/imprint`;
+6 -6
View File
@@ -31,15 +31,15 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
const titles: Record<Locale, string> = {
de: 'Damjan Savić | AI & Automation Specialist aus Köln',
en: 'Damjan Savić | AI & Automation Specialist from Cologne',
sr: 'Damjan Savić | AI & Automation Specialist iz Kelna',
de: 'Damjan Savić | Fullstack Developer aus Köln',
en: 'Damjan Savić | Fullstack Developer from Cologne',
sr: 'Damjan Savić | Fullstack Developer iz Kelna',
};
const descriptions: Record<Locale, string> = {
de: 'Entwicklung von KI-Agenten und Automatisierungslösungen. Von Voice-AI-Plattformen bis zu autonomen Web-Agenten.',
en: 'Building AI agents and automation solutions. From voice AI platforms to autonomous web agents.',
sr: 'Razvoj AI agenata i resenja za automatizaciju. Od voice AI platformi do autonomnih web agenata.',
de: 'Fullstack Entwicklung für Websites, Apps und SaaS mit Next.js, React und TypeScript.',
en: 'Fullstack development for websites, apps and SaaS with Next.js, React and TypeScript.',
sr: 'Fullstack razvoj za web stranice, aplikacije i SaaS sa Next.js, React i TypeScript.',
};
// Generate alternates using localeMetadata
+2 -2
View File
@@ -4,9 +4,9 @@ import './globals.css';
export const metadata: Metadata = {
title: {
template: '%s | Damjan Savić',
default: 'Damjan Savić | AI & Automation Specialist',
default: 'Damjan Savić | Fullstack Developer',
},
description: 'AI & Automation Specialist - Building AI agents and automation solutions.',
description: 'Fullstack Developer - Building websites, apps and SaaS with Next.js, React and TypeScript.',
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com'),
};
+1 -1
View File
@@ -52,7 +52,7 @@ const SEO: React.FC<SEOProps> = ({
'@context': 'https://schema.org',
'@type': 'Person',
name: 'Damjan Savić',
alternateName: 'Damjan Savic',
alternateName: 'Damjan Savić',
url: siteUrl,
image: `${siteUrl}${image}`,
description: defaultDescription,
@@ -6,7 +6,7 @@ export function LocalBusinessSchema() {
"@type": "LocalBusiness",
"@id": "https://damjan-savic.com/#localbusiness",
"name": "Damjan Savić - Senior Fullstack Developer & IT Consultant",
"alternateName": ["CoderConda", "Damjan Savic IT Services"],
"alternateName": ["CoderConda", "Damjan Savić IT Services"],
"description": "Damjan Savić bietet professionelle IT-Dienstleistungen in Köln und Umgebung. Spezialisiert auf Enterprise Software Development, KI-Integration, Cloud Architecture und digitale Transformation. Damjan Savić entwickelt maßgeschneiderte Lösungen für Ihr Unternehmen.",
"url": "https://damjan-savic.com",
"logo": {
@@ -10,7 +10,7 @@ export function LocalizedPersonSchema() {
"@type": "Person",
"@id": "https://damjan-savic.com/#person",
"name": currentLanguage === 'sr' ? "Дамјан Савић" : "Damjan Savić",
"alternateName": currentLanguage === 'sr' ? ["Damjan Savić", "Damjan Savic"] : "Damjan Savic",
"alternateName": currentLanguage === 'sr' ? ["Damjan Savić", "Damjan Savić"] : "Damjan Savić",
"jobTitle": currentLanguage === 'de' ?
["AI & Automation Specialist", "Process Automation Specialist", "Fullstack Entwickler"] :
currentLanguage === 'sr' ?
@@ -199,7 +199,7 @@ export function LocalizedWebsiteSchema() {
currentLanguage === 'sr' ?
"Дамјан Савић - AI & Automation Specialist" :
"Damjan Savić - AI & Automation Specialist",
"alternateName": ["Damjan Savic Portfolio"],
"alternateName": ["Damjan Savić Portfolio"],
"description": currentLanguage === 'de' ?
"Offizielle Website von Damjan Savić - AI & Automation Specialist. Spezialisiert auf KI-Agenten, Voice AI, Prozessautomatisierung mit n8n und Fullstack Development." :
currentLanguage === 'sr' ?
+1 -1
View File
@@ -6,7 +6,7 @@ export function PersonSchema() {
"@type": "Person",
"@id": "https://damjan-savic.com/#person",
"name": "Damjan Savić",
"alternateName": "Damjan Savic",
"alternateName": "Damjan Savić",
"jobTitle": ["Senior Fullstack Developer", "Digital Solutions Consultant", "Software Architect", "KI/AI Spezialist"],
"description": "Damjan Savić ist ein Senior Fullstack Entwickler und Digital Solutions Consultant aus Köln. Damjan Savić ist spezialisiert auf Python, JavaScript, React, Next.js, TypeScript und KI-Integration. Mit über 10 Jahren Erfahrung entwickelt Damjan Savić maßgeschneiderte Enterprise-Lösungen, moderne Web-Applikationen und innovative KI-gestützte Systeme für Unternehmen jeder Größe.",
"url": "https://damjan-savic.com",
@@ -7,7 +7,7 @@ export function WebsiteSchema() {
"@id": "https://damjan-savic.com/#website",
"url": "https://damjan-savic.com",
"name": "Damjan Savić - Senior Fullstack Developer & Digital Solutions Consultant",
"alternateName": ["Damjan Savic Portfolio", "CoderConda"],
"alternateName": ["Damjan Savić Portfolio", "CoderConda"],
"description": "Offizielle Website von Damjan Savić - Senior Fullstack Entwickler und Digital Solutions Consultant aus Köln. Spezialisiert auf Enterprise Software Development, KI-Integration, Cloud Architecture und moderne Web-Technologien. Entdecken Sie innovative Lösungen von Damjan Savić.",
"publisher": {
"@id": "https://damjan-savic.com/#person"
+26 -11
View File
@@ -111,7 +111,7 @@ export function ContactForm() {
<div className="relative w-24 h-24 rounded-full overflow-hidden border-2 border-zinc-700 flex-shrink-0">
<Image
src="/images/headshot.webp"
alt="Damjan Savić"
alt="Damjan Savić - Full-Stack Web Developer"
fill
sizes="96px"
className="object-cover"
@@ -135,7 +135,7 @@ export function ContactForm() {
className="flex items-start gap-4"
>
<div className="p-3 bg-zinc-800/50 rounded-lg">
<MapPin className="h-6 w-6 text-zinc-400" />
<MapPin className="h-6 w-6 text-zinc-400" aria-hidden="true" />
</div>
<div>
<h3 className="text-sm font-medium text-zinc-300 mb-1">
@@ -154,7 +154,7 @@ export function ContactForm() {
className="flex items-start gap-4"
>
<div className="p-3 bg-zinc-800/50 rounded-lg">
<Phone className="h-6 w-6 text-zinc-400" />
<Phone className="h-6 w-6 text-zinc-400" aria-hidden="true" />
</div>
<div>
<h3 className="text-sm font-medium text-zinc-300 mb-1">
@@ -176,7 +176,7 @@ export function ContactForm() {
className="flex items-start gap-4"
>
<div className="p-3 bg-zinc-800/50 rounded-lg">
<Mail className="h-6 w-6 text-zinc-400" />
<Mail className="h-6 w-6 text-zinc-400" aria-hidden="true" />
</div>
<div>
<h3 className="text-sm font-medium text-zinc-300 mb-1">
@@ -210,7 +210,7 @@ export function ContactForm() {
>
<div className="bg-zinc-800/30 border border-zinc-800 rounded-xl p-6 md:p-8">
<div className="flex items-center gap-2 mb-2">
<Mail className="h-5 w-5 text-zinc-400" />
<Mail className="h-5 w-5 text-zinc-400" aria-hidden="true" />
<h2 className="text-xl font-semibold text-white">
{t('contactForm.title')}
</h2>
@@ -233,10 +233,13 @@ export function ContactForm() {
onChange={handleChange}
placeholder={t('contactForm.name.placeholder')}
disabled={isSubmitting}
aria-required="true"
aria-invalid={!!errors.name}
aria-describedby={errors.name ? 'name-error' : undefined}
className="w-full h-12 px-4 bg-zinc-900/50 border border-zinc-800 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-600 disabled:opacity-50"
/>
{errors.name && (
<p className="text-sm text-red-400">{errors.name}</p>
<p id="name-error" role="alert" className="text-sm text-red-400">{errors.name}</p>
)}
</div>
@@ -252,10 +255,13 @@ export function ContactForm() {
onChange={handleChange}
placeholder={t('contactForm.email.placeholder')}
disabled={isSubmitting}
aria-required="true"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
className="w-full h-12 px-4 bg-zinc-900/50 border border-zinc-800 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-600 disabled:opacity-50"
/>
{errors.email && (
<p className="text-sm text-red-400">{errors.email}</p>
<p id="email-error" role="alert" className="text-sm text-red-400">{errors.email}</p>
)}
</div>
@@ -271,10 +277,13 @@ export function ContactForm() {
placeholder={t('contactForm.message.placeholder')}
rows={6}
disabled={isSubmitting}
aria-required="true"
aria-invalid={!!errors.message}
aria-describedby={errors.message ? 'message-error' : undefined}
className="w-full px-4 py-3 bg-zinc-900/50 border border-zinc-800 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-600 disabled:opacity-50 resize-none"
/>
{errors.message && (
<p className="text-sm text-red-400">{errors.message}</p>
<p id="message-error" role="alert" className="text-sm text-red-400">{errors.message}</p>
)}
</div>
</div>
@@ -283,9 +292,11 @@ export function ContactForm() {
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
role="alert"
aria-live="polite"
className="flex items-center gap-2 p-3 bg-green-900/20 border border-green-900/50 rounded-lg"
>
<CheckCircle2 className="h-4 w-4 text-green-500" />
<CheckCircle2 className="h-4 w-4 text-green-500" aria-hidden="true" />
<p className="text-green-400 text-sm">
{t('contactForm.successMessage')}
</p>
@@ -296,6 +307,8 @@ export function ContactForm() {
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
role="alert"
aria-live="assertive"
className="p-3 bg-red-900/20 border border-red-900/50 rounded-lg"
>
<p className="text-red-400 text-sm">
@@ -307,13 +320,15 @@ export function ContactForm() {
<button
type="submit"
disabled={isSubmitting}
aria-busy={isSubmitting}
aria-label={isSubmitting ? t('contactForm.submitting') : undefined}
className="w-full h-12 bg-zinc-100 hover:bg-white text-zinc-900 font-medium rounded-lg transition-colors duration-300 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? (
<Loader2 className="h-5 w-5 animate-spin" />
<Loader2 className="h-5 w-5 animate-spin" aria-hidden="true" />
) : (
<>
<Send className="h-5 w-5" />
<Send className="h-5 w-5" aria-hidden="true" />
{t('contactForm.submit')}
</>
)}
+1 -1
View File
@@ -26,7 +26,7 @@ const Hero = async () => {
<div className="md:hidden absolute inset-x-0 bottom-0 h-[60%] bg-gradient-to-t from-zinc-950 via-zinc-950/80 via-40% to-transparent z-10 pointer-events-none" />
{/* Social Links - Top */}
<div className="absolute top-8 left-6 sm:left-12 lg:left-20 flex gap-4 z-20">
<div className="absolute top-8 left-6 sm:left-12 lg:left-20 flex gap-4 z-30">
<a
href="https://www.linkedin.com/in/damjan-savi%C4%87-720288127/"
target="_blank"
+6 -46
View File
@@ -2,13 +2,12 @@
import { useState, useEffect } from 'react';
import { useTranslations } from 'next-intl';
import { motion, AnimatePresence } from 'framer-motion';
import { motion } from 'framer-motion';
import { Database, Server, Code2, Bot, Workflow, FileCode2 } from 'lucide-react';
interface Skill {
name: string;
level: number;
description: string;
}
const iconMap: Record<string, React.ReactNode> = {
@@ -25,7 +24,6 @@ const iconMap: Record<string, React.ReactNode> = {
const Skills = () => {
const t = useTranslations('pages.home.skills');
const [selectedSkill, setSelectedSkill] = useState<string | null>(null);
const [skills, setSkills] = useState<Skill[]>([]);
useEffect(() => {
@@ -96,54 +94,16 @@ const Skills = () => {
{skills.map((skill, idx) => (
<motion.div
key={skill.name}
className="relative"
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: idx * 0.1 }}
className="p-6 rounded-xl bg-zinc-800/30 backdrop-blur-sm flex flex-col items-center justify-center gap-3"
>
<motion.div
className={`relative p-6 rounded-xl bg-zinc-800/30 backdrop-blur-sm flex flex-col items-center justify-center gap-3 cursor-pointer transition-colors duration-300 hover:bg-zinc-700/50 ${
selectedSkill === skill.name ? 'ring-2 ring-zinc-500 z-20' : ''
}`}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.98 }}
onHoverStart={() => setSelectedSkill(skill.name)}
onHoverEnd={() => setSelectedSkill(null)}
role="button"
>
<motion.div
className={selectedSkill === skill.name ? 'text-zinc-500' : 'text-white'}
animate={{
rotate: selectedSkill === skill.name ? [0, -10, 10, 0] : 0,
scale: selectedSkill === skill.name ? 1.1 : 1
}}
transition={{ duration: 0.4 }}
>
{iconMap[skill.name] || <Code2 className="w-8 h-8" />}
</motion.div>
<span className="text-white text-sm text-center">{skill.name}</span>
<AnimatePresence>
{selectedSkill === skill.name && (
<motion.div
className="absolute left-1/2 top-0 -translate-x-1/2 bg-zinc-800/80 backdrop-blur-sm rounded-lg px-4 py-2 text-zinc-300 text-xs text-center shadow-xl pointer-events-none"
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 5 }}
transition={{ duration: 0.15 }}
style={{
whiteSpace: 'nowrap',
transform: 'translate(-50%, calc(-100% - 12px))',
zIndex: 999
}}
>
{skill.description}
<div className="absolute left-1/2 bottom-0 translate-y-1/2 -translate-x-1/2 w-0 h-0 border-l-[6px] border-l-transparent border-r-[6px] border-r-transparent border-t-[6px] border-t-zinc-800" />
</motion.div>
)}
</AnimatePresence>
</motion.div>
<div className="text-white">
{iconMap[skill.name] || <Code2 className="w-8 h-8" />}
</div>
<span className="text-white text-sm text-center">{skill.name}</span>
</motion.div>
))}
</div>
+6 -6
View File
@@ -15,15 +15,15 @@ export function PersonJsonLd({ locale }: JsonLdProps) {
givenName: 'Damjan',
familyName: 'Savić',
jobTitle: locale === 'de'
? 'AI & Automation Specialist | Fullstack Entwickler'
? 'Fullstack Entwickler'
: locale === 'sr'
? 'AI & Automation Specialist | Fullstack Developer'
: 'AI & Automation Specialist | Fullstack Developer',
? 'Fullstack Developer'
: 'Fullstack Developer',
description: locale === 'de'
? 'Entwicklung von KI-Agenten und Automatisierungslösungen. Von Voice-AI-Plattformen bis zu autonomen Web-Agenten.'
? 'Fullstack Entwicklung für Websites, Apps und SaaS mit Next.js, React und TypeScript.'
: locale === 'sr'
? '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.',
? 'Fullstack razvoj za web stranice, aplikacije i SaaS sa Next.js, React i TypeScript.'
: 'Fullstack development for websites, apps and SaaS with Next.js, React and TypeScript.',
url: baseUrl,
image: `${baseUrl}/images/og-image.jpg`,
email: 'info@damjan-savic.com',
+1 -1
View File
@@ -7,7 +7,7 @@ export const meta = {
keywords: [
// Name & Branding
'Damjan Savić',
'Damjan Savic',
'Damjan Savić',
'CoderConda',
// Voice AI & Agenten
+1 -1
View File
@@ -7,7 +7,7 @@ export const meta = {
keywords: [
// Name & Branding
'Damjan Savić',
'Damjan Savic',
'Damjan Savić',
'CoderConda',
// Voice AI & Agents
+1
View File
@@ -395,6 +395,7 @@
"placeholder": "Ihre Nachricht an mich..."
},
"submit": "Nachricht senden",
"submitting": "Nachricht wird gesendet...",
"successMessage": "Vielen Dank für Ihre Nachricht! Ich werde mich so schnell wie möglich bei Ihnen melden.",
"errorMessage": "Entschuldigung, beim Senden Ihrer Nachricht ist ein Fehler aufgetreten."
}
+1
View File
@@ -410,6 +410,7 @@
"placeholder": "Your message to me..."
},
"submit": "Send Message",
"submitting": "Sending message...",
"successMessage": "Thank you for your message! I will get back to you as soon as possible.",
"errorMessage": "Sorry, there was an error sending your message."
}
+1
View File
@@ -417,6 +417,7 @@
"placeholder": "Vasa poruka za mene..."
},
"submit": "Posalji poruku",
"submitting": "Slanje poruke...",
"successMessage": "Hvala vam na poruci! Javicu vam se sto je pre moguce.",
"errorMessage": "Izvinite, doslo je do greske prilikom slanja vase poruke."
}
+2 -2
View File
@@ -22,7 +22,7 @@ export const CURRENT_ROLE = {
export const PROFILE = {
name: "Damjan Savić",
title: "AI & Automation Specialist",
subtitle: "Building AI agents and automation solutions",
title: "Fullstack Developer",
subtitle: "Building websites, apps and SaaS with Next.js, React and TypeScript",
languages: ["German", "English", "Serbian", "French", "Spanish", "Russian"]
} as const;