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
8 changes: 5 additions & 3 deletions .coderabbit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ language: en-US
enable_free_tier: false

reviews:
# "assertive" emits a broad range of issues including nitpicks and improvements. "chill" only reports critical issues.
# "assertive" emits a broad range of issues including nitpicks and improvements.
# "chill" provides lighter feedback with fewer non-critical comments.
profile: assertive

# If true, CodeRabbit will request changes on the PR if there are issues. If false, it will only comment.
Expand Down Expand Up @@ -37,6 +38,7 @@ reviews:
changed_files_summary: true

# Generate sequence diagrams to visualize control flow.
# Very useful for understanding logic in Nuxt server routes or complex composables.
sequence_diagrams: true

# Estimate the effort required to review the PR.
Expand Down Expand Up @@ -98,7 +100,7 @@ reviews:
- `app/components/layout/` - Site-wide layout (header, footer)
- `app/components/content/` - Domain content components (cards, carousel)
- `app/components/OgImage/` - Satori-rendered OG image templates
- `content/` - File-based CMS (Markdown, YAML): talks/, projects/, clients/, publications/
- `content/` - File-based CMS (Markdown, YAML): talks/, projects/, clients/, publications/, socials/
- `public/` - Static assets
- `scripts/` - Maintenance scripts

Expand Down Expand Up @@ -131,7 +133,7 @@ reviews:
- Borders: `rounded-lg` for cards/buttons/images, `rounded` for tags/badges, `rounded-full` for avatars.

### Content Model (@nuxt/content)
- Collections: talks (Page, `talks/*.md`), projects (Page, `projects/*.md`), clients (Page, `clients/*.md`), publications (Data, `publications/*.yml`).
- Collections: talks (Page, `talks/*.md`), projects (Page, `projects/*.md`), clients (Page, `clients/*.md`), publications (Data, `publications/*.yml`), socials (Data, `socials/*.yml`).
- Schemas defined in `content.config.ts` using Zod.
- Talk filenames: `YYYY-MM-DD-event-slug.md`. Slugs derived from filenames (kebab-case).
- Testimonials embedded as frontmatter arrays in talk/project/client files.
Expand Down
17 changes: 17 additions & 0 deletions app/app.vue
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
<script setup lang="ts">
/**
* Load social links from the `socials` content collection and injects them into the Schema.org Person identity
* via `sameAs`.
*/
const { data: socials } = await useAsyncData('socials', () =>
queryCollection('socials').order('sortOrder', 'ASC').all(),
)
if (socials.value?.length) {
useSchemaOrg([
definePerson({
sameAs: socials.value.map(s => s.url),
}),
])
}
</script>

<template>
<NuxtRouteAnnouncer />
<NuxtPage />
Expand Down
140 changes: 140 additions & 0 deletions app/components/OgImage/OgImageHome.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<script setup lang="ts">
/**
* Default OG image component for the homepage.
* Rendered by Satori (not a browser) - must use inline styles, <img>, and hardcoded colors.
*/
withDefaults(defineProps<{
/** Page title override. */
title?: string
/** Page description override. */
description?: string
}>(), {
title: 'todde.tv',
description: 'IT consultant, senior full-stack developer, and conference speaker.',
})
</script>

<template>
<div
:style="{
width: '1200px',
height: '630px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
backgroundColor: '#0a0a0b',
fontFamily: 'Inter, system-ui, sans-serif',
padding: '60px',
position: 'relative',
overflow: 'hidden',
}"
>
<!-- Radial accent glow (top-right, behind avatar) -->
<div
:style="{
position: 'absolute',
top: '-80px',
right: '100px',
width: '700px',
height: '700px',
borderRadius: '50%',
background: 'radial-gradient(circle, rgba(0,220,130,0.25) 0%, transparent 65%)',
}"
/>

<!-- Secondary glow (bottom-left) -->
<div
:style="{
position: 'absolute',
bottom: '-200px',
left: '-100px',
width: '500px',
height: '500px',
borderRadius: '50%',
background: 'radial-gradient(circle, rgba(0,220,130,0.18) 0%, transparent 65%)',
}"
/>

<!-- Subtle horizontal rule -->
<div
:style="{
position: 'absolute',
top: '315px',
left: '60px',
width: '600px',
height: '1px',
background: 'linear-gradient(90deg, rgba(0,220,130,0.15), transparent)',
}"
/>

<!-- Title and description (vertically centered) -->
<div
:style="{
display: 'flex',
flexDirection: 'column',
flexWrap: 'nowrap',
gap: '20px',
width: '670px',
}"
>
<div
:style="{
fontSize: '72px',
fontWeight: 700,
color: '#fafafa',
lineHeight: 1.1,
letterSpacing: '-0.02em',
}"
>
{{ title }}
</div>
<div
:style="{
fontSize: '30px',
fontWeight: 400,
color: '#a1a1aa',
lineHeight: 1.5,
}"
>
{{ description }}
</div>
</div>

<!-- Avatar with accent gradient border (large, centered-right) -->
<div
:style="{
position: 'absolute',
top: '115px',
right: '80px',
width: '350px',
height: '350px',
borderRadius: '50%',
background: 'linear-gradient(135deg, #00dc82, #00c474)',
padding: '5px',
}"
>
<img
alt="Thorsten Seyschab"
src="/avatar.jpg"
:style="{
width: '340px',
height: '340px',
borderRadius: '50%',
objectFit: 'cover',
}"
>
</div>

<!-- Footer (absolute to stay at bottom despite centered content) -->
<div
:style="{
position: 'absolute',
bottom: '60px',
left: '60px',
right: '60px',
}"
>
<OgImageFooter />
</div>
</div>
</template>
50 changes: 50 additions & 0 deletions app/components/OgImage/components/OgImageFooter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<script setup lang="ts">
/**
* Shared footer for all OG image components.
* Renders author name (left), site URL (right), and a bottom gradient accent line.
* Satori constraints: inline styles only, no Tailwind, hardcoded colors.
*/
</script>

<template>
<!-- Author name and site URL -->
<div
:style="{
display: 'flex',
flexWrap: 'nowrap',
justifyContent: 'space-between',
alignItems: 'flex-end',
}"
>
<div
:style="{
fontSize: '24px',
fontWeight: 600,
color: '#00dc82',
}"
>
Thorsten Seyschab
</div>
<div
:style="{
fontSize: '22px',
fontWeight: 400,
color: '#71717a',
}"
>
todde.tv
</div>
</div>

<!-- Bottom gradient accent line -->
<div
:style="{
position: 'absolute',
bottom: '0',
left: '0',
right: '0',
height: '4px',
background: 'linear-gradient(90deg, #00dc82, #00c474, transparent)',
}"
/>
</template>
10 changes: 10 additions & 0 deletions app/pages/[...slug].vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ const { data: page } = await useAsyncData('page-' + route.path, () => {
if (!page.value) {
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
}

if (page.value.seo) {
useSeoMeta(page.value.seo)
}
if (page.value.head) {
useHead(page.value.head as Record<string, unknown>)
}
if (page.value.ogImage) {
defineOgImage(page.value.ogImage)
}
</script>

<template>
Expand Down
30 changes: 27 additions & 3 deletions content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,36 @@
*/

import { defineContentConfig, defineCollection } from '@nuxt/content'
import { asSeoCollection } from '@nuxtjs/seo/content'
import { z } from 'zod'

export default defineContentConfig({
collections: {
content: defineCollection({
type: 'page',
source: '**',
content: defineCollection(
asSeoCollection({
type: 'page',
source: {
include: '**',
exclude: [
'socials/**',
],
},
}),
),

socials: defineCollection({
type: 'data',
source: 'socials/*.yml',
schema: z.object({
/** Display name of the social platform (e.g. "GitHub"). */
name: z.string(),
/** Full profile URL. */
url: z.url(),
/** Iconify icon identifier (e.g. "simple-icons:github"). */
icon: z.string(),
/** Controls display order (ascending). */
sortOrder: z.number().default(99),
}),
}),
},
})
27 changes: 27 additions & 0 deletions content/index.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
---
title: todde.tv - Thorsten Seyschab # REQUIRED - sets <title>, og:title, twitter:title
description: Personal portfolio of Thorsten Seyschab - IT consultant, senior full-stack developer, and conference speaker. # REQUIRED - sets <meta description>, og:description, twitter:description
head:
meta:
- name: author # OPTIONAL - already set globally in nuxt.config.ts app.head.meta
content: Thorsten Seyschab
seo:
ogTitle: todde.tv - Thorsten Seyschab # REDUNDANT - auto-derived from title
ogDescription: Personal portfolio of Thorsten Seyschab - IT consultant, senior full-stack developer, and conference speaker. # REDUNDANT - auto-derived from description
ogType: website # REDUNDANT - defaults to "website"
ogUrl: https://todde.tv # REDUNDANT - auto-set from route + site.url
ogImage: https://todde.tv/__og-image__/image/og.png # REDUNDANT - auto-generated by nuxt-og-image / Satori; never hardcode this URL
twitterCard: summary_large_image # REDUNDANT - auto-set when OG image exists
twitterTitle: todde.tv - Thorsten Seyschab # REDUNDANT - auto-derived from title
twitterDescription: Personal portfolio of Thorsten Seyschab - IT consultant, senior full-stack developer, and conference speaker. # REDUNDANT - auto-derived from description
ogImage:
component: OgImageHome # REDUNDANT - already the default in nuxt.config.ts ogImage.defaults
props:
title: Thorsten Seyschab # OPTIONAL - in OgImageHome withDefaults() there is a default
description: IT consultant, senior full-stack developer, and conference speaker. # OPTIONAL - in OgImageHome withDefaults() there is a default
Comment thread
toddeTV marked this conversation as resolved.
sitemap:
lastmod: 2026-03-04 # USEFUL - Google uses this for crawl scheduling
changefreq: monthly # IGNORED - Google ignores changefreq since ~2023
priority: 1.0 # IGNORED - Google ignores priority; only relative within your own sitemap
---

# todde.tv

Welcome.
4 changes: 4 additions & 0 deletions content/socials/bluesky.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name: Bluesky
url: https://bsky.app/profile/todde.tv
icon: simple-icons:bluesky
sortOrder: 3
4 changes: 4 additions & 0 deletions content/socials/github.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name: GitHub
url: https://github.com/toddeTV/
icon: simple-icons:github
sortOrder: 1
4 changes: 4 additions & 0 deletions content/socials/linkedin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name: LinkedIn
url: https://www.linkedin.com/in/toddetv/
icon: simple-icons:linkedin
sortOrder: 4
4 changes: 4 additions & 0 deletions content/socials/x.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name: X (Twitter)
url: https://x.com/toddeTV
icon: simple-icons:x
sortOrder: 2
Loading