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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
# Public URL, used for OG Image when running nuxt generate
NUXT_PUBLIC_SITE_URL=https://fmeyer.dev

# Google Search Console verification token (from the meta-tag method).
# Leave empty to omit the meta tag entirely.
NUXT_PUBLIC_GOOGLE_SITE_VERIFICATION=
49 changes: 46 additions & 3 deletions app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,24 @@ const canonicalUrl = computed(() => {
return new URL(route.path || '/', runtimeConfig.public.siteUrl).toString()
})

const verificationMeta = computed(() => {
const token = runtimeConfig.public.googleSiteVerification
return token ? [{ name: 'google-site-verification', content: token }] : []
})

useHead({
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ key: 'theme-color', name: 'theme-color', content: color },
...(runtimeConfig.public.noindex ? [{ name: 'robots', content: 'noindex, nofollow' }] : [])
{ name: 'author', content: 'Fabian Meyer' },
{
name: 'robots',
content: runtimeConfig.public.noindex
? 'noindex, nofollow'
: 'index, follow, max-image-preview:large'
},
...verificationMeta.value
],
link: [
{ rel: 'icon', href: '/favicon.ico' },
Expand All @@ -27,15 +39,46 @@ useHead({
useSeoMeta({
titleTemplate: '%s - fmeyer.dev',
ogSiteName: 'fmeyer.dev',
ogType: 'website',
ogLocale: 'en_US',
ogUrl: canonicalUrl,
twitterCard: 'summary'
twitterCard: 'summary_large_image'
})

useSchemaOrg([
defineWebSite({
name: 'fmeyer.dev',
description: 'Personal site of Fabian Meyer — Leader, software engineer and AI mentor at eventim Tech.',
inLanguage: 'en'
}),
defineWebPage(),
definePerson({
name: 'Fabian Meyer',
image: '/profile/fabian-meyer-portrait.jpg',
jobTitle: 'Chapter Lead Vue, Software Engineer, AI Mentor',
worksFor: { name: 'eventim Tech GmbH' },
sameAs: [
'https://github.com/dinooo13',
'https://linkedin.com/in/fabian-meyer-02038813a'
]
})
])
</script>

<template>
<UApp>
<a
href="#main-content"
class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:rounded-md focus:bg-primary focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:text-white focus:shadow-lg focus:outline-2 focus:outline-offset-2 focus:outline-primary"
>
Skip to main content
</a>
<NuxtLayout>
<UMain class="relative">
<UMain
id="main-content"
class="relative"
tabindex="-1"
>
<NuxtPage />
</UMain>
</NuxtLayout>
Expand Down
26 changes: 26 additions & 0 deletions app/assets/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,29 @@
background-color: #474747;
}
}

:focus-visible {
outline: 2px solid var(--ui-primary, currentColor);
outline-offset: 2px;
border-radius: 0.25rem;
}

[id="main-content"]:focus {
outline: none;
}

@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}

::view-transition-old(root),
::view-transition-new(root) {
animation: none !important;
}
}
26 changes: 17 additions & 9 deletions app/components/AppFooter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,24 @@ const { footer } = useAppConfig()
</script>

<template>
<footer class="z-10 bg-default py-8">
<footer
class="z-10 bg-default py-8"
role="contentinfo"
>
<UContainer class="flex flex-col items-center gap-3">
<div class="flex items-center gap-2">
<UButton
v-for="link of footer?.links"
:key="link['aria-label'] || link.to"
v-bind="{ size: 'xs', color: 'neutral', variant: 'ghost', ...link }"
class="text-highlighted hover:text-highlighted"
/>
</div>
<nav aria-label="Social and contact links">
<ul class="flex items-center gap-2">
<li
v-for="link of footer?.links"
:key="link['aria-label'] || link.to"
>
<UButton
v-bind="{ size: 'xs', color: 'neutral', variant: 'ghost', ...link }"
class="text-highlighted hover:text-highlighted"
/>
</li>
</ul>
</nav>
<p class="text-xs text-muted">
{{ footer.credits }}
</p>
Expand Down
8 changes: 6 additions & 2 deletions app/components/AppHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ defineProps<{
</script>

<template>
<div class="fixed top-2 sm:top-4 mx-auto left-1/2 transform -translate-x-1/2 z-10">
<header
class="fixed top-2 sm:top-4 mx-auto left-1/2 transform -translate-x-1/2 z-10"
role="banner"
>
<UNavigationMenu
:items="links"
variant="link"
color="neutral"
aria-label="Primary"
class="bg-muted/80 backdrop-blur-sm rounded-full px-2 sm:px-4 border border-muted/50 shadow-lg shadow-neutral-950/5"
:ui="{
link: 'px-2 py-1',
Expand All @@ -22,5 +26,5 @@ defineProps<{
<ColorModeButton />
</template>
</UNavigationMenu>
</div>
</header>
</template>
65 changes: 65 additions & 0 deletions app/components/OgImage/NuxtSeo.satori.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<script setup>
import { computed } from "vue";
const props = defineProps({
colorMode: { type: String, required: false, default: "light" },
title: { type: String, required: false, default: "title" },
description: { type: String, required: false },
isPro: { type: Boolean, required: false }
});
const themeColor = computed(() => props.isPro ? "124, 58, 237" : "34, 197, 94");
</script>

<template>
<div
class="w-full h-full justify-center items-center relative p-10 lg:p-[60px] bg-white text-neutral-900 dark:bg-neutral-900 dark:text-neutral-50"
>
<!-- Gradient background -->
<div
class="absolute top-0 left-0 right-0 bottom-0" :style="{
backgroundImage: `radial-gradient(ellipse 100% 100% at 100% 100%, rgba(${themeColor}, 0.15) 0%, transparent 60%)`
}"
/>
<div
class="absolute top-0 left-0 right-0 bottom-0" :style="{
backgroundImage: `radial-gradient(ellipse 100% 100% at 0.1% 0.1%, rgba(${themeColor}, 0.1) 0%, transparent 50%)`
}"
/>

<div class="w-full flex-col justify-center relative items-center text-center gap-5 lg:gap-8">
<!-- Logo -->
<div class="flex items-center gap-1">
<svg viewBox="0 0 64 64" class="w-10 h-10 lg:w-16 lg:h-16">
<defs>
<linearGradient :id="isPro ? 'nsLine2' : 'nsLine1'" x1="0%" y1="100%" x2="100%" y2="0%">
<stop offset="0%" :stop-color="isPro ? '#7c3aed' : '#22c55e'" />
<stop offset="100%" :stop-color="isPro ? '#c4b5fd' : '#86efac'" />
</linearGradient>
<linearGradient :id="isPro ? 'nsFill2' : 'nsFill1'" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" :stop-color="isPro ? '#7c3aed' : '#22c55e'" stop-opacity="0.6" />
<stop offset="100%" :stop-color="isPro ? '#7c3aed' : '#22c55e'" stop-opacity="0" />
</linearGradient>
</defs>
<path d="M8 52 Q20 48 24 36 T40 20 T56 12 L56 56 L8 56 Z" :fill="`url(#${isPro ? 'nsFill2' : 'nsFill1'})`" />
<path d="M8 52 Q20 48 24 36 T40 20 T56 12" fill="none" :stroke="`url(#${isPro ? 'nsLine2' : 'nsLine1'})`" stroke-width="4" stroke-linecap="round" />
<circle cx="56" cy="12" r="6" :fill="`url(#${isPro ? 'nsLine2' : 'nsLine1'})`" />
</svg>
<span class="text-[32px] lg:text-[42px] font-bold tracking-tight">
Nuxt<span :class="isPro ? 'text-violet-500' : 'text-green-500'" class="ml-1">SEO{{ isPro ? " Pro" : "" }}</span>
</span>
</div>

<!-- Title -->
<h1
class="w-full justify-center text-center text-[48px] lg:text-[80px] font-bold m-0 leading-tight max-w-[700px] lg:max-w-[1000px]"
style="display: block; line-clamp: 3; text-overflow: ellipsis; text-wrap: balance;"
>
{{ title }}
</h1>

<!-- Description -->
<p v-if="description" class="text-[24px] lg:text-[32px] opacity-70 max-w-[650px] lg:max-w-[900px] leading-relaxed" style="display: block; line-clamp: 2; text-overflow: ellipsis;">
{{ description }}
</p>
</div>
</div>
</template>
8 changes: 6 additions & 2 deletions app/components/landing/Focus.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ defineProps<{
description: 'max-w-2xl text-left text-sm text-muted'
}"
>
<div class="grid gap-4 md:grid-cols-2">
<ul
class="grid gap-4 md:grid-cols-2 list-none p-0"
:aria-label="page.focus.title"
>
<Motion
v-for="(item, index) in page.focus.items"
:key="item.title"
as="li"
:initial="{ opacity: 0, transform: 'translateY(12px)' }"
:while-in-view="{ opacity: 1, transform: 'translateY(0)' }"
:transition="{ delay: index * 0.08 }"
Expand All @@ -47,6 +51,6 @@ defineProps<{
</p>
</UCard>
</Motion>
</div>
</ul>
</UPageSection>
</template>
2 changes: 1 addition & 1 deletion app/components/landing/Hero.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ defineProps<{
:animate="heroAnimate"
:transition="heroTransition(0.2)"
>
{{ page.hero.role }}
<h1>{{ page.hero.role }}</h1>
</Motion>
</div>
</template>
Expand Down
15 changes: 12 additions & 3 deletions app/components/landing/WorkExperience.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,25 @@ const visibleItems = computed(() => {
description: 'max-w-2xl text-left text-sm text-muted'
}"
>
<div class="relative space-y-6 border-l border-default pl-6 sm:pl-8">
<ol
id="experience-list"
class="relative space-y-6 border-l border-default pl-6 sm:pl-8 list-none"
:aria-label="page.experience.title"
>
<Motion
v-for="(experience, index) in visibleItems"
:key="experience.title"
as="li"
:initial="{ opacity: 0, transform: 'translateY(16px)' }"
:while-in-view="{ opacity: 1, transform: 'translateY(0)' }"
:transition="{ delay: index * 0.1 }"
:in-view-options="{ once: true }"
class="relative"
>
<span class="absolute -left-[1.95rem] top-6 size-3 rounded-full border border-default bg-default sm:-left-[2.45rem]" />
<span
aria-hidden="true"
class="absolute -left-[1.95rem] top-6 size-3 rounded-full border border-default bg-default sm:-left-[2.45rem]"
/>

<UCard
class="border border-default"
Expand Down Expand Up @@ -100,7 +108,7 @@ const visibleItems = computed(() => {
</ul>
</UCard>
</Motion>
</div>
</ol>

<div
v-if="hasHiddenItems"
Expand All @@ -111,6 +119,7 @@ const visibleItems = computed(() => {
variant="ghost"
class="inline-flex cursor-pointer items-center gap-2 rounded-md px-3 py-2 text-sm text-highlighted hover:bg-muted/50 hover:text-highlighted"
:aria-expanded="expanded"
aria-controls="experience-list"
@click="expanded = !expanded"
>
<span>{{ expanded ? 'Show less experience' : 'Show more experience' }}</span>
Expand Down
2 changes: 1 addition & 1 deletion app/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ if (!page.value) {

usePageSeo(page.value)

defineOgImage()
defineOgImage('NuxtSeoSatori')
</script>

<template>
Expand Down
2 changes: 1 addition & 1 deletion app/pages/labs/[slug].vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ if (!lab.value) {

usePageSeo(lab.value)

defineOgImage()
defineOgImage('NuxtSeoSatori')

const formattedDate = computed(() => formatLabDate(lab.value!.date))
const hasImage = computed(() => Boolean(lab.value?.image))
Expand Down
17 changes: 13 additions & 4 deletions app/pages/labs/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,25 @@ if (!page.value) {

usePageSeo(page.value)

defineOgImage()
defineOgImage('NuxtSeoSatori')
</script>

<template>
<UPage v-if="page">
<UPageHero
:title="page.title"
:description="page.description"
:ui="{
title: '!mx-0 max-w-3xl text-left',
description: '!mx-0 max-w-2xl text-left',
links: 'justify-start'
}"
>
<template #title>
<h1 class="text-3xl font-bold tracking-tight text-highlighted sm:text-4xl lg:text-5xl">
{{ page.title }}
</h1>
</template>

<template #links>
<div
v-if="page.links?.length"
Expand All @@ -53,18 +58,22 @@ defineOgImage()
container: '!pt-0'
}"
>
<div class="grid gap-6 lg:grid-cols-3">
<ul
class="grid gap-6 lg:grid-cols-3 list-none p-0"
aria-label="Lab projects"
>
<Motion
v-for="(lab, index) in labs"
:key="lab.title"
as="li"
:initial="{ opacity: 0, transform: 'translateY(12px)' }"
:while-in-view="{ opacity: 1, transform: 'translateY(0)' }"
:transition="{ delay: index * 0.08 }"
:in-view-options="{ once: true }"
>
<LabCard :lab="lab" />
</Motion>
</div>
</ul>
</UPageSection>
</UPage>
</template>
2 changes: 1 addition & 1 deletion app/pages/speaking/[slug].vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ usePageSeo({
description: talk.value.summary ?? talk.value.description
})

defineOgImage()
defineOgImage('NuxtSeoSatori')

const organizerSubtitle = computed(() => {
if (!talk.value?.organizerTitle) {
Expand Down
Loading
Loading