Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/og-image-es.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/og-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions public/robots.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,30 @@ Allow: /

User-agent: *
Allow: /

# Block AI training crawlers
User-agent: GPTBot
Disallow: /

User-agent: ChatGPT-User
Disallow: /

User-agent: CCBot
Disallow: /

User-agent: Google-Extended
Disallow: /

User-agent: anthropic-ai
Disallow: /

User-agent: ClaudeBot
Disallow: /

User-agent: Bytespider
Disallow: /

User-agent: cohere-ai
Disallow: /

Sitemap: https://jodaz.xyz/sitemap-index.xml
17 changes: 10 additions & 7 deletions src/components/BaseHead.astro
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ interface Props {
alternateUrl?: string; // URL of the same page in the other language
}

const { title, description, image = '/og-image.png', lang = 'en', alternateUrl } = Astro.props;
const { title, description, image, lang = 'en', alternateUrl } = Astro.props;
const ogImage = image ?? (lang === 'es' ? '/og-image-es.png' : '/og-image.png');
const canonicalURL = new URL(Astro.url.pathname, Astro.site);

// Compute hreflang URLs
Expand All @@ -22,7 +23,9 @@ const esURL = lang === 'es' ? canonicalURL.href : (alternateUrl ?? new URL('/es/
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta name="generator" content={Astro.generator} />
<meta name="theme-color" content="#0b5ef3" />

<!-- Canonical URL -->
<link rel="canonical" href={canonicalURL} />
Expand All @@ -43,14 +46,14 @@ const esURL = lang === 'es' ? canonicalURL.href : (alternateUrl ?? new URL('/es/
<meta property="og:url" content={canonicalURL} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={new URL(image, Astro.site)} />
<meta property="og:image" content={new URL(ogImage, Astro.site)} />

<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={canonicalURL} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={new URL(image, Astro.site)} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content={canonicalURL} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={new URL(ogImage, Astro.site)} />

<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
Expand Down
2 changes: 1 addition & 1 deletion src/components/ContactSection.astro
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface ContactStrings {
subhead: string;
email: string;
phone: string;
portraitAlt: string;
portraitAlt: string; // Should be descriptive e.g. "Jesus Ordosgoitty — Full Stack Web Developer"
}

interface Props {
Expand Down
3 changes: 1 addition & 2 deletions src/components/Footer.astro
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,7 @@ const navLinks = [
href={FOOTER_CREDITS_LINK.href}
>
{t('footer.companyName')}
</a>
. {t('footer.allRights')} /&gt;
</a> /&gt;
</p>

</div>
Expand Down
155 changes: 155 additions & 0 deletions src/components/StructuredData.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
---
// src/components/StructuredData.astro
// Injects JSON-LD structured data into the <head> for SEO rich results.

interface PersonSchema {
type: 'person';
lang?: string;
}

interface ProfessionalServiceSchema {
type: 'professionalService';
lang?: string;
}

interface ArticleSchema {
type: 'article';
headline: string;
description: string;
datePublished: string;
tags?: string[];
url?: string;
lang?: string;
}

type SchemaInput = PersonSchema | ProfessionalServiceSchema | ArticleSchema;

interface Props {
schemas: SchemaInput[];
}

const { schemas } = Astro.props;

const SITE_URL = 'https://jodaz.xyz';

const SOCIAL_PROFILES = [
'https://www.linkedin.com/in/jodaz/',
'https://www.instagram.com/jodaz.dev',
'https://github.com/jodaz-dev',
'https://upwork.com/freelancers/jesusordosgoitty4',
];

const SKILLS = [
'React', 'JavaScript', 'TypeScript', 'Node.js', 'MongoDB',
'PostgreSQL', 'AWS', 'CSS', 'Linux', 'System Administration',
'Next.js', 'Astro', 'Tailwind CSS',
];

function buildPerson(lang: string = 'en') {
return {
'@context': 'https://schema.org',
'@type': 'Person',
name: 'Jesus Ordosgoitty',
url: SITE_URL,
jobTitle: lang === 'es' ? 'Desarrollador Full Stack' : 'Full Stack Developer',
description: lang === 'es'
? 'Desarrollador Frontend, Ingeniero Full Stack, apasionado por la ingeniería de software.'
: 'Frontend Developer, Full Stack Engineer, and passionate about software engineering.',
sameAs: SOCIAL_PROFILES,
knowsAbout: SKILLS,
alumniOf: {
'@type': 'CollegeOrUniversity',
name: 'Universidad de Oriente',
},
image: `${SITE_URL}/og-image.png`,
};
}

function buildProfessionalService(lang: string = 'en') {
const services = lang === 'es'
? [
{ name: 'Ecommerce y Sitios Web', description: 'Tiendas en línea y sitios web rápidos, accesibles y optimizados para convertir visitantes.' },
{ name: 'Software a Medida', description: 'Software adaptado a tu negocio: inventario, citas, calculadoras, dashboards.' },
{ name: 'Aplicaciones Móviles', description: 'Aplicaciones móviles personalizadas con interfaces fluidas y alto rendimiento.' },
{ name: 'Automatizaciones', description: 'Automatización de tareas repetitivas conectando herramientas y orquestando datos.' },
]
: [
{ name: 'Ecommerce and Websites', description: 'Fast, accessible websites and optimized online stores ready to sell and convert visitors.' },
{ name: 'Custom Software', description: 'Software tailored for your business: inventory control, appointments, dashboards.' },
{ name: 'Mobile Apps', description: 'Custom mobile applications with fluid interfaces and high performance.' },
{ name: 'Automations', description: 'Automate repetitive tasks across your stack, connecting tools and orchestrating data.' },
];

return {
'@context': 'https://schema.org',
'@type': 'ProfessionalService',
name: lang === 'es' ? 'Jesús Ordosgoitty — Desarrollo Web' : 'Jesus Ordosgoitty — Web Development',
url: SITE_URL,
description: lang === 'es'
? 'Soluciones digitales de extremo a extremo: sitios web, apps móviles, software a medida y automatizaciones.'
: 'End-to-end digital solutions: websites, mobile apps, custom software, and automations.',
areaServed: 'Worldwide',
provider: {
'@type': 'Person',
name: 'Jesus Ordosgoitty',
},
hasOfferCatalog: {
'@type': 'OfferCatalog',
name: lang === 'es' ? 'Servicios de Desarrollo' : 'Development Services',
itemListElement: services.map((service, index) => ({
'@type': 'Offer',
itemOffered: {
'@type': 'Service',
name: service.name,
description: service.description,
},
position: index + 1,
})),
},
};
}

function buildArticle(input: ArticleSchema) {
return {
'@context': 'https://schema.org',
'@type': 'Article',
headline: input.headline,
description: input.description,
datePublished: input.datePublished,
author: {
'@type': 'Person',
name: 'Jesus Ordosgoitty',
url: SITE_URL,
},
publisher: {
'@type': 'Person',
name: 'Jesus Ordosgoitty',
url: SITE_URL,
},
...(input.url && { mainEntityOfPage: input.url }),
...(input.tags?.length && { keywords: input.tags.join(', ') }),
inLanguage: input.lang === 'es' ? 'es' : 'en',
};
}

function buildSchema(input: SchemaInput) {
switch (input.type) {
case 'person':
return buildPerson(input.lang);
case 'professionalService':
return buildProfessionalService(input.lang);
case 'article':
return buildArticle(input);
default:
return null;
}
}

const jsonLdBlocks = schemas
.map(buildSchema)
.filter(Boolean);
---

{jsonLdBlocks.map((schema) => (
<script type="application/ld+json" set:html={JSON.stringify(schema)} />
))}
2 changes: 1 addition & 1 deletion src/components/views/About.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const AboutView = ({ strings }: Props) => {

<div className="flex justify-center items-center animate-fade-in">
<div className="w-[150px] h-[150px] md:w-[300px] md:h-[300px] rounded-full overflow-hidden">
<img src={typeof founderImg === 'string' ? founderImg : founderImg.src} alt="Profile" className="w-full h-full object-cover" />
<img src={typeof founderImg === 'string' ? founderImg : founderImg.src} alt="Jesus Ordosgoitty — Full Stack Web Developer" className="w-full h-full object-cover" />
</div>
</div>
</div>
Expand Down
6 changes: 4 additions & 2 deletions src/pages/about.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import AboutView from '@/components/views/About';
import ExperienceView from '@/components/views/Experience';
import en from '../../public/locales/en/common.json';
import BottomGridWrapper from '@/components/BottomGridWrapper.astro';
import StructuredData from '@/components/StructuredData.astro';

const lang = 'en' as const;
const t = useTranslations(lang);
Expand All @@ -26,11 +27,12 @@ const experienceStrings = {
---

<BaseLayout
title={`${t('about.line1')} ${t('about.line2')} — Jesus Ordosgoitty`}
description={t('about.description')}
title="About — Jesus Ordosgoitty | Full Stack Developer"
description="Full Stack Developer with 5+ years of experience building fast, accessible, and scalable digital products. Specializing in React, Node.js, and modern web technologies."
lang={lang}
alternateUrl="https://jodaz.xyz/es/about"
>
<StructuredData schemas={[{ type: 'person', lang: 'en' }]} />
<AboutView strings={aboutStrings} client:load />
<ExperienceView strings={experienceStrings} client:visible />
<BottomGridWrapper lang={lang} />
Expand Down
6 changes: 4 additions & 2 deletions src/pages/es/about.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import AboutView from '@/components/views/About';
import ExperienceView from '@/components/views/Experience';
import es from '../../../public/locales/es/common.json';
import BottomGridWrapper from '@/components/BottomGridWrapper.astro';
import StructuredData from '@/components/StructuredData.astro';

const lang = 'es' as const;
const t = useTranslations(lang);
Expand All @@ -26,11 +27,12 @@ const experienceStrings = {
---

<BaseLayout
title={`${t('about.line1')} ${t('about.line2')} — Jesús Ordosgoitty`}
description={t('about.description')}
title="Sobre Mí — Jesús Ordosgoitty | Desarrollador Full Stack"
description="Desarrollador Full Stack con más de 5 años de experiencia creando productos digitales rápidos, accesibles y escalables. Especializado en React, Node.js y tecnologías web modernas."
lang={lang}
alternateUrl="https://jodaz.xyz/about"
>
<StructuredData schemas={[{ type: 'person', lang: 'es' }]} />
<AboutView strings={aboutStrings} client:load />
<ExperienceView strings={experienceStrings} client:visible />
<BottomGridWrapper lang={lang} />
Expand Down
4 changes: 3 additions & 1 deletion src/pages/es/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import HeroIsland from '@/components/islands/HeroIsland';
import ServicesSection from '@/components/ServicesSection.astro';
import PortfolioIsland from '@/components/islands/PortfolioIsland';
import BottomGridWrapper from '@/components/BottomGridWrapper.astro';
import StructuredData from '@/components/StructuredData.astro';
import es from '../../../public/locales/es/common.json';

const lang = 'es' as const;
Expand Down Expand Up @@ -33,7 +34,7 @@ const contactStrings = {
subhead: t('contact.subhead'),
email: t('contact.email'),
phone: t('contact.phone'),
portraitAlt: 'Retrato',
portraitAlt: 'Jesús Ordosgoitty — Desarrollador Web Full Stack',
};
---

Expand All @@ -43,6 +44,7 @@ const contactStrings = {
lang={lang}
alternateUrl="https://jodaz.xyz/"
>
<StructuredData schemas={[{ type: 'person', lang: 'es' }, { type: 'professionalService', lang: 'es' }]} />
<HeroIsland {...heroProps} client:load />
<ServicesSection lang={lang} />
<PortfolioIsland
Expand Down
18 changes: 17 additions & 1 deletion src/pages/es/projects/[slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { getCollection } from 'astro:content';
import BaseLayout from '@/layouts/BaseLayout.astro';
import BottomGridWrapper from '@/components/BottomGridWrapper.astro';
import StructuredData from '@/components/StructuredData.astro';

export async function getStaticPaths() {
const projects = await getCollection('projects', ({ data }) => data.lang === 'es');
Expand All @@ -17,9 +18,24 @@ export async function getStaticPaths() {

const { project } = Astro.props;
const { Content } = await project.render();
const slug = project.slug.startsWith('es/') ? project.slug.slice(3) : project.slug;
---

<BaseLayout title={project.data.title} description={project.data.description} lang="es">
<BaseLayout
title={`${project.data.title} — Jesús Ordosgoitty`}
description={project.data.description}
lang="es"
alternateUrl={`https://jodaz.xyz/projects/${slug}/`}
>
<StructuredData schemas={[{
type: 'article',
headline: project.data.title,
description: project.data.description,
datePublished: project.data.publishDate.toISOString().split('T')[0],
tags: project.data.tags,
url: `https://jodaz.xyz/es/projects/${slug}/`,
lang: 'es',
}]} />
<section class="bg-gray-100 border-t-2 border-border/50 blueprint-grid py-20 min-h-screen">
<article class="max-w-4xl mx-auto px-6 py-16 bg-white border-2 border-border shadow-elegant animate-fade-in">
<header class="mb-12">
Expand Down
4 changes: 3 additions & 1 deletion src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import HeroIsland from '@/components/islands/HeroIsland.tsx';
import ServicesSection from '@/components/ServicesSection.astro';
import PortfolioIsland from '@/components/islands/PortfolioIsland.tsx';
import BottomGridWrapper from '@/components/BottomGridWrapper.astro';
import StructuredData from '@/components/StructuredData.astro';
import en from '../../public/locales/en/common.json';

const lang = 'en' as const;
Expand Down Expand Up @@ -33,7 +34,7 @@ const contactStrings = {
subhead: t('contact.subhead'),
email: t('contact.email'),
phone: t('contact.phone'),
portraitAlt: 'Portrait',
portraitAlt: 'Jesus Ordosgoitty — Full Stack Web Developer',
};
---

Expand All @@ -43,6 +44,7 @@ const contactStrings = {
lang={lang}
alternateUrl="https://jodaz.xyz/es/"
>
<StructuredData schemas={[{ type: 'person', lang: 'en' }, { type: 'professionalService', lang: 'en' }]} />
<HeroIsland {...heroProps} client:load />
<ServicesSection lang={lang} />
<PortfolioIsland
Expand Down
Loading
Loading