chore: implement SEO composables, structured data, and sitemap with robots config

- Added `usePageSeo` composable for dynamic page-level SEO.
- Configured default head manager using `@vueuse/head`.
- Introduced sitemap.xml and robots.txt for search engine indexing.
- Updated `head` plugin and integrated SEO composable into pages.
- Added localized SEO data for English and German translations.
- Updated `yarn.lock` and dependencies to include `@vueuse/head`.
This commit is contained in:
2025-10-18 18:43:44 +02:00
parent b703ed3139
commit 78f72506b5
16 changed files with 491 additions and 30 deletions

View File

@@ -12,37 +12,9 @@
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<meta
content="Portfolio of Christian Nachtigall, a front-end engineer crafting accessible Vue & Vuetify experiences with performance-first thinking."
content="Christian Nachtigall is a front-end focused full-stack engineer building accessible Vue and Vuetify interfaces, Rust-backed APIs, and automation that keeps teams shipping fast."
name="description"
/>
<meta content="#7c5cff" name="theme-color" />
<meta
content="Christian Nachtigall · Front-end Engineer"
property="og:title"
/>
<meta
content="Explore projects, experience, and accessibility-driven front-end work by Christian Nachtigall."
property="og:description"
/>
<meta content="website" property="og:type" />
<meta content="https://nachtigall.dev" property="og:url" />
<meta
content="https://images.unsplash.com/photo-1523475472560-d2df97ec485c?auto=format&fit=crop&w=1200&q=80"
property="og:image"
/>
<meta content="summary_large_image" name="twitter:card" />
<meta
content="Christian Nachtigall · Front-end Engineer"
name="twitter:title"
/>
<meta
content="Portfolio for accessibility-focused Vue developer Christian Nachtigall."
name="twitter:description"
/>
<meta
content="https://images.unsplash.com/photo-1523475472560-d2df97ec485c?auto=format&fit=crop&w=1200&q=80"
name="twitter:image"
/>
<title>Christian Nachtigall · Front-end Engineer</title>
<link

View File

@@ -28,6 +28,7 @@
},
"dependencies": {
"@mdi/font": "^7.4.47",
"@vueuse/head": "^2.0.0",
"core-js": "^3.45.1",
"prettier": "^3.6.2",
"roboto-fontface": "*",

4
public/robots.txt Normal file
View File

@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://nachtigall.dev/sitemap.xml

53
public/sitemap.xml Normal file
View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://nachtigall.dev/</loc>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://nachtigall.dev/projects</loc>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://nachtigall.dev/experience</loc>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://nachtigall.dev/contact</loc>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://nachtigall.dev/projects/owlen</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://nachtigall.dev/projects/itsh</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://nachtigall.dev/projects/owly-news</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://nachtigall.dev/projects/polyscribe</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://nachtigall.dev/projects/abtruennige</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://nachtigall.dev/projects/kiropraktikk</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
</urlset>

View File

@@ -0,0 +1,113 @@
import type { MaybeRefOrGetter } from 'vue'
import { computed, toValue } from 'vue'
import { useRoute } from 'vue-router/auto'
import { useI18n } from 'vue-i18n'
import { useHead, useSeoMeta } from '@vueuse/head'
import {
DEFAULT_DESCRIPTION,
DEFAULT_SOCIAL_IMAGE,
DEFAULT_SOCIAL_IMAGE_ALT,
DEFAULT_TITLE,
OG_LOCALES,
SITE_TITLE_SUFFIX,
SITE_URL,
} from '@/config/site'
interface UsePageSeoOptions {
readonly title?: MaybeRefOrGetter<string | null | undefined>
readonly description?: MaybeRefOrGetter<string | null | undefined>
readonly image?: MaybeRefOrGetter<string | null | undefined>
readonly imageAlt?: MaybeRefOrGetter<string | null | undefined>
readonly type?: MaybeRefOrGetter<string | null | undefined>
readonly canonical?: MaybeRefOrGetter<string | null | undefined>
readonly structuredData?: MaybeRefOrGetter<
| Record<string, unknown>
| readonly Record<string, unknown>[]
| null
| undefined
>
readonly noindex?: MaybeRefOrGetter<boolean | undefined>
}
export function usePageSeo(options: UsePageSeoOptions) {
const route = useRoute()
const { locale } = useI18n()
const canonical = computed(() => {
const explicitCanonical = options?.canonical
? toValue(options.canonical)
: null
if (explicitCanonical) {
return explicitCanonical
}
try {
return new URL(route.fullPath ?? '/', SITE_URL).toString()
} catch {
return SITE_URL
}
})
const title = computed(() => toValue(options?.title)?.trim() || DEFAULT_TITLE)
const description = computed(
() => toValue(options?.description)?.trim() || DEFAULT_DESCRIPTION
)
const image = computed(() => toValue(options?.image) || DEFAULT_SOCIAL_IMAGE)
const imageAlt = computed(
() => toValue(options?.imageAlt) || DEFAULT_SOCIAL_IMAGE_ALT
)
const ogLocale = computed(() => OG_LOCALES[locale.value] ?? OG_LOCALES.en)
const robots = computed(() =>
toValue(options?.noindex) ? 'noindex, nofollow' : 'index, follow'
)
useSeoMeta({
title: () => title.value,
description: () => description.value,
robots: () => robots.value,
ogTitle: () => title.value,
ogDescription: () => description.value,
ogType: () => toValue(options?.type) || 'website',
ogUrl: () => canonical.value,
ogImage: () => image.value,
ogImageAlt: () => imageAlt.value,
ogLocale: () => ogLocale.value,
ogSiteName: () => SITE_TITLE_SUFFIX,
twitterCard: 'summary_large_image',
twitterTitle: () => title.value,
twitterDescription: () => description.value,
twitterImage: () => image.value,
twitterImageAlt: () => imageAlt.value,
})
const structured = computed(() => toValue(options?.structuredData) ?? null)
useHead(() => {
const scripts = []
const value = structured.value
if (value) {
const dataArray = Array.isArray(value) ? value : [value]
scripts.push(
...dataArray.map((item, index) => ({
key: `ld-json-${index}`,
type: 'application/ld+json',
children: JSON.stringify(item),
}))
)
}
return {
link: [
{
key: 'canonical',
rel: 'canonical',
href: canonical.value,
},
],
script: scripts,
}
})
}

16
src/config/site.ts Normal file
View File

@@ -0,0 +1,16 @@
export const SITE_URL = 'https://nachtigall.dev'
export const SITE_AUTHOR = 'Christian Nachtigall'
export const SITE_TITLE_SUFFIX = 'Christian Nachtigall'
export const DEFAULT_TITLE = 'Christian Nachtigall · Front-end Engineer'
export const DEFAULT_DESCRIPTION =
'Christian Nachtigall is a front-end focused full-stack engineer building accessible Vue and Vuetify interfaces, Rust-backed APIs, and automation that keeps teams shipping fast.'
export const DEFAULT_SOCIAL_IMAGE =
'https://images.unsplash.com/photo-1523475472560-d2df97ec485c?auto=format&fit=crop&w=1200&q=80'
export const DEFAULT_SOCIAL_IMAGE_ALT =
'Neon-lit workspace representing Christian Nachtigalls interface projects.'
export const CONTACT_EMAIL = 'contact@nachtigall.dev'
export const OG_LOCALES: Record<string, string> = {
en: 'en_US',
de: 'de_DE',
}

View File

@@ -10,6 +10,48 @@ const de = {
viewTimeline: 'Zeitleiste ansehen',
backToProjects: 'Zurück zu den Projekten',
},
seo: {
defaultTitle: 'Christian Nachtigall · Frontend-Engineer',
defaultDescription:
'Christian Nachtigall ist ein frontend-orientierter Full-Stack-Engineer, der zugängliche Vue- und Vuetify-Oberflächen, Rust-gestützte Services und GitOps-Automatisierungen entwickelt.',
imageAlt:
'Arbeitsplatz mit Code-Editor, der die Frontend-Projekte von Christian Nachtigall zeigt.',
home: {
title: 'Portfolio eines Frontend Engineers',
description:
'Entdecke Christian Nachtigalls ausgewählte Projekte, den Fokus auf Barrierefreiheit sowie die Zusammenarbeit rund um Vue, Vuetify und Rust.',
},
projects: {
title: 'Projekte & Fallstudien',
description:
'Detaillierte Einblicke in Christian Nachtigalls Produktarbeit mit Kennzahlen, Tech-Stacks und Wirkung in verschiedenen Projekten.',
},
experience: {
title: 'Erfahrung & Erfolge',
description:
'Karriereverlauf mit Engineering-Rollen, Erfolgen und Tooling-Leadership in designorientierten Teams.',
},
contact: {
title: 'Kontakt zu Christian Nachtigall',
description:
'Starte ein Gespräch über barrierefreie, performante Vue-Oberflächen, Designsysteme und Produkt-Tooling.',
},
project: {
title: '{project} · Projekt von Christian Nachtigall',
description: '{summary}',
fallbackTitle: 'Projektüberblick',
fallbackDescription:
'Erkunde Christian Nachtigalls aktuelle Projektarbeiten, Kennzahlen und Highlights der Zusammenarbeit.',
imageAlt: 'Hero-Bild des Projekts {project}',
fallbackImageAlt:
'Hero-Bild, das ein Projekt von Christian Nachtigall darstellt.',
},
person: {
jobTitle: 'Frontend-orientierter Full-Stack-Engineer',
description:
'Christian Nachtigall verbindet Vue/Vuetify-Expertise mit Rust-Automatisierung, um inklusive, leistungsstarke digitale Produkte zu liefern.',
},
},
nav: {
ariaMain: 'Hauptnavigation',
ariaMobile: 'Mobile Navigation',

View File

@@ -10,6 +10,48 @@ const en = {
viewTimeline: 'View timeline',
backToProjects: 'Back to projects',
},
seo: {
defaultTitle: 'Christian Nachtigall · Front-end Engineer',
defaultDescription:
'Christian Nachtigall is a front-end focused full-stack engineer crafting accessible Vue and Vuetify interfaces, Rust-backed services, and GitOps automation.',
imageAlt:
'Workspace with code editor representing Christian Nachtigalls front-end projects.',
home: {
title: 'Front-end Engineer Portfolio',
description:
'Explore Christian Nachtigalls featured projects, accessibility-focused process, and collaboration style across Vue, Vuetify, and Rust.',
},
projects: {
title: 'Projects & Case Studies',
description:
'Detailed breakdowns of Christian Nachtigalls product work, highlighting metrics, tech stacks, and impact across client engagements.',
},
experience: {
title: 'Experience & Achievements',
description:
'Career timeline showcasing engineering roles, achievements, and tooling leadership across design-forward teams.',
},
contact: {
title: 'Contact Christian Nachtigall',
description:
'Start a conversation about building accessible, high-performing Vue interfaces, design systems, and product tooling.',
},
project: {
title: '{project} · Project by Christian Nachtigall',
description: '{summary}',
fallbackTitle: 'Project Overview',
fallbackDescription:
'Explore Christian Nachtigalls recent project work, metrics, and collaboration highlights.',
imageAlt: '{project} case study hero image',
fallbackImageAlt:
'Hero image representing a Christian Nachtigall project.',
},
person: {
jobTitle: 'Frontend-first Full-Stack Engineer',
description:
'Christian Nachtigall blends Vue/Vuetify craftsmanship with Rust automation to ship inclusive, high-performing digital products.',
},
},
nav: {
ariaMain: 'Main navigation',
ariaMobile: 'Mobile navigation',

View File

@@ -134,12 +134,26 @@
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import ContactGrid from '@/components/portfolio/ContactGrid.vue'
import { usePortfolio } from '@/composables/usePortfolio'
import { usePageSeo } from '@/composables/usePageSeo'
import { DEFAULT_SOCIAL_IMAGE } from '@/config/site'
import { useI18n } from 'vue-i18n'
const portfolio = usePortfolio()
const { t } = useI18n()
const seoTitle = computed(() => t('seo.contact.title'))
const seoDescription = computed(() => t('seo.contact.description'))
const seoImageAlt = computed(() => t('seo.imageAlt'))
usePageSeo({
title: seoTitle,
description: seoDescription,
image: DEFAULT_SOCIAL_IMAGE,
imageAlt: seoImageAlt,
})
</script>
<style scoped lang="sass">

View File

@@ -88,11 +88,24 @@
import { computed } from 'vue'
import ExperienceTimeline from '@/components/portfolio/ExperienceTimeline.vue'
import { usePortfolio } from '@/composables/usePortfolio'
import { usePageSeo } from '@/composables/usePageSeo'
import { DEFAULT_SOCIAL_IMAGE } from '@/config/site'
import { useI18n } from 'vue-i18n'
const portfolio = usePortfolio()
const { t } = useI18n()
const seoTitle = computed(() => t('seo.experience.title'))
const seoDescription = computed(() => t('seo.experience.description'))
const seoImageAlt = computed(() => t('seo.imageAlt'))
usePageSeo({
title: seoTitle,
description: seoDescription,
image: DEFAULT_SOCIAL_IMAGE,
imageAlt: seoImageAlt,
})
const yearsOfExperience = computed(() => {
const currentYear = new Date().getFullYear()
const startYear = 2019 // Based on the experience data

View File

@@ -147,13 +147,48 @@
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import HeroSection from '@/components/portfolio/HeroSection.vue'
import ProjectCard from '@/components/portfolio/ProjectCard.vue'
import { usePortfolio } from '@/composables/usePortfolio'
import { usePageSeo } from '@/composables/usePageSeo'
import { CONTACT_EMAIL, DEFAULT_SOCIAL_IMAGE, SITE_URL } from '@/config/site'
import { useI18n } from 'vue-i18n'
const portfolio = usePortfolio()
const { t } = useI18n()
const seoTitle = computed(() => t('seo.home.title'))
const seoDescription = computed(() => t('seo.home.description'))
const seoImageAlt = computed(() => t('seo.imageAlt'))
const structuredData = computed(() => ({
'@context': 'https://schema.org',
'@type': 'Person',
name: portfolio.intro.name,
jobTitle: t('seo.person.jobTitle'),
description: t('seo.person.description'),
url: SITE_URL,
image: DEFAULT_SOCIAL_IMAGE,
email: `mailto:${CONTACT_EMAIL}`,
sameAs: portfolio.socials
.map(link => link.url)
.filter((value): value is string => Boolean(value)),
address: {
'@type': 'PostalAddress',
addressLocality: portfolio.intro.location,
},
knowsAbout: portfolio.intro.specialties,
}))
usePageSeo({
title: seoTitle,
description: seoDescription,
image: DEFAULT_SOCIAL_IMAGE,
imageAlt: seoImageAlt,
structuredData,
})
</script>
<style scoped lang="sass">

View File

@@ -135,9 +135,12 @@
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router/auto'
import { usePortfolio } from '@/composables/usePortfolio'
import { usePageSeo } from '@/composables/usePageSeo'
import { DEFAULT_SOCIAL_IMAGE } from '@/config/site'
import { useI18n } from 'vue-i18n'
const portfolio = usePortfolio()
@@ -147,6 +150,39 @@ const { t } = useI18n()
const slug = computed(() => route.params.slug)
const project = computed(() => portfolio.findProject(slug.value) ?? null)
const seoTitle = computed(() =>
project.value
? t('seo.project.title', { project: project.value.name })
: t('seo.project.fallbackTitle')
)
const seoDescription = computed(() =>
project.value
? t('seo.project.description', { summary: project.value.summary })
: t('seo.project.fallbackDescription')
)
const seoImage = computed(
() => project.value?.heroImage ?? DEFAULT_SOCIAL_IMAGE
)
const seoImageAlt = computed(() =>
project.value
? t('seo.project.imageAlt', { project: project.value.name })
: t('seo.project.fallbackImageAlt')
)
const shouldNoIndex = computed(() => project.value === null)
usePageSeo({
title: seoTitle,
description: seoDescription,
image: seoImage,
imageAlt: seoImageAlt,
noindex: shouldNoIndex,
type: 'article',
})
</script>
<style scoped lang="sass">

View File

@@ -123,14 +123,28 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import ProjectCard from '@/components/portfolio/ProjectCard.vue'
import { usePortfolio } from '@/composables/usePortfolio'
import { usePageSeo } from '@/composables/usePageSeo'
import { DEFAULT_SOCIAL_IMAGE } from '@/config/site'
import { useI18n } from 'vue-i18n'
const portfolio = usePortfolio()
const selectedFilter = ref<'all' | 'featured'>('all')
const { t } = useI18n()
const seoTitle = computed(() => t('seo.projects.title'))
const seoDescription = computed(() => t('seo.projects.description'))
const seoImageAlt = computed(() => t('seo.imageAlt'))
usePageSeo({
title: seoTitle,
description: seoDescription,
image: DEFAULT_SOCIAL_IMAGE,
imageAlt: seoImageAlt,
})
const filteredProjects = computed(() => {
if (selectedFilter.value === 'featured') {
return portfolio.featuredProjects

10
src/plugins/head.ts Normal file
View File

@@ -0,0 +1,10 @@
import { createHead } from '@vueuse/head'
import { DEFAULT_TITLE, SITE_TITLE_SUFFIX } from '@/config/site'
export const head = createHead({
titleTemplate: title =>
title && title !== DEFAULT_TITLE
? `${title} · ${SITE_TITLE_SUFFIX}`
: DEFAULT_TITLE,
})

View File

@@ -13,11 +13,12 @@ import pinia from '../stores'
import vuetify from './vuetify'
import reveal from '@/directives/reveal'
import { i18n } from './i18n'
import { head } from './head'
// Types
export function registerPlugins(app: App) {
app.use(vuetify).use(router).use(pinia).use(i18n)
app.use(vuetify).use(router).use(pinia).use(i18n).use(head)
app.directive('reveal', reveal)
}

View File

@@ -1985,6 +1985,60 @@ __metadata:
languageName: node
linkType: hard
"@unhead/dom@npm:1.11.20, @unhead/dom@npm:^1.7.0":
version: 1.11.20
resolution: "@unhead/dom@npm:1.11.20"
dependencies:
"@unhead/schema": "npm:1.11.20"
"@unhead/shared": "npm:1.11.20"
checksum: 10c0/344a61a00418ddc6f06b33f313b589945df18e43af29ab8e19e1df97a102f55bfbc7e8e163f7e8f0a415c58c48ad9878d552b29fdf83080e05cdcf30724a17c6
languageName: node
linkType: hard
"@unhead/schema@npm:1.11.20, @unhead/schema@npm:^1.7.0":
version: 1.11.20
resolution: "@unhead/schema@npm:1.11.20"
dependencies:
hookable: "npm:^5.5.3"
zhead: "npm:^2.2.4"
checksum: 10c0/f2f968639bbd18f90ddfb83b77c9256bc4c0379ab75efa24dc759f3f597aae707d4dde97df690823f8902eab31d73a5faa8bdd8daf18c6ac8e4503a78b42be74
languageName: node
linkType: hard
"@unhead/shared@npm:1.11.20":
version: 1.11.20
resolution: "@unhead/shared@npm:1.11.20"
dependencies:
"@unhead/schema": "npm:1.11.20"
packrup: "npm:^0.1.2"
checksum: 10c0/65ab0230e6338541f7a62e131c6d82b1160c71c86479fdb17d733fb5187422f6e287739cf50a1e7556690fb10951990d06e861e7aa3a90def479cb7a5ec638fa
languageName: node
linkType: hard
"@unhead/ssr@npm:^1.7.0":
version: 1.11.20
resolution: "@unhead/ssr@npm:1.11.20"
dependencies:
"@unhead/schema": "npm:1.11.20"
"@unhead/shared": "npm:1.11.20"
checksum: 10c0/f4ca6809a2436d23dbb4b57aaaa8981cac97eebd34c0c6237377e86cbd90c26079305adf97db9334966ebd4514ad62985648b653512ec35df916f6448d7624e5
languageName: node
linkType: hard
"@unhead/vue@npm:^1.7.0":
version: 1.11.20
resolution: "@unhead/vue@npm:1.11.20"
dependencies:
"@unhead/schema": "npm:1.11.20"
"@unhead/shared": "npm:1.11.20"
hookable: "npm:^5.5.3"
unhead: "npm:1.11.20"
peerDependencies:
vue: ">=2.7 || >=3"
checksum: 10c0/75f1cd8d671de363b02bc8ad2aaf6dc7fb0a402bdd306046ad71608d370190b539e74d6b03ab455d9c2453c5dd62956a235f92d74b09b98c336b68b2d3911602
languageName: node
linkType: hard
"@vitejs/plugin-vue@npm:^6.0.1":
version: 6.0.1
resolution: "@vitejs/plugin-vue@npm:6.0.1"
@@ -2354,6 +2408,20 @@ __metadata:
languageName: node
linkType: hard
"@vueuse/head@npm:^2.0.0":
version: 2.0.0
resolution: "@vueuse/head@npm:2.0.0"
dependencies:
"@unhead/dom": "npm:^1.7.0"
"@unhead/schema": "npm:^1.7.0"
"@unhead/ssr": "npm:^1.7.0"
"@unhead/vue": "npm:^1.7.0"
peerDependencies:
vue: ">=2.7 || >=3"
checksum: 10c0/0c8c51951eb3d4e8394cc04ba6f1fc36a6e6dfbde9ef677c525b10e14d06007f71df9f2c90d6b02afc6476ca53582070c834357a7af205bf8ef856fe5303f7a1
languageName: node
linkType: hard
"abbrev@npm:^2.0.0":
version: 2.0.0
resolution: "abbrev@npm:2.0.0"
@@ -7960,6 +8028,7 @@ __metadata:
"@vitest/ui": "npm:^3.2.4"
"@vue/eslint-config-typescript": "npm:^14.6.0"
"@vue/test-utils": "npm:^2.4.6"
"@vueuse/head": "npm:^2.0.0"
autoprefixer: "npm:^10.4.21"
core-js: "npm:^3.45.1"
eslint: "npm:^9.36.0"
@@ -8540,6 +8609,13 @@ __metadata:
languageName: node
linkType: hard
"packrup@npm:^0.1.2":
version: 0.1.2
resolution: "packrup@npm:0.1.2"
checksum: 10c0/8236a89e02a86a1539ee7ff920050544f2abcdc74d1a4c16c6182ad23ca36b8b686adb2203f08ae23f422852d856ff7213e18411a7a3f1ac90b1f850b0b6e86d
languageName: node
linkType: hard
"parent-module@npm:^1.0.0":
version: 1.0.1
resolution: "parent-module@npm:1.0.1"
@@ -10941,6 +11017,18 @@ __metadata:
languageName: node
linkType: hard
"unhead@npm:1.11.20":
version: 1.11.20
resolution: "unhead@npm:1.11.20"
dependencies:
"@unhead/dom": "npm:1.11.20"
"@unhead/schema": "npm:1.11.20"
"@unhead/shared": "npm:1.11.20"
hookable: "npm:^5.5.3"
checksum: 10c0/cce50a79509984679d3b8f7a2299f7ec7e252c8efdac76e9156ffa816c04c53ff25e526ead88df4cb67dc016b98c37adec8c3e93b7322a972561d3f4cd1c21d8
languageName: node
linkType: hard
"unimport@npm:^5.4.0":
version: 5.4.0
resolution: "unimport@npm:5.4.0"
@@ -11865,6 +11953,13 @@ __metadata:
languageName: node
linkType: hard
"zhead@npm:^2.2.4":
version: 2.2.4
resolution: "zhead@npm:2.2.4"
checksum: 10c0/3d166fb661f1b7fdf8a0ef2222d9e574ab241e72141f2f1fda62a9250ca73aabf2eaf0d66046a3984cd24d1dd9bac231338c6271684d6b8caa6b66af7c45f275
languageName: node
linkType: hard
"zod@npm:^3.24.1":
version: 3.25.76
resolution: "zod@npm:3.25.76"