+
+
+
+
+
From 1e94f3048af7add8c304be1041181f3c9fa2f6b7 Mon Sep 17 00:00:00 2001
From: Thorsten Seyschab
Date: Sat, 7 Mar 2026 17:59:34 +0100
Subject: [PATCH 02/11] feat: add landing page
---
app/composables/useAllTestimonials.ts | 38 +++++++
app/pages/index.vue | 157 ++++++++++++++++++++++++++
2 files changed, 195 insertions(+)
create mode 100755 app/composables/useAllTestimonials.ts
create mode 100755 app/pages/index.vue
diff --git a/app/composables/useAllTestimonials.ts b/app/composables/useAllTestimonials.ts
new file mode 100755
index 0000000..881dc65
--- /dev/null
+++ b/app/composables/useAllTestimonials.ts
@@ -0,0 +1,38 @@
+export interface LinkedTestimonial {
+ quote: string
+ author: string
+ role: string
+ linkTo: string
+}
+
+export function useAllTestimonials() {
+ return useAsyncData('all-testimonials', async () => {
+ const [clients, projects, talks] = await Promise.all([
+ queryCollection('clients').all(),
+ queryCollection('projects').all(),
+ queryCollection('talks').all(),
+ ])
+
+ const result: LinkedTestimonial[] = []
+
+ for (const p of clients) {
+ for (const t of p.testimonials ?? []) {
+ result.push({ ...t, linkTo: p.path })
+ }
+ }
+
+ for (const p of projects) {
+ for (const t of p.testimonials ?? []) {
+ result.push({ ...t, linkTo: p.path })
+ }
+ }
+
+ for (const talk of talks) {
+ for (const t of talk.testimonials ?? []) {
+ result.push({ ...t, linkTo: talk.path })
+ }
+ }
+
+ return result
+ })
+}
diff --git a/app/pages/index.vue b/app/pages/index.vue
new file mode 100755
index 0000000..2ac6197
--- /dev/null
+++ b/app/pages/index.vue
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+
+
+
+ Hi, I'm
+
+
+ Thorsten Seyschab
+
+
+ IT consultant, senior full-stack developer, and conference speaker
+
+
+
+ Dresden, Germany
+
+
+
+
+
+
+
+
+
+
+
+
+ I'm an IT consultant, senior full-stack developer, and conference speaker from Germany,
+ self-employed since 2014. I help companies ship their projects, provide
+ technical guidance, and build robust applications - from architecture to deployment.
+
+
+ My main stack revolves around web technologies, modern build tooling, and
+ databases. Beyond classic web development, I have a particular passion for
+ bringing 3D experiences to the browser.
+
+
+ I hold a Master's degree in Computer Science from TUD Dresden University
+ of Technology, where I contributed to database research published at IEEE ICDE 2016 and
+ BTW 2017.
+
+
+ When I'm not working with clients, you'll find me contributing to
+ open-source projects, giving talks at international conferences like
+ Vue.js Amsterdam, NuxtNation, ViteConf, and Vue Fes Japan - or tinkering with my
+ 3D printers.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Recent Talks
+
+
+ View all talks
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ social.name }}
+
+
+ {{ social.handle }}
+
+
+
+
+
+
+
+
From b3fb7a250e0fd0158b5726835e705ddc87aea332 Mon Sep 17 00:00:00 2001
From: Thorsten Seyschab
Date: Sat, 7 Mar 2026 18:08:45 +0100
Subject: [PATCH 03/11] feat: new landing page block order and new block for
recent projects
---
app/composables/useSortedProjects.ts | 32 ++++++++++++++
app/pages/index.vue | 66 ++++++++++++++++++----------
app/pages/projects/index.vue | 23 +---------
3 files changed, 77 insertions(+), 44 deletions(-)
create mode 100644 app/composables/useSortedProjects.ts
diff --git a/app/composables/useSortedProjects.ts b/app/composables/useSortedProjects.ts
new file mode 100644
index 0000000..a48f925
--- /dev/null
+++ b/app/composables/useSortedProjects.ts
@@ -0,0 +1,32 @@
+/**
+ * Fetches all projects and returns them sorted by recency.
+ * Sort order: ongoing first, then by endDate, startDate, and stars.
+ * @param limit - Optional max number of projects to return.
+ */
+export function useSortedProjects(limit?: number) {
+ const key = limit ? `sorted-projects-${limit}` : 'sorted-projects'
+
+ return useAsyncData(key, async () => {
+ const projects = await queryCollection('projects').all()
+ const sorted = projects.sort((a, b) => {
+ // 1. Ongoing (no endDate) before completed
+ const aOngoing = a.endDate == null
+ const bOngoing = b.endDate == null
+ if (aOngoing !== bOngoing) return aOngoing ? -1 : 1
+
+ // 2. Within completed: newest endDate first
+ if (!aOngoing && !bOngoing) {
+ const endCmp = b.endDate!.localeCompare(a.endDate!)
+ if (endCmp !== 0) return endCmp
+ }
+
+ // 3. Newest startDate first
+ const startCmp = b.startDate.localeCompare(a.startDate)
+ if (startCmp !== 0) return startCmp
+
+ // 4. More stars first
+ return (b.repoStars ?? 0) - (a.repoStars ?? 0)
+ })
+ return limit ? sorted.slice(0, limit) : sorted
+ })
+}
diff --git a/app/pages/index.vue b/app/pages/index.vue
index 2ac6197..522eadd 100755
--- a/app/pages/index.vue
+++ b/app/pages/index.vue
@@ -20,12 +20,14 @@ const skills = [
const [
{ data: recentTalks },
+ { data: recentProjects },
{ data: featuredTestimonials },
{ data: socials },
] = await Promise.all([
await useAsyncData('recent-talks', () =>
queryCollection('talks').order('date', 'DESC').limit(3).all(),
),
+ await useSortedProjects(2),
await useAllTestimonials(),
await useAsyncData('index-socials-all', () =>
queryCollection('socials').where('active', '=', true).order('sortOrder', 'ASC').all(),
@@ -91,8 +93,32 @@ const [