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
60 changes: 20 additions & 40 deletions composables/useBlogContent.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,13 @@
import { createError, queryCollection } from '#imports'
import type { PageCollectionItemBase } from '@nuxt/content'
import { createError } from '#imports'
import type { LocaleObject } from 'vue-i18n-routing'
import { useContentRepository } from '~/composables/useContentRepository'
import type { Locale } from '~/utils/content/collections'
import type {
BlogArticle,
BlogAlternateHeader,
BlogAuthorProfile
} from '~/types/blog'

type BlogCollectionKey = 'blog_de' | 'blog_en'
type AuthorCollectionItem = BlogAuthorProfile & PageCollectionItemBase

declare module '@nuxt/content' {
interface PageCollections {
authors: AuthorCollectionItem
}
interface Collections {
authors: AuthorCollectionItem
}
}

const normalizeReleaseDate = (entry: BlogArticle): Date | null => {
const raw = entry.releaseDate ?? entry.pubDate
if (!raw) return null
Expand Down Expand Up @@ -84,17 +73,13 @@ export interface BlogOverviewOptions {
*/
export function useBlogOverview(options: BlogOverviewOptions = {}) {
const { locale } = useI18n()
const blogCollection = computed<BlogCollectionKey>(
() => (`blog_${locale?.value || 'de'}`) as BlogCollectionKey
)
const repo = useContentRepository()
const activeLocale = computed<Locale>(() => (locale?.value || 'de') as Locale)

const { data: allPostsData } = useAsyncData<BlogArticle[]>(
() => `all-posts-${locale.value}`,
() =>
queryCollection(blogCollection.value)
.order('pubDate', 'DESC')
.all(),
{ watch: [locale] }
() => `all-posts-${activeLocale.value}`,
() => repo.listBlogArticles(activeLocale.value),
{ watch: [activeLocale] }
)

const visiblePosts = computed<BlogArticle[]>(() => {
Expand Down Expand Up @@ -143,9 +128,8 @@ export function useBlogOverview(options: BlogOverviewOptions = {}) {
export async function useBlogArticle() {
const { locale, locales } = useI18n()
const route = useRoute()
const blogCollection = computed<BlogCollectionKey>(
() => (`blog_${locale?.value || 'de'}`) as BlogCollectionKey
)
const repo = useContentRepository()
const activeLocale = computed<Locale>(() => (locale?.value || 'de') as Locale)
const availableLocales = computed<LocaleObject[]>(() =>
normalizeLocales((locales.value || []) as unknown[])
)
Expand Down Expand Up @@ -185,9 +169,7 @@ export async function useBlogArticle() {
async () => {
if (!slug.value) return null

const doc = (await queryCollection(blogCollection.value)
.where('slug', '=', slug.value)
.first()) as BlogArticle | null
const doc = await repo.getBlogArticleBySlug(activeLocale.value, slug.value)

// A missing or not-yet-released article is a 404. Returning a marker
// (instead of throwing here) lets us raise a single fatal error after
Expand All @@ -203,17 +185,13 @@ export async function useBlogArticle() {

const authorDocs = slugs.length
? await Promise.all(
slugs.map((authorSlug) =>
queryCollection('authors')
.where('slug', '=', authorSlug)
.first()
)
slugs.map((authorSlug) => repo.getAuthorBySlug(authorSlug))
)
: []

return {
article: doc,
authors: (authorDocs.filter(Boolean) as AuthorCollectionItem[]) || []
authors: authorDocs.filter((a): a is BlogAuthorProfile => Boolean(a))
}
},
{ watch: [locale, slug] }
Expand Down Expand Up @@ -266,17 +244,19 @@ export async function useBlogArticle() {
return
}

if (!blog.value.translationKey) {
const translationKey = blog.value.translationKey
if (!translationKey) {
publishLocaleParams(localeSlugs)
return
}

const otherLocales = availableLocales.value.filter((l) => l.code !== locale.value)
for (const otherLocale of otherLocales) {
const translated = await queryCollection(`blog_${otherLocale.code}` as BlogCollectionKey)
.where('translationKey', '=', blog.value?.translationKey)
.first()
const translatedSlug = (translated as BlogArticle | null)?.slug
const translated = await repo.getBlogArticleByTranslationKey(
otherLocale.code as Locale,
translationKey
)
const translatedSlug = translated?.slug
if (translatedSlug) localeSlugs[otherLocale.code] = translatedSlug
}

Expand Down
21 changes: 21 additions & 0 deletions composables/useContentRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { ContentRepository } from '~/utils/content/repository'
import { createNuxtContentAdapter } from '~/utils/content/nuxtContentAdapter'

let instance: ContentRepository | null = null

/**
* Returns the active {@link ContentRepository}. This is the single place that
* decides which content backend the app talks to — swapping @nuxt/content for
* a headless CMS later means changing only the adapter constructed here (or
* branching on `useRuntimeConfig().public.content.provider`); no composable,
* page or component needs to change.
*
* The adapter is stateless, so a module-level singleton is safe for both SSR
* and client.
*/
export function useContentRepository(): ContentRepository {
if (!instance) {
instance = createNuxtContentAdapter()
}
return instance
}
26 changes: 10 additions & 16 deletions composables/useFaqContent.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import { queryCollection } from '#imports'
import type { PageCollectionItemBase } from '@nuxt/content'

type FaqCollectionKey = 'faq_de' | 'faq_en'

export interface FaqDocument extends PageCollectionItemBase {
key: string
question: string
order?: number
}
import { useContentRepository } from '~/composables/useContentRepository'
import type { Locale } from '~/utils/content/collections'
import type { FaqEntry } from '~/types/faq'

/**
* Fetches all FAQ entries for the active locale, ordered by the optional
Expand All @@ -17,15 +10,16 @@ export interface FaqDocument extends PageCollectionItemBase {
*/
export function useFaqContent() {
const { locale } = useI18n()
const collection = computed<FaqCollectionKey>(() => (`faq_${locale?.value || 'en'}`) as FaqCollectionKey)
const repo = useContentRepository()
const activeLocale = computed<Locale>(() => (locale?.value || 'en') as Locale)

const { data: entries } = useAsyncData<FaqDocument[]>(
() => `faq-${collection.value}`,
() => queryCollection(collection.value).order('order', 'ASC').all() as Promise<FaqDocument[]>,
{ watch: [collection] }
const { data: entries } = useAsyncData<FaqEntry[]>(
() => `faq-${activeLocale.value}`,
() => repo.listFaqEntries(activeLocale.value),
{ watch: [activeLocale] }
)

const items = computed<FaqDocument[]>(() => entries.value || [])
const items = computed<FaqEntry[]>(() => entries.value || [])

return { items }
}
47 changes: 17 additions & 30 deletions composables/useHomeContent.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useContentRepository } from '~/composables/useContentRepository'
import type { Locale } from '~/utils/content/collections'
import type {
HomeCarouselDocument,
HomeCarouselSlide,
Expand All @@ -7,44 +9,29 @@ import type {

export function useHomeContent() {
const { locale } = useI18n()
const repo = useContentRepository()
const activeLocale = computed<Locale>(() => (locale?.value || 'de') as Locale)

const fetchCollection = async <T>(key: string) => {
// @ts-expect-error queryCollection is provided by @nuxt/content at runtime
return (await queryCollection(key).all()) as T[]
}

// Server concept content from Nuxt Content (i18n)
const { data: conceptData } = useAsyncData<ServerConceptDocument[]>(
'server-concept-home',
() => fetchCollection<ServerConceptDocument>('server_concept_' + (locale?.value || 'de')),
{ watch: [locale] }
)

const concept = computed<ServerConceptDocument | null>(
() => (conceptData.value?.[0] as ServerConceptDocument | undefined) ?? null
)

// Server Connect content from Nuxt Content (i18n)
const { data: connectData } = useAsyncData<ServerConnectDocument[]>(
'server-connect',
() => fetchCollection<ServerConnectDocument>('server_connect_' + (locale?.value || 'de')),
{ watch: [locale] }
const { data: concept } = useAsyncData<ServerConceptDocument | null>(
() => `server-concept-home-${activeLocale.value}`,
() => repo.getServerConcept(activeLocale.value),
{ watch: [activeLocale] }
)

const connect = computed<ServerConnectDocument | null>(
() => (connectData.value?.[0] as ServerConnectDocument | undefined) ?? null
const { data: connect } = useAsyncData<ServerConnectDocument | null>(
() => `server-connect-${activeLocale.value}`,
() => repo.getServerConnect(activeLocale.value),
{ watch: [activeLocale] }
)

// Carousel content from Nuxt Content (i18n)
const { data: homeCarousel } = useAsyncData<HomeCarouselDocument[]>(
'home-carousel',
() => fetchCollection<HomeCarouselDocument>('home_carousel_' + (locale?.value || 'de')),
{ watch: [locale] }
const { data: homeCarousel } = useAsyncData<HomeCarouselDocument | null>(
() => `home-carousel-${activeLocale.value}`,
() => repo.getHomeCarousel(activeLocale.value),
{ watch: [activeLocale] }
)

const slides = computed<HomeCarouselSlide[]>(() => {
const doc = homeCarousel.value?.[0] as HomeCarouselDocument | undefined
return (doc?.slides as HomeCarouselSlide[] | undefined) ?? []
return (homeCarousel.value?.slides as HomeCarouselSlide[] | undefined) ?? []
})

return {
Expand Down
15 changes: 7 additions & 8 deletions composables/useSponsoring.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { queryCollection } from '#imports'
import { useContentRepository } from '~/composables/useContentRepository'
import type { Locale } from '~/utils/content/collections'
import type { SponsorEntry, SponsorsDocument } from '~/types/sponsoring'

export function useSponsoring() {
const { locale } = useI18n()
type SponsorsCollectionKey = 'sponsors_de' | 'sponsors_en'
const collectionKey = computed<SponsorsCollectionKey>(
() => (`sponsors_${locale?.value || 'de'}`) as SponsorsCollectionKey
)
const repo = useContentRepository()
const activeLocale = computed<Locale>(() => (locale?.value || 'de') as Locale)

const { data } = useAsyncData<SponsorsDocument | null>(
'sponsors',
() => queryCollection(collectionKey.value).first(),
{ watch: [collectionKey] }
() => `sponsors-${activeLocale.value}`,
() => repo.getSponsorsDocument(activeLocale.value),
{ watch: [activeLocale] }
)

const sponsors = computed<SponsorEntry[]>(() => {
Expand Down
18 changes: 9 additions & 9 deletions composables/useTeamProfile.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import { useContentRepository } from '~/composables/useContentRepository'
import type { Locale } from '~/utils/content/collections'
import type { TeamDocument, TeamMember } from '~/types/team'

export function useTeamProfile(slugOverride?: string) {
const route = useRoute()
const { locale } = useI18n()
const repo = useContentRepository()
const activeLocale = computed<Locale>(() => (locale?.value || 'de') as Locale)

const slug = computed(() => slugOverride ?? (route.params.slug as string))

const { data: teamData } = useAsyncData<TeamDocument[]>(
() => `team-profile-${locale.value}`,
() => {
// @ts-ignore provided by @nuxt/content
return queryCollection('team_' + (locale?.value || 'de')).all()
},
{ watch: [locale] }
const { data: teamDoc } = useAsyncData<TeamDocument | null>(
() => `team-profile-${activeLocale.value}`,
() => repo.getTeamDocument(activeLocale.value),
{ watch: [activeLocale] }
)

const member = computed<TeamMember | null>(() => {
const doc = teamData.value?.[0] as TeamDocument | undefined
const list = doc?.members || []
const list = teamDoc.value?.members || []
return (list as TeamMember[]).find((m) => m.slug === slug.value) || null
})

Expand Down
17 changes: 17 additions & 0 deletions types/faq.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { PageCollectionItemBase } from '@nuxt/content'

/**
* Domain model for a single FAQ entry.
*
* NOTE: `body`/`PageCollectionItemBase` is currently required because the FAQ
* page renders the Markdown body via `<ContentRenderer>`. This is the one
* remaining provider-specific coupling at the rendering layer; a future CMS
* adapter would need to map its rich-text payload onto this shape (or the
* rendering would move to plain HTML). Data access itself is already behind
* the ContentRepository.
*/
export interface FaqEntry extends PageCollectionItemBase {
key: string
question: string
order?: number
}
Loading
Loading