This commit is contained in:
Sabrina Hennrich 2025-07-04 17:51:43 +02:00
parent b12ae0f00b
commit 97091be646
11 changed files with 274 additions and 62 deletions

View File

@ -1,7 +1,8 @@
<template>
<div class="ctaBox">
<h3>{{ headline }}</h3>
<p>{{ text }}</p>
<h3 v-if="headline">{{ headline }}</h3>
<p v-if="text">{{ text }}</p>
<span v-if="content" v-html="content"></span>
<button class="btn pinkBtn mt-1" role="button" @click.prevent="toggleContactBubble">
{{ buttonText }}
</button>
@ -18,8 +19,9 @@
}
const props = defineProps({
headline: { type: String, required: true },
text: { type: String, required: true },
headline: { type: String, required: false },
text: { type: String, required: false },
content: { type: String, required: false },
buttonText: { type: String, required: true }
})
</script>

View File

@ -46,15 +46,15 @@
<svg aria-hidden="true">
<use xlink:href="/assets/icons/collection.svg#phone" />
</svg>
<span>{{ companyinfo.phone }}</span>
<span>{{ companyinfo?.phone }}</span>
</p>
<div v-if="screenWidth > 768" class="pt-3">
<h3>{{ $t('contactForm.ourOffice') }}</h3>
<p class="address">
{{ companyinfo.company }}<br >
{{ companyinfo.street }} <br >
{{ companyinfo.postalcode }} {{ companyinfo.city }}
{{ companyinfo?.company }}<br >
{{ companyinfo?.street }} <br >
{{ companyinfo?.postalcode }} {{ companyinfo?.city }}
</p>
<p class="aspProf">{{ $t('contactForm.yourcontactperson') }} <b>Sabrina Hennrich</b></p>
<div class="aspBox">

View File

@ -23,7 +23,7 @@ import { useI18n } from 'vue-i18n'
import { useMainStore } from '@/stores/main'
import { useHtmlConverter } from '~/composables/useHTMLConverter'
const { t } = useI18n()
const { t, locale } = useI18n()
const { convertToHTML } = useHtmlConverter()
const props = defineProps({
@ -38,13 +38,19 @@ const button = computed(() => props.button ?? t('faqBox.btnDefault'))
const mainStore = useMainStore()
const rawFaqs = computed(() =>
mainStore.getFaqsByPageLink(props.pageLink, locale.value) ?? []
)
const accordionItems = computed(() =>
mainStore.getFaqsByPageLink(props.pageLink).map(faq => ({
rawFaqs.value.map(faq => ({
title: faq.question,
html: convertToHTML(faq.answer)
html: convertToHTML(faq.answer)
}))
)
const toggleContactBubble = () => mainStore.toggleContactBubble()
// FAQ JSON-LD

View File

@ -18,7 +18,7 @@
<div class="box">
<div class="container">
<h2 class="pt-4 pb-3">{{ title }}</h2>
<span v-html="title"></span>
<div class="marquee">
<div class="marquee-track" :style="`animation-duration: ${speed}s`">
@ -120,7 +120,7 @@ const props = defineProps({
</script>
<style lang="sass" scoped>
<style lang="sass">
.banner-wrapper
position: relative
margin: 3rem auto 6rem auto

View File

@ -1,9 +1,7 @@
<template>
<section class="services">
<div class="textBox">
<h2>Unsere Kernkompetenzen für Ihren Erfolg im Web</h2>
<h3>Vier starke Säulen für Ihre Website</h3>
<p v-show="false"><b>Vier starke Säulen für Ihre Website:</b> Von barrierefrei über SEO bis hin zu Headless CMS und KI-Kompatibilität. Entdecken Sie, wie unsere spezialisierten Leistungen Ihre digitale Präsenz voranbringen. Einfach, effektiv und auf Ihre Bedürfnisse zugeschnitten.</p>
<span v-html="getHtmlBySection('home_services_header').value"></span>
</div>
<div class="serviceBoxes">
@ -25,9 +23,10 @@
<svg>
<use :xlink:href="`/assets/icons/collection.svg#${service.icon}`" />
</svg>
<h3>{{ service.title }}</h3>
<p>{{ service.description }}</p>
<span class="link">{{ service.linktext }}</span>
<!-- <h3>{{ service.title }}</h3>
<p>{{ service.description }}</p> -->
<span v-html="getHtmlBySection(service.content).value"></span>
<span class="link">{{ $t('buttons.learnMore') }}</span>
</div>
</NuxtLinkLocale>
</div>
@ -35,36 +34,29 @@
</template>
<script setup>
const { getHtmlBySection } = usePageContentRenderer()
const services = [
{
title: 'Barrierefreie Webseiten',
description: 'Webseiten, die für alle zugänglich sind. Barrierefrei, nutzerfreundlich und technisch modern gestaltet für optimale Bedienbarkeit und bessere Reichweite.',
content: 'home_services_accessibility',
link: 'services-accessibility',
linktext: 'Mehr erfahren',
backgroundImage: '/uploads/DML_Service_Header_Accessibility_de8f72f0c1.webp',
icon: 'accessibility'
},
{
title: 'Suchmaschinen-Optimierung',
description: 'Mit technischer SEO sorgen wir für bessere Sichtbarkeit in Google & Co. Schnelle, strukturierte Webseiten, die Besucher anziehen und überzeugen.',
content: 'home_services_seo',
link: 'services-seo',
linktext: 'Mehr erfahren',
backgroundImage: '/uploads/DML_Service_Header_SEO_b11ae8940a.webp',
icon: 'search'
},
{
title: 'Headless CMS',
description: 'Flexibles Content-Management mit modernem Headless CMS. Schnelle Webseiten, einfache Pflege und volle Kontrolle über Inhalte und Design.',
content: 'home_services_cms',
link: 'services-cms',
linktext: 'Mehr erfahren',
backgroundImage: '/uploads/DML_Service_Header_CMS_a438599970.webp',
icon: 'database'
},
{
title: 'KI-Kompatibilität im Web',
description: 'Webseiten, die mit Künstlicher Intelligenz zusammenarbeiten. Zukunftssicher, smart optimiert und bereit für automatisierte Prozesse und Personalisierung.',
content: 'home_services_ai',
link: 'services-ai',
linktext: 'Mehr erfahren',
backgroundImage: '/uploads/DML_Service_Header_AI_639dd0d7b1.webp',
icon: 'ai'
},

View File

@ -38,7 +38,7 @@
{{ companyinfo?.postalcode }}
{{ companyinfo?.city }}
</p>
<p><i>({{ $t('districtSta') }} | {{ $t('upperBavaria') }})</i></p>
<p><i>({{ $t('footer.districtSta') }} | {{ $t('footer.upperBavaria') }})</i></p>
<br >
<p v-if="false" class="mb-4">
<span class="icon">

View File

@ -0,0 +1,39 @@
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { storeToRefs } from 'pinia'
import { useMainStore } from '@/stores/main'
import { useHtmlConverter } from '@/composables/useHtmlConverter'
export function usePageContentRenderer() {
const { locale } = useI18n()
const mainStore = useMainStore()
const { pagecontents } = storeToRefs(mainStore)
const { convertToHTML } = useHtmlConverter()
// Hole die Inhalte für section und aktuelle Sprache, fallback auf de oder erstes Item
const getHtmlBySection = (section: string, prepend?: string) => {
return computed(() => {
if (!pagecontents.value?.length) return ''
// Filter alle Items mit der gesuchten section
const sectionItems = pagecontents.value.filter(c => c.section === section)
if (!sectionItems.length) return ''
// Suche Item passend zur aktuellen Sprache
let contentBlock = sectionItems.find(c => c.locale === locale.value)
// Falls nicht gefunden, fallback auf deutsch
if (!contentBlock) contentBlock = sectionItems.find(c => c.locale === 'de')
// Wenn immer noch nichts, fallback auf erstes Item mit der section
if (!contentBlock) contentBlock = sectionItems[0]
if (!contentBlock?.content) return ''
// content ist Array von RichTextBlöcken, convertiere zu HTML
return convertToHTML(contentBlock.content, prepend)
})
}
return { getHtmlBySection }
}

View File

@ -1,7 +1,8 @@
{
"welcome": "Willkommen",
"districtSta": "Landkreis Starnberg",
"upperBavaria": "Oberbayern",
"footer": {
"districtSta": "Landkreis Starnberg",
"upperBavaria": "Oberbayern"
},
"menu": {
"webagency": "Webagentur",
"menuAbout": "Über uns",
@ -17,6 +18,11 @@
"references": "Referenzen",
"contact": "Kontakt"
},
"buttons": {
"learnMore": "Mehr erfahren",
"contactUs": "Kontaktieren Sie uns!",
"ourReferences": "Unsere Referenzen"
},
"referenceoverview": "Referenzübersicht",
"imprint": "Impressum",
"privacy": "Datenschutz",

View File

@ -29,6 +29,7 @@
onBeforeMount(() => {
mainStore.setDarkHeroBack(false) // Standardmäßig false
mainStore.syncLocaleWithI18n()
})
</script>

View File

@ -5,15 +5,38 @@
:aria-label="$t('pages.services.hero.ariaLabel')"
:dark-background="true"
>
<h1>High-Performance-Webseiten</h1>
<div v-html="getHtmlBySection('home_hero').value"></div>
<!-- <h1>High-Performance-Webseiten</h1>
<h2>die Ihre Zielgruppe begeistern</h2>
<h3>Schnell, effizient und leistungsstark</h3>
<p>Wir nutzen modernste Technologien wie Nuxt 3 + Strapi in Headless-Architektur, um Webseiten zu entwickeln, die zukunftssicher, robust und flexibel sind.</p>
<NuxtLinkLocale to="references" class="btn whiteBtn">Unsere Referenzen</NuxtLinkLocale>
<p>Wir nutzen modernste Technologien wie Nuxt 3 + Strapi in Headless-Architektur, um Webseiten zu entwickeln, die zukunftssicher, robust und flexibel sind.</p> -->
<NuxtLinkLocale to="references" class="btn whiteBtn">{{ $t('buttons.ourReferences') }}</NuxtLinkLocale>
</HeroBox>
<section class="whatWeDo">
<section class="dmlLoop">
<span class="loopHeader" v-html="getHtmlBySection('home_loopHeader').value"></span>
<div class="content">
<div class="column">
<span v-html="getHtmlBySection('home_loopInput').value"></span>
</div>
<div class="column">
<NuxtImg
src="/uploads/Loop_83b4311dd0.webp"
provider="strapi"
class="loopImg"
alt="Mensch"
/>
</div>
<div class="column">
<span v-html="getHtmlBySection('home_loopOutput').value"></span>
</div>
</div>
</section>
<section class="whatWeDo">
<NuxtImg
src="/uploads/laechelnde_Dame_a393baf878.webp"
provider="strapi"
@ -21,14 +44,7 @@
alt="Mensch"
/>
<div class="content">
<h2>Webseiten für Mensch und Maschine</h2>
<h3>Design das Menschen begeistert mit Struktur die Maschinen verstehen</h3>
<p>Wir entwickeln <b>performante, barrierefreie Webseiten</b> mit <b>klarer Struktur</b>,
die Nutzer überzeugen und von Suchmaschinen wie Google oder Bing, sowie KI-Systemen optimal verstanden werden.</p>
<p>Dabei setzen wir auf moderne Standards, erfüllen rechtliche Anforderungen wie Barrierefreiheit und Datenschutz
und sorgen mit <b>zukunftsfähiger Technologie</b> dafür, dass Ihre Webseite auf dem neuesten Stand ist.</p>
<div v-html="getHtmlBySection('home_human_machine').value"></div>
</div>
<NuxtImg
src="/uploads/energy_1_e15df59b8a.webp"
@ -124,12 +140,12 @@
</section> -->
<CallToActionBox
:headline="$t('pages.home.finalCall.title')"
:content="getHtmlBySection('ctaBox_home').value"
:button-text="$t('pages.home.finalCall.button')"
:button-text="$t('buttons.contactUs')"
/>
<MarqueeBanner :items="logoItems" :logo-height="60" :title="$t('pages.home.marqueeBanner.title')" :greyscale="true" />
<MarqueeBanner :items="logoItems" :logo-height="60" :title="getHtmlBySection('home_company_trustus').value" :greyscale="true" />
<FAQArea page-link="/" :headline="$t('pages.home.faqArea.headline')" />
</div>
@ -143,6 +159,8 @@ import { storeToRefs } from 'pinia';
const mainStore = useMainStore();
const { customers } = storeToRefs(mainStore);
const { getHtmlBySection } = usePageContentRenderer()
const logoItems = computed(() => {
return customers.value.map(customer => ({
text: customer.company || '',
@ -156,9 +174,85 @@ const logoItems = computed(() => {
<style lang="sass">
.homePage
.dmlLoop
width: 80%
margin: 3rem 10%
h2
font-size: clamp(1.8rem, .9rem + 2vw, 2.4rem)
margin-bottom: 0
margin-top: -.5rem
h3
font-size: clamp(1.4rem, .7rem + 2vw, 2rem)
margin: 0 0 1rem 0
h4
font-size: clamp(1.2rem, .5rem + 2vw, 1.6rem)
margin: .5rem 0 1.5rem 0
p
margin-top: 1rem
.content
display: flex
flex-direction: column
gap: 2rem
margin: 3rem 0
.column
padding: 0
border-radius: .5rem
min-width: 280px
width: calc((100% / 3) - 2rem)
// nur für 1 und 3 box
// child(1) border rights offen und child(3) border left offen
&:nth-child(1), &:nth-child(3)
//border-top: 1px solid #effbf7
border-bottom: 1px solid #effbf7
&:nth-child(1)
border-left: 1px solid #effbf7
h4
background-image: linear-gradient(to right, #effbf7, white)
&:nth-child(3)
border-right: 1px solid #effbf7
h4
background-image: linear-gradient(to right, white, #effbf7)
&:nth-child(2)
display: flex
align-items: center
justify-content: center
h4
margin: 0
padding: .8rem 2rem
color: $darkgrey
border-top-left-radius: .5rem
border-top-right-radius: .5rem
font-family: 'Mainfont-Bold'
font-size: 1.1rem
text-transform: uppercase
text-align: center
letter-spacing: .05rem
p
padding: .5rem 1rem
font-size: .9rem
ul
margin-left: 2vw
li
padding: .3rem 0
.loopImg
width: 100%
height: auto
object-fit: cover
object-position: center center
@media screen and (min-width: $breakPointLG)
flex-direction: row
.whatWeDo
position: relative
overflow-x: hidden
min-height: 25vw
margin: 3rem 0
.rightImg, .leftImg
height: 100%
@ -189,13 +283,13 @@ const logoItems = computed(() => {
background-color: rgba(255,255,255,.8)
h2
font-size: clamp(1.2rem, .7rem + 2vw, 2rem)
font-size: clamp(1.5rem, .7rem + 2vw, 2rem)
font-family: 'Comfortaa'
h3
font-family: 'Mainfont-Bold'
font-size: 1.3rem
font-size: clamp(1.2rem, .9rem + 2vw, 1.6rem)
p
font-size: 1.1rem
font-size: clamp(1rem, .5rem + 2vw, 1.2rem)

View File

@ -42,6 +42,7 @@ interface PageSection {
interface FAQ {
question: string
answer: string
locale: string
}
interface Page {
@ -54,6 +55,13 @@ interface Page {
pageSections: PageSection[]
}
interface PageContent {
id: number
section: string
content: any // Strapi Blocks ggf. präzisieren, wenn du eine feste Struktur nutzt
locale: string
}
interface CustomerProject {
id: number
projectTitle: string
@ -111,6 +119,7 @@ interface ContactData {
export const useMainStore = defineStore('main', {
state: () => ({
locale: 'de',
menuOpen: false,
contactBoxOpen: false,
scrollPosition: 0,
@ -119,6 +128,7 @@ export const useMainStore = defineStore('main', {
darkHeroBack: false,
companyinfo: null as CompanyInfo | null,
pages: [] as Page[],
pagecontents: [] as PageContent[],
customers: [] as Customer[],
projects: [] as CustomerProject[],
articles: [] as NewsArticle[],
@ -143,10 +153,10 @@ export const useMainStore = defineStore('main', {
state.customers.find((c) => c.id === id),
getFaqsByPageId: (state) => (pageId: number) =>
state.pages.find((p) => p.id === pageId)?.faqs ?? [],
getFaqsByPageLink: (state) => (link: string) => {
const page = state.pages.find(p => p.pageLink === link);
return page?.faqs ?? [];
},
getFaqsByPageLink: (state) => (link: string, locale = state.locale) => {
const page = state.pages.find(p => p.pageLink === link && p.locale === locale)
return page?.faqs ?? []
},
getProjectByLink: (state) => (link: string) =>
state.projects.find(project => project.link === link),
getArticleBySlug: (state) => (slug: string) =>
@ -169,7 +179,12 @@ export const useMainStore = defineStore('main', {
return state.articles.filter(article =>
article.categories?.some(cat => categoryIds.includes(cat.id))
)
}
},
getPageContentBySection: (state) => (section: string): PageContent | undefined =>
state.pagecontents.find((pc) => pc.section === section),
getAllPageContentsBySection: (state) => (section: string): PageContent[] =>
state.pagecontents.filter((pc) => pc.section === section),
},
actions: {
@ -194,7 +209,14 @@ export const useMainStore = defineStore('main', {
setDarkHeroBack(value: boolean) {
this.darkHeroBack = value
},
syncLocaleWithI18n() {
const { locale } = useI18n()
this.locale = locale.value
watch(locale, (newLocale) => {
this.locale = newLocale
})
},
// SEND CONTACT REQUEST TO STRAPI
sendContactRequestToCMS: async (contactData: ContactData): Promise<void> => {
@ -235,13 +257,32 @@ export const useMainStore = defineStore('main', {
const { public: cfg } = useRuntimeConfig()
try {
const [companyRes, pagesRes, customersRes, projectsRes, articlesRes, categoriesRes] = await Promise.all([
const [companyRes, pagesRes, customersRes, projectsRes, articlesRes, categoriesRes, pageContentsRes] = await Promise.all([
$fetch(`${cfg.cmsBaseUrl}/api/companyinfo?populate=*`, {
headers: { Authorization: `Bearer ${cfg.cmsToken}` },
}),
$fetch(`${cfg.cmsBaseUrl}/api/pages?populate=*`, {
/* $fetch(`${cfg.cmsBaseUrl}/api/pages?populate=*`, {
headers: { Authorization: `Bearer ${cfg.cmsToken}` },
}), */
$fetch(`${cfg.cmsBaseUrl}/api/pages`, {
headers: { Authorization: `Bearer ${cfg.cmsToken}` },
params: {
locale: 'all',
populate: [
'header_image',
'SEO',
'SEO.seoImage',
'faqs',
'faqs.localizations',
'pageSections',
'pageSections.sectionImage',
'pageContents',
'pageContents.content',
].join(','),
}
}),
$fetch(`${cfg.cmsBaseUrl}/api/customers?populate=*`, {
headers: { Authorization: `Bearer ${cfg.cmsToken}` },
}),
@ -251,9 +292,31 @@ export const useMainStore = defineStore('main', {
$fetch(`${cfg.cmsBaseUrl}/api/newsarticels?populate=image,SEO,categories,author&locale=all&sort=createdAt:desc`, {
headers: { Authorization: `Bearer ${cfg.cmsToken}` },
}),
$fetch(`${cfg.cmsBaseUrl}/api/magazin-cats?populate=*&locale=all`, {
/* $fetch(`${cfg.cmsBaseUrl}/api/magazin-cats?populate=*&locale=all`, {
headers: { Authorization: `Bearer ${cfg.cmsToken}` },
}),
}),
$fetch(`${cfg.cmsBaseUrl}/api/pagecontents?populate=*&locale=all`, {
headers: { Authorization: `Bearer ${cfg.cmsToken}` },
}), */
$fetch(`${cfg.cmsBaseUrl}/api/magazin-cats`, {
headers: { Authorization: `Bearer ${cfg.cmsToken}` },
params: {
locale: 'all',
populate: '*',
},
}),
$fetch(`${cfg.cmsBaseUrl}/api/pagecontents`, {
headers: { Authorization: `Bearer ${cfg.cmsToken}` },
params: {
locale: 'all',
// Falls `content` ein RichText-Feld ist und evtl. weitere Relationen,
// solltest du die Felder explizit nennen, nicht nur '*'
populate: {
content: '*',
},
'pagination[limit]': 3000,
},
}),
])
this.companyinfo = companyRes.data?.attributes ?? companyRes
@ -262,6 +325,7 @@ export const useMainStore = defineStore('main', {
const a = item.attributes
return {
id: item.id,
locale: a.locale,
pageName: a.pageName,
pageLink: a.pageLink,
header_image: a.header_image?.data
@ -287,6 +351,7 @@ export const useMainStore = defineStore('main', {
faqs: a.faqs?.data?.map((f: any) => ({
question: f.attributes.question,
answer: f.attributes.answer,
locale: f.attributes.locale,
})) ?? [],
pageSections: a.pageSections?.map((s: any) => ({
id: s.id,
@ -301,6 +366,13 @@ export const useMainStore = defineStore('main', {
}
})
this.pagecontents = pageContentsRes.data.map((item: any) => ({
id: item.id,
section: item.attributes.section,
content: item.attributes.content,
locale: item.attributes.locale
}))
this.customers = customersRes.data.map((item: any) => {
const a = item.attributes
return {