new Routing

This commit is contained in:
Sabrina Hennrich 2025-06-03 09:18:45 +02:00
parent e97365b0b6
commit 5b16d988f5
30 changed files with 1213 additions and 341 deletions

10
app.vue
View File

@ -7,8 +7,16 @@
<script setup>
import { onMounted, onBeforeUnmount } from 'vue'
import { useMainStore } from '@/stores/main'
import { useRouter } from 'vue-router'
const mainStore = useMainStore()
const router = useRouter()
router.beforeEach((to, from, next) => {
mainStore.setDarkHeroBack(false)
next()
})
// Scroll- und Resize-Listener in den Lifecycle-Hooks registrieren
onMounted(() => {

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -4,6 +4,7 @@
v-for="(item, i) in items"
:key="i"
class="accordion-item"
:class="{ open: openIndex === i }"
>
<!-- Header --------------------------------------------------->
<button
@ -44,6 +45,18 @@ const toggle = (i: number) =>
.accordion-item
border-bottom: 1px solid $lightgrey
width: 100%
transition: .8s
&.open
background-image: linear-gradient(to right, $lightgrey, white)
padding: 1rem
.accordion-header
background: transparent
font-size: 120%
&:hover
background: transparent
.accordion-header
all: unset
@ -93,6 +106,16 @@ const toggle = (i: number) =>
.accordion-content
padding: 0 1rem 1rem
h3
font-size: 1rem
margin: 1.2rem 0 .6rem 0
p
font-size: 1rem
ul
li
line-height: 140%
margin-bottom: 1rem
// simple height-fade transition
.accordion-enter-from,
.accordion-leave-to

View File

@ -1,7 +1,7 @@
<template>
<transition name="fade">
<button class="back-to-top" v-show="isVisible" @click="scrollToTop" aria-label="Zurück nach oben">
<span class="arrow-up"></span>
<button v-show="isVisible" class="back-to-top" aria-label="Zurück nach oben" @click="scrollToTop">
<span class="arrow-up"/>
</button>
</transition>
</template>

View File

@ -2,7 +2,7 @@
<div class="ctaBox">
<h3>{{ headline }}</h3>
<p>{{ text }}</p>
<button class="pinkBtn mt-1" @click.prevent="toggleContactBubble" role="button">
<button class="pinkBtn mt-1" role="button" @click.prevent="toggleContactBubble">
{{ buttonText }}
</button>
</div>

View File

@ -47,8 +47,8 @@
<div v-if="screenWidth > 768" class="pt-3">
<h3>{{ $t('contactForm.ourOffice') }}</h3>
<p class="address">
{{ companyinfo.company }}<br />
{{ companyinfo.street }} <br />
{{ companyinfo.company }}<br >
{{ companyinfo.street }} <br >
{{ companyinfo.postalcode }} {{ companyinfo.city }}
</p>
<p class="aspProf">{{ $t('contactForm.yourcontactperson') }} <b>Sabrina Hennrich</b></p>
@ -68,21 +68,21 @@
<!-- Rechte Seite -->
<div class="col-md-6">
<div v-if="!formSent">
<form @submit.prevent="submitForm" novalidate>
<form novalidate @submit.prevent="submitForm">
<div class="form-group">
<label for="name">{{ $t('contactForm.name') }}</label>
<input
id="name"
class="form-control"
v-model="form.name"
class="form-control"
type="text"
name="name"
required
autocomplete="name"
@blur="validateName"
:aria-invalid="!!errors.name"
:aria-describedby="errors.name ? 'error-name' : null"
/>
@blur="validateName"
>
<span
v-if="errors.name"
id="error-name"
@ -97,16 +97,16 @@
<label for="email">{{ $t('contactForm.email') }}</label>
<input
id="email"
class="form-control"
v-model="form.email"
class="form-control"
type="email"
name="email"
required
autocomplete="email"
@blur="validateEmail"
:aria-invalid="!!errors.email"
:aria-describedby="errors.email ? 'error-email' : null"
/>
@blur="validateEmail"
>
<span
v-if="errors.email"
id="error-email"
@ -121,15 +121,15 @@
<label for="phone">{{ $t('contactForm.phone') }}</label>
<input
id="phone"
class="form-control"
v-model="form.phone"
class="form-control"
type="tel"
name="phone"
autocomplete="tel"
@blur="validatePhone"
:aria-invalid="!!errors.phone"
:aria-describedby="errors.phone ? 'error-phone' : null"
/>
@blur="validatePhone"
>
<span
v-if="errors.phone"
id="error-phone"
@ -144,19 +144,19 @@
<label for="message">{{ $t('contactForm.message') }}</label>
<textarea
id="message"
class="form-control mt-4"
v-model="form.message"
class="form-control mt-4"
name="message"
rows="4"
required
></textarea>
/>
</div>
<p class="smallText">
<span class="check"></span>
{{ $t('contactForm.privacyInfotextBeforeLink') }}
<NuxtLinkLocale
:to="getRoute('privacy')"
:to="'privacy'"
:aria-label="$t('privacy')"
>
{{ $t('contactForm.privacyInfotextLinkText') }}
@ -191,7 +191,7 @@ import { useMainStore } from '@/stores/main';
import { ref, reactive, computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
const { getRoute } = useI18nPages()
// i18n Setup
const { t } = useI18n();

View File

@ -1,6 +1,6 @@
// components/HeroBox.vue
<template>
<section class="heroBox" :aria-label="ariaLabel">
<section ref="heroBoxRef" class="heroBox" :aria-label="ariaLabel">
<NuxtImg
provider="strapi"
:src="image"
@ -13,6 +13,20 @@
preload
fetchpriority="high"
/>
<!-- Optionales Overlay-Bild -->
<NuxtImg
v-if="overlayImage"
provider="strapi"
:src="overlayImage"
class="overlay-img"
:alt="overlayAltText"
:style="{
...overlayPosition,
width: typeof overlayWidth === 'number' ? overlayWidth + 'px' : overlayWidth,
transform: `translate3d(0, ${parallaxY}px, 0)`
}"
aria-hidden="true"
/>
<div class="container-10 content">
<h1>{{ $t(content.headline1) }}</h1>
<h2>{{ $t(content.headline2) }}</h2>
@ -25,14 +39,78 @@
</template>
<script setup>
defineProps({
// Parallax Overlay-Image
import { ref, onMounted, onUnmounted } from 'vue'
import { useMainStore } from '@/stores/main'
const mainStore = useMainStore()
const props = defineProps({
image: String,
ariaLabel: String,
content: {
type: Object,
required: true
},
overlayImage: {
type: String,
default: null
},
overlayAltText: {
type: String,
default: ''
},
overlayWidth: {
type: [String, Number],
default: '200px'
},
darkBackground: {
type: Boolean,
default: false
},
overlayPosition: {
type: Object,
default: () => ({
bottom: '0px',
right: '0px'
})
}
})
const parallaxY = ref(0)
const heroBoxRef = ref(null)
let targetY = 0
let animationFrame
const updateParallax = () => {
// Ease: Linear interpolation (lerp) zwischen aktuellem Wert und Zielwert
parallaxY.value += (targetY - parallaxY.value) * 0.1
animationFrame = requestAnimationFrame(updateParallax)
}
const onScroll = () => {
targetY = window.scrollY * 0.3 // oder was dir gefällt
}
const updateHeight = () => {
if (heroBoxRef.value) {
mainStore.setHeroBoxHeight(heroBoxRef.value.offsetHeight)
mainStore.setDarkHeroBack(props.darkBackground)
}
}
onMounted(() => {
window.addEventListener('scroll', onScroll, { passive: true })
updateParallax()
// HeroBoxHeight for Logo
updateHeight()
window.addEventListener('resize', updateHeight)
})
onUnmounted(() => {
window.removeEventListener('scroll', onScroll)
cancelAnimationFrame(animationFrame)
window.removeEventListener('resize', updateHeight)
})
</script>
<style lang="sass" scoped>
@ -71,13 +149,22 @@ defineProps({
object-position: center top
z-index: -1
.overlay-img
position: absolute
bottom: 0
right: 0
z-index: 1
height: auto
max-height: 100%
object-fit: contain
.content, h1, h2, h3
position: relative
z-index: 1
z-index: 2
h1, h2, h3
color: white
z-index: 2
z-index: 3
line-height: 1.5
max-width: 70%
@media (max-width: 768px)

View File

@ -1,5 +1,5 @@
<template>
<div class="languageBox" v-if="locales.length >=2" @click="toggleOpen">
<div v-if="locales.length >=2" class="languageBox" @click="toggleOpen">
<div v-if="!open" class="current">{{ currentLanguage }}</div>
<transition name="slide">

View File

@ -1,10 +1,10 @@
<template>
<section class="topSpace" v-if="currentPage">
<section v-if="currentPage" class="topSpace">
<div v-if="currentPage.pageSections[0].sectionText">
<div class="container content" v-html="htmlContent(currentPage.pageSections[0].sectionText)"></div>
<div class="container content" v-html="htmlContent(currentPage.pageSections[0].sectionText)"/>
</div>
</section>
<section class="topSpace" v-else>
<section v-else class="topSpace">
<h1>Seite nicht gefunden</h1>
<p>Die angeforderte Seite existiert nicht.</p>
</section>

View File

@ -2,11 +2,11 @@
<section class="recommendations">
<!-- Vor dem Container: Welle oben -->
<svg class="sectionWave wave-top" :style="`height: ${waveHeight};top:-${waveHeight-2}`" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 20">
<path d="M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z" fill="#EEEBE5"></path>
<path d="M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z" fill="#EEEBE5"/>
</svg>
<div class="container">
<h2>Das sagen andere Designer und Kreative über digimedialoop</h2>
<div class="personBox" v-for="person, index in persons" :key="index" :class="person.active ? 'active' : ''" @click="setActive(index)">
<div v-for="person, index in persons" :key="index" class="personBox" :class="person.active ? 'active' : ''" @click="setActive(index)">
<img :src="person.image" alt="">
<div class="infoBox">
<h3>{{ person.name }}</h3>
@ -17,7 +17,7 @@
<!-- Nach dem Container: Spiegelwelle unten -->
<svg class="sectionWave wave-bottom" :style="`height: ${waveHeight};bottom:-${waveHeight-2}`" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 20">
<path d="M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z" fill="#EEEBE5"></path>
<path d="M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z" fill="#EEEBE5"/>
</svg>
</section>
</template>

180
components/SchemaCoder.vue Normal file
View File

@ -0,0 +1,180 @@
<template>
<div class="schema-typer">
<span class="token-tag">&lt;<span class="token-tag-name">script</span> <span class="token-attr">type</span>=<span class="token-string">"application/ld+json"</span>&gt;</span><br >
<span
v-for="(line, lineIndex) in displayedLines"
:key="lineIndex"
class="code-line"
>
<template v-for="(segment, index) in line" :key="index">
<span :class="segment.class">{{ segment.text }}</span>
</template>
<template v-if="(lineIndex === currentLine || (isTypingDone && lineIndex === displayedLines.length - 1)) && showCursor">
<span class="cursor">|</span>
</template>
<br >
</span>
<span class="token-tag">&lt;/<span class="token-tag-name">script</span>&gt;</span>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const schema = `{
"@context": "https://schema.org",
"@type": "HowTo",
"name": "So machen wir deine Website KI-kompatibel",
"description": "Wir optimieren deine Website gezielt für künstliche Intelligenz. Strukturiert, effizient und zukunftssicher.",
"step1": "Wir integrieren strukturierte Daten nach schema.org, damit KI-Systeme Inhalte korrekt interpretieren können.",
"step2": "Wir überarbeiten den Quellcode mit semantischem HTML und klaren Überschriften zur besseren maschinellen Lesbarkeit.",
"step3": "Wir stellen relevante Inhalte zusätzlich in maschinenfreundlichen Formaten wie JSON-LD oder über APIs bereit.",
"step4": "Wir sorgen für barrierefreie Gestaltung und klare Sprache, so verstehen auch KI-Sprachmodelle deine Inhalte besser.",
"step5": "Wir ergänzen deine Seiten mit Metadaten wie OpenGraph, Twitter Cards und Robots-Tags für optimalen Kontext.",
"estimatedTime": "ca. 45 Minuten Analyse + Umsetzung je nach Umfang",
"image": "/images/ki-optimierung.jpg"
}`
function parseLine(line) {
const segments = []
const regex = /("[^"]+":)|("[^"]+")|(\d+)|(true|false|null)|(\s+|[{}[\],:])/g
let match
let lastIndex = 0
while ((match = regex.exec(line)) !== null) {
const start = match.index
if (start > lastIndex) {
segments.push({ text: line.slice(lastIndex, start), class: 'token-text' })
}
const [matched, key, str, num, bool, symbol] = match
let cls = 'token-text'
if (key) cls = 'token-key'
else if (str) cls = 'token-string'
else if (num) cls = 'token-number'
else if (bool) cls = 'token-boolean'
else if (symbol) cls = 'token-text'
segments.push({ text: matched, class: cls })
lastIndex = start + matched.length
}
if (lastIndex < line.length) {
segments.push({ text: line.slice(lastIndex), class: 'token-text' })
}
return segments
}
const lines = schema.split('\n').map(parseLine)
const displayedLines = ref([])
const showCursor = ref(true)
const currentLine = ref(0)
const isTypingDone = ref(false)
const startTyping = () => {
let lineIndex = 0
let charIndex = 0
displayedLines.value.push([])
const typeChar = () => {
if (lineIndex >= lines.length) {
isTypingDone.value = true
return
}
const segment = lines[lineIndex][charIndex]
if (segment) {
displayedLines.value[lineIndex].push(segment)
charIndex++
} else {
lineIndex++
charIndex = 0
currentLine.value = lineIndex
if (lineIndex < lines.length) displayedLines.value.push([])
}
setTimeout(typeChar, Math.random() * 80 + 40)
}
typeChar()
setInterval(() => (showCursor.value = !showCursor.value), 500)
}
onMounted(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
observer.disconnect()
startTyping()
}
}, { threshold: 0.3 })
const el = document.querySelector('.schema-typer')
if (el) observer.observe(el)
})
</script>
<style lang="sass" scoped>
.schema-typer
background-color: #1e1e1e
padding: 1.5rem
margin: 2rem
border-radius: 0.5rem
font-family: monospace
font-size: 0.7rem
color: #ffffff
overflow-x: auto
overflow-y: auto
line-height: 1.4
height: auto
min-height: 450px
width: 400px
transform: rotate(6deg)
box-shadow: -3px 3px 6px 4px grey
span
margin: 0
padding: 0
.code-line
display: block
line-height: 1.2
.token-tag
color: #569cd6
.token-tag-name
color: #4ec9b0
.token-attr
color: #c586c0
.token-key
color: #9cdcfe
margin-left: 1rem
.token-string
color: #ce9178
.token-number
color: #b5cea8
.token-boolean
color: #dcdcaa
.token-text
color: #d4d4d4
margin-left: .2rem
.cursor
display: inline-block
width: 1ch
background-color: white
animation: blink 1s steps(2, start) infinite
@keyframes blink
to
visibility: hidden
</style>

View File

@ -1,8 +1,9 @@
<template>
<div class="sideBarNaviSlider"
<div
class="sideBarNaviSlider"
:class="{ 'slide-in': showSideBar }"
@click="navigate">
<slot></slot>
<slot/>
</div>
</template>

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
import { useI18n } from 'vue-i18n'
/* import { useI18n } from 'vue-i18n'
import { i18nPages } from '@/i18n/i18n-pages'
export function useI18nPages () {
@ -31,3 +31,4 @@ export function useI18nPages () {
getArticleLink
}
}
*/

View File

@ -7,7 +7,7 @@ export const i18nPages = {
es: '/inicio',
tr: '/anasayfa'
},
webagency: {
webagency: {
de: '/webagentur',
en: '/webagency',
fr: '/agence-web',
@ -15,7 +15,7 @@ export const i18nPages = {
es: '/agencia-web',
tr: '/web-ajansi'
},
services: {
services: {
de: '/leistungen',
en: '/services',
fr: '/services',
@ -23,23 +23,39 @@ export const i18nPages = {
es: '/servicios',
tr: '/hizmetler'
},
'services/accessibility': {
de: '/leistungen/barrierefreiheit',
en: '/services/accessibility',
fr: '/services/accessibilite',
it: '/servizi/accessibilita',
es: '/servicios/accesibilidad',
tr: '/hizmetler/erisilebilirlik'
'services-accessibility': {
de: '/leistungen/barrierefreie-webseiten',
en: '/services/accessible-websites',
fr: '/services/sites-accessibles',
it: '/servizi/siti-accessibili',
es: '/servicios/sitios-accesibles',
tr: '/hizmetler/erisilebilir-websiteler'
},
'services/seo': {
de: '/leistungen/barrierefreiheit',
en: '/services/accessibility',
fr: '/services/accessibilite',
it: '/servizi/accessibilita',
es: '/servicios/accesibilidad',
tr: '/hizmetler/erisilebilirlik'
'services-seo': {
de: '/leistungen/suchmaschinenoptimierung',
en: '/services/search-engine-optimization',
fr: '/services/optimisation-pour-moteurs-de-recherche',
it: '/servizi/ottimizzazione-per-motori-di-ricerca',
es: '/servicios/optimizacion-en-buscadores',
tr: '/hizmetler/arama-motoru-optimizasyonu'
},
references: {
'services-cms': {
de: '/leistungen/headless-cms',
en: '/services/headless-cms',
fr: '/services/headless-cms',
it: '/servizi/headless-cms',
es: '/servicios/headless-cms',
tr: '/hizmetler/headless-cms'
},
'services-ai': {
de: '/leistungen/ki-kompatible-webseiten',
en: '/services/ai-compatible-websites',
fr: '/services/sites-compatibles-ia',
it: '/servizi/siti-compatibili-ai',
es: '/servicios/sitios-compatibles-ai',
tr: '/hizmetler/ai-uyumlu-websiteler'
},
references: {
de: '/referenzen',
en: '/references',
fr: '/références',
@ -47,7 +63,7 @@ export const i18nPages = {
es: '/referencias',
tr: '/referanslar'
},
imprint: {
imprint: {
de: '/impressum',
en: '/imprint',
fr: '/mentions-legales',
@ -55,17 +71,15 @@ export const i18nPages = {
es: '/aviso-legal',
tr: '/künye'
},
'projekt___link': {
paths: {
de: '/projekt/:link',
en: '/projekt/:link',
fr: '/projekt/:link',
it: '/projekt/:link',
es: '/projekt/:link',
tr: '/projekt/:link'
}
'project-link': {
de: '/projekt/[link]',
en: '/project/[link]',
fr: '/projet/[link]',
it: '/progetto/[link]',
es: '/proyecto/[link]',
tr: '/proje/[link]'
},
designer: {
designer: {
de: '/designer',
en: '/designers',
fr: '/createurs',
@ -73,7 +87,7 @@ export const i18nPages = {
es: '/disenadores',
tr: '/tasarimcilar'
},
privacy: {
privacy: {
de: '/datenschutz',
en: '/privacy',
fr: '/confidentialite',
@ -81,7 +95,7 @@ export const i18nPages = {
es: '/privacidad',
tr: '/gizlilik'
},
terms: {
terms: {
de: '/agb',
en: '/terms',
fr: '/conditions',
@ -89,7 +103,7 @@ export const i18nPages = {
es: '/condiciones',
tr: '/kosullar'
},
magazin: {
magazin: {
de: '/wissenswertes',
en: '/magazine',
fr: '/magazine',
@ -97,14 +111,12 @@ export const i18nPages = {
es: '/revista',
tr: '/dergi'
},
'artikel___link': {
paths: {
de: '/artikel/:link',
en: '/artikel/:link',
fr: '/artikel/:link',
it: '/artikel/:link',
es: '/artikel/:link',
tr: '/artikel/:link'
}
},
'article-link': {
de: '/artikel/[link]',
en: '/article/[link]',
fr: '/article/[link]',
it: '/articolo/[link]',
es: '/articulo/[link]',
tr: '/makale/[link]'
},
}

View File

@ -4,6 +4,10 @@
"upperBavaria": "Oberbayern",
"webagency": "Webagentur",
"services": "Leistungen",
"menuAi": "KI-kompatible Webseiten",
"menuCms": "Headless Content-Management-System (CMS)",
"menuAccessibility": "Barrierefeies Webdesign",
"menuSEO": "Suchmaschinen-Optimierung (SEO)",
"contact": "Kontakt",
"references": "Referenzen",
"referenceoverview": "Referenzübersicht",
@ -51,7 +55,7 @@
}
},
"faqBox": {
"questions": "Fragen?",
"questions": "Weitere Fragen?",
"faqsDefault": "Häufig gestellte Fragen (FAQs)",
"btnDefault": "Sprechen Sie uns gerne an!"
},
@ -166,14 +170,71 @@
"hero": {
"headline1": "Barrierefeies Webdesign",
"headline2": "Zugänglichkeit für Mensch und Maschine nach den Standards WCAG und BITV",
"headline3": "Verständlich, nutzbar und robust, für eine inklusive und zukunftssichere Webseite"
"headline3": "Verständlich, nutzbar und robust, für eine inklusive und zukunftssichere Webseite",
"ariaLabel": "Barrierefreies Webdesign"
}
},
"seo": {
"hero": {
"headline1": "Suchmaschinen-Optimierung",
"headline2": "mit strukturierten Daten, Meta-Tags und semantischem HTML",
"headline3": "Mehr Sichbarkeit durch technische Exzellenz!"
"headline3": "Mehr Sichbarkeit durch technische Exzellenz!",
"ariaLabel": "Suchmaschinen-Optimierung"
}
},
"cms": {
"hero": {
"headline1": "Flexibles Headless Content-Management-System",
"headline2": "mit strukturierten Daten, Meta-Tags und semantischem HTML",
"headline3": "Mehr Sichbarkeit durch technische Exzellenz",
"ariaLabel": "Content-Management-System"
}
},
"ai": {
"hero": {
"ariaLabel": "Hero-Bereich: KI-kompatible Webseiten",
"headline1": "KI-kompatible Webseiten",
"headline2": "Strukturierte, maschinenlesbare Inhalte für eine neue Generation digitaler Assistenten.",
"headline3": "Bereit für Chatbots, Sprachsuche und generative KI-Systeme wie ChatGPT & Co.?"
},
"whyAI": {
"headline": "Warum KI-Kompatibilität im Web immer wichtiger wird",
"text1": "KI-Assistenten greifen heutzutage nicht nur auf strukturierte Inhalte im Web zu, um präzise Antworten zu liefern, sondern auch, um direkt mit Webseiten zu interagieren. Sie nehmen Buchungen vor, lösen Bestellungen aus oder vereinbaren Termine.",
"text2": "Wer sicherstellen möchte, dass die eigene Webseite dabei berücksichtigt wird, muss eine technische Grundlage schaffen, um Sichtbarkeit zu sein und Interaktion zu ermöglichen."
},
"targetGroup": {
"headline": "Für wen lohnt sich eine KI-kompatible Webseite?",
"point1_headline": "Content-Ersteller & Medienunternehmen",
"point1_description": "Maximieren Sie Ihre Reichweite, indem KI Ihre Inhalte intelligent versteht und verteilt.",
"point2_headline": "Onlineshop-Betreiber",
"point2_description": "Ermöglichen Sie Kunden, Ihre Produkte per Sprachbefehl zu finden und direkt zu kaufen.",
"point3_headline": "Dienstleister & Freiberufler",
"point3_description": "Werden Sie per Sprachsuche schnell gefunden und bieten Sie unkomplizierte Terminbuchungen an.",
"point4_headline": "Unternehmen mit digitaler Wachstumsstrategie",
"point4_description": "Bleiben Sie auf Suchmaschinen und bei KI-Assistenten an der Spitze der Sichtbarkeit."
},
"howItWorks": {
"headline": "Was macht eine Webseite KI-kompatibel?",
"pretext": "Damit künstliche Intelligenzen Webseiten richtig verstehen und Inhalte optimal nutzen können, braucht es mehr als nur gute Texte und schönes Design. Es braucht eine klar strukturierte, technisch saubere Basis.",
"point1": "Strukturierte Daten via Schema.org & JSON-LD",
"point2": "Semantisches HTML für kontextuelles Verständnis",
"point3": "Logische Inhalts-Hierarchien",
"point4": "Optimierung für Sprachsuche & Featured Snippets",
"point5": "Content-Strategien für Large Language Models (LLMs)",
"point6": "Maschinenlesbare Metadaten & Open Graph Tags",
"posttext": "All diese maschinenlesbaren Strukturen integrieren wir direkt im Code. Ohne Plugins, ohne Umwege. So bleiben Performance und Kontrolle erhalten und die Basis der Website ist von Grund auf KI-optimiert."
},
"howWeDo": {
"headline": "Wie wir KI-Kompatibilität mit Nuxt + Strapi umsetzen",
"text": "Unser Headless-Ansatz mit Nuxt 3 und Strapi ermöglicht volle Kontrolle über Metadaten, Struktur und Performance.",
"point1": "Integration strukturierter Daten direkt im CMS",
"point2": "SEO-optimiertes Rendering durch SSR & SSG",
"point3": "Zentrale Steuerung aller Meta- und JSON-LD-Daten",
"point4": "Flexible Inhaltsmodelle für semantischen Aufbau",
"point5": "Anpassung für Chatbots, Sprachassistenten und KI-Crawler"
},
"faq": {
"headline": "Weitere Fragen zum Thema künstliche Intelligenz im Webdesign"
}
}
},

View File

@ -17,11 +17,16 @@
</template>
<script setup>
import { useMainStore } from '@/stores/main'
const mainStore = useMainStore()
import { usePageMeta } from '~/composables/usePageMeta'
usePageMeta()
onBeforeMount(() => {
mainStore.setDarkHeroBack(false) // Standardmäßig false
})
</script>
<style lang="sass">
@ -168,19 +173,22 @@ main
.check
list-style: none
padding: .2rem 1rem
margin: 0
padding: 0 0 1rem 1rem
li
position: relative
padding-left: 1.5em
margin: .8em 0
position: relative
padding-left: 2rem
font-size: 1rem
margin-bottom: 1rem
&::before
content: "\2713"
position: absolute
left: 0
color: $primaryColor
&::before
content: "✔"
color: $primaryColor
font-weight: bold
position: absolute
font-size: 1.4rem
left: 0
top: 0
section
margin-bottom: 5vh

View File

@ -1,5 +1,5 @@
<template>
<div class="article" v-if="article">
<div v-if="article" class="article">
<SideBarNaviSlider link="/wissenswertes">
{{ $t('pages.article.artikelUebersicht') }}
</SideBarNaviSlider>
@ -30,12 +30,12 @@
</section>
<section class="articleBox container">
<div v-html="htmlContent(article.content)" class="content"></div>
<div class="content" v-html="htmlContent(article.content)"/>
<button
@click.prevent="toggleContactBubble"
class="pinkBtn"
role="button"
aria-label="Kontakt aufnehmen"
@click.prevent="toggleContactBubble"
>
{{ $t('pages.article.kontaktieren') }}
</button>
@ -49,6 +49,9 @@
<script setup>
definePageMeta({
name: 'article-link'
})
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useMainStore } from '@/stores/main'

View File

@ -37,18 +37,18 @@
</ul>
<p><b>Du bleibst der kreative Kopf!</b><br>
Ich sorge dafür, dass dein Design genauso im Web erscheint, wie du es geplant hast mit Top-Performance & perfekter technischer Umsetzung.</p>
<button class="pinkBtn" @click.prevent="toggleContactBubble" role="button">Lass uns kennenlernen</button>
<button class="pinkBtn" role="button" @click.prevent="toggleContactBubble">Lass uns kennenlernen</button>
</div>
</section>
<section class="deviceCheck" v-if="false">
<section v-if="false" class="deviceCheck">
<div class="container">
<div class="row">
<div class="col-md-6">
<p class="supheadlinePink">Responsive Check</p>
<h2>Wie sieht deine Webseite auf den verschiedenen Bildschirmbreiten aus?</h2>
<h3>Probiers einfach aus!</h3>
<input type="text" placeholder="https://www.deinewebseite.de" v-model="weblink" @keyup.enter="updateIframe">
<input v-model="weblink" type="text" placeholder="https://www.deinewebseite.de" @keyup.enter="updateIframe">
</div>
<div class="col-md-6">
<svg v-if="desk" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 450">
@ -81,7 +81,7 @@
<rect id="deskScreenBlank" x="46" y="47.5" width="409.5" height="221.5" transform="matrix(1,0,0,1,0,0)" fill="rgb(0,0,0)"/>
<g>
<foreignObject v-if="isValidUrl(weblink)" id="deskScreen" x="46" y="47.5" width="409.5" height="221.5">
<iframe ref="iframeRef" :src="weblink" frameborder="0"></iframe>
<iframe ref="iframeRef" :src="weblink" frameborder="0"/>
</foreignObject>
</g>
</g>
@ -119,19 +119,20 @@
</div>
</section>
<Recommendations />
<section class="contrastCalcLink" v-if="false">
<section v-if="false" class="contrastCalcLink">
<div class="container">
<img src="https://strapi.digimedialoop.de/uploads/wcag_kontrastrechner_77abf9d9be.png" alt="kontrast check" class="imgRight">
<p class="supheadlinePink">Barrierefreies Webdesign</p>
<h2>Hat Dein Design den richtigen Kontrast?</h2>
<p>Mit dem praktischen Kontrastchecker kannst Du herausfinden, ob die Schrift und der Hintergrund in Deinem Design den WCAG_Richtlinien entsprechen. So stellst Du sicher, dass Dein Design für alle gut lesbar ist.</p>
<button class="mintBtn" @click.prevent="navigateTo('/toolbox/kontrastchecker')"
role="button"
aria-label="Zum Kontrastchecker">Jetzt kostenlos Konstrast prüfen</button>
<button
class="mintBtn" role="button"
aria-label="Zum Kontrastchecker"
@click.prevent="navigateTo('/toolbox/kontrastchecker')">Jetzt kostenlos Konstrast prüfen</button>
</div>
</section>
<FAQArea
pageLink="/webentwicklung-fuer-designer-und-mediengestalter"
page-link="/webentwicklung-fuer-designer-und-mediengestalter"
headline="Wichtige Antworten zur Zusammenarbeit im Überblick für Dich"
button="Dann lass uns quatschen!"
/>
@ -139,11 +140,11 @@
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick } from "vue";
import { useMainStore } from '@/stores/main';
definePageMeta({
name: 'designer'
})
import { ref, onMounted, onUnmounted, nextTick } from "vue";
import { useMainStore } from '@/stores/main';
// Lade diese Komponente synchron wegen SEO

View File

@ -8,6 +8,7 @@
headline2: 'pages.services.hero.headline2',
headline3: 'pages.services.hero.headline3'
}"
:dark-background="true"
/>
<section class="explainBox" :aria-label="$t('pages.services.explain.ariaLabel')">
@ -32,7 +33,9 @@
<li>{{ $t('pages.services.explain.bullet4') }}</li>
<li>{{ $t('pages.services.explain.bullet5') }}</li>
</ul>
<button @click="navigateTo(localePath('accessibility'))">Informationen zu barrierefreien Webseiten</button>
</div>
</section>
@ -88,6 +91,10 @@ class="pinkBtn mt-3"
import { useMainStore } from '@/stores/main';
import { storeToRefs } from 'pinia';
import { useRouter } from 'vue-router';
import { useRuntimeConfig, navigateTo } from '#app'
import { useLocalePath } from '#i18n'
const localePath = useLocalePath()
const mainStore = useMainStore();
const { customers } = storeToRefs(mainStore);

View File

@ -16,7 +16,7 @@
class="article"
>
<NuxtLink :to="getArticleLink(article.slug)" class="article-link">
<NuxtLinkLocale :to="localePath({ name: 'article-link', params: { link: article.slug } })" class="article-link">
<div class="image-wrapper">
<NuxtImg
v-if="article.image?.url"
@ -31,7 +31,7 @@
<button class="mintBtn">{{ $t('pages.magazin.readmore') }}</button>
</div>
</div>
</NuxtLink>
</NuxtLinkLocale>
</div>
@ -42,14 +42,14 @@
<script setup lang="ts">
import { computed, watch } from 'vue'
import { watch } from 'vue'
import { useMainStore } from '@/stores/main'
import { storeToRefs } from 'pinia'
import { useLocalePath } from '#i18n'
const localePath = useLocalePath()
const mainStore = useMainStore()
const { articles, cmsUrl } = storeToRefs(mainStore)
const { getArticleLink } = useI18nPages()
const { articles } = storeToRefs(mainStore)
const truncateText = (text: string, length = 200) =>
text?.length > length ? text.substring(0, length) + '…' : text
@ -58,6 +58,7 @@ const currentDomain = typeof window !== 'undefined'
? window.location.origin
: 'https://www.digimedialoop.de'
// SEO: JSON-LD für Artikelübersicht
watch(articles, (newVal) => {
if (newVal?.length) {

View File

@ -46,8 +46,8 @@
:src="img.url"
:alt="img.alternativeText"
provider="strapi"
@click="setCurrentImage(img)"
:class="{ active: currentImage?.url === img.url }"
@click="setCurrentImage(img)"
/>
</div>
</div>
@ -55,7 +55,7 @@
<div class="col-lg-8 pt-4">
<span v-html="htmlContent(project.projectDescription)"></span>
<span v-html="htmlContent(project.projectDescription)"/>
<h4>{{ t('project.detail.technologies', 'Verwendete Technologien') }}</h4>
@ -68,7 +68,7 @@
{{ tech.titel }}
</span>
</div>
<div class="row" v-if="project.webpage">
<div v-if="project.webpage" class="row">
<div class="col-12 text-end">
<a
class="webPageBtn"
@ -93,15 +93,20 @@
</template>
<script setup lang="ts">
definePageMeta({
name: 'project-link'
})
import { ref, computed, watch } from "vue"
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const localePath = useLocalePath()
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
import { useMainStore } from '@/stores/main'
import { useHtmlConverter } from '@/composables/useHTMLConverter'
const { t } = useI18n()
const localePath = useLocalePath()
const route = useRoute()
const mainStore = useMainStore()
const project = computed(() => mainStore.getProjectByLink(route.params.link))
const customer = computed(() => {
@ -121,8 +126,6 @@ watch(project, (newProject) => {
currentImage.value = null
}
})
import { useHtmlConverter } from '@/composables/useHTMLConverter'
const { convertToHTML } = useHtmlConverter()
const htmlContent = (data: string) => {
return convertToHTML(data)

View File

@ -3,18 +3,18 @@
<section class="topSpace">
<div class="container">
<h1>{{ $t('pages.references.hero.h1') }}</h1>
<p v-html="$t('pages.references.hero.p')"></p>
<p v-html="$t('pages.references.hero.p')"/>
</div>
</section>
<section>
<div class="referenceBox" v-if="projects && projects.length">
<div v-if="projects && projects.length" class="referenceBox">
<slot>
<NuxtLink
class="reference"
<NuxtLinkLocale
v-for="project in projects"
:key="project.id"
:to="getProjectLink(project.link)"
class="reference"
:to="localePath({ name: 'project-link', params: { link: project.link } })"
>
<NuxtImg
provider="strapi"
@ -38,7 +38,7 @@
/>
</div>
</div>
</NuxtLink>
</NuxtLinkLocale>
</slot>
</div>
</section>
@ -46,7 +46,7 @@
<CallToActionBox
:headline="$t('pages.references.ctaBox.headline')"
:text="$t('pages.references.ctaBox.text')"
:buttonText="$t('pages.references.ctaBox.button')"
:button-text="$t('pages.references.ctaBox.button')"
/>
</div>
@ -55,6 +55,8 @@
<script setup>
import { useMainStore } from '@/stores/main'
import { storeToRefs } from 'pinia'
import { useLocalePath } from '#i18n'
const localePath = useLocalePath()
const runtimeConfig = useRuntimeConfig();
const cmsUrl = runtimeConfig.public.cmsBaseUrl
@ -62,8 +64,6 @@ const cmsUrl = runtimeConfig.public.cmsBaseUrl
const mainStore = useMainStore()
const { projects } = storeToRefs(mainStore)
import { useI18nPages } from '@/composables/useI18nPages'
const { getProjectLink } = useI18nPages()
function getCustomerLogo(customerId) {
if (!customerId) return ''
@ -95,7 +95,7 @@ const jsonLdProjects = computed(() => {
"position": index + 1,
"item": {
"@type": "CreativeWork", // alternativ "WebPage", wenn es sich um Projektseiten handelt
"@id": origin + (project.link ? getProjectLink(project.link) : ''),
"@id": origin + (project.link ? project.link : ''),
"name": project.projectTitle || 'Projekt',
"image": cmsUrl + (project.projectImages?.[0]?.url || ''),
"description": project.projectDescription?.[0]?.children?.[0]?.text || ''

View File

@ -8,11 +8,89 @@
headline2: 'pages.services.accessibility.hero.headline2',
headline3: 'pages.services.accessibility.hero.headline3'
}"
overlay-image="/uploads/DML_Access_Key_3ccf07fbb4.webp"
overlay-alt-text="barrierefreiheit symbol"
:overlay-width="'30%'"
:overlay-position="{ bottom: '5%', right: '1vw' }"
:dark-background="true"
/>
<section class="targetGroup">
<div class="container-10">
<NuxtImg
provider="strapi"
src="/uploads/rollstuhl_a67c73b6a9.webp"
alt="accessibility"
/>
<h2>Für wen ist Barrierefreiheit wichtig?</h2>
<p>Barrierefreiheit macht Ihre Webseite nicht nur für alle Menschen, insbesondere für Menschen mit Einschränkungen zugänglich, sondern verbessert auch die Auffindbarkeit bei Suchmaschinen und die Nutzbarkeit durch moderne KI-Systeme.</p>
<h3>Auf was kommt es bei Barrierefreiheit im Web an?</h3>
<ul class="check">
<li><b>Tastaturbedienbarkeit:</b> Alle Funktionen sind ohne Maus erreichbar.</li>
<li><b>Kontraststarke Gestaltung:</b> Gute Lesbarkeit für alle Sehfähigkeiten.</li>
<li><b>Screenreader-Kompatibilität:</b> Inhalte werden klar und verständlich vorgelesen.</li>
<li><b>Logische Struktur:</b> Überschriften und Inhalte folgen einer nachvollziehbaren Reihenfolge.</li>
<li><b>Barrierefreie Medien:</b> Texte als Alternativen, keine automatisch startenden Videos ohne Kontrolle.</li>
</ul>
<h3>Möchten Sie wissen, wie barrierefrei Ihre aktuelle Webseite ist?</h3>
<button
class="pinkBtn" role="button"
aria-label="Barrierefreiheitscheck"
@click.prevent="toggleContactBubble">Kostenlosen Barrierefreiheits-Check anfordern!</button>
</div>
</section>
<section class="legalBasis">
<svg class="sectionWave wave-top" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 20">
<path d="M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z" fill="#FFF"/>
</svg>
<div class="container-10">
<h2>Rechtliche Grundlagen für Barrierefreiheit in Deutschland und der EU</h2>
<p>Barrierefreiheit im Web ist nicht nur eine gute Praxis, sondern in Deutschland und der Europäischen Union auch gesetzlich vorgeschrieben. Diese Vorgaben sollen sicherstellen, dass digitale Angebote für alle Menschen uneingeschränkt nutzbar sind.</p>
<h3>Wichtige Gesetze und Verordnungen:</h3>
<ul class="check">
<li><b>Behindertengleichstellungsgesetz (BGG):</b> Regelt die Barrierefreiheit öffentlicher Einrichtungen und deren digitale Angebote in Deutschland.</li>
<li><b>Barrierefreie-Informationstechnik-Verordnung (BITV 2.0):</b> Konkretisiert die Anforderungen des BGG für Webseiten und mobile Anwendungen öffentlicher Stellen.</li>
<li><b>EU-Richtlinie 2016/2102:</b> Verpflichtet öffentliche Stellen der Mitgliedstaaten, ihre Websites und mobilen Apps barrierefrei zu gestalten.</li>
<li><b>European Accessibility Act (EAA):</b> Richtlinie für barrierefreie Produkte und Dienstleistungen, inklusive digitaler Angebote.</li>
<li><b>WCAG 2.1 (Web Content Accessibility Guidelines):</b> International anerkannte technische Standards, die als Basis für gesetzliche Vorgaben dienen.</li>
</ul>
<p>Die Einhaltung dieser Regelungen schützt Sie vor Abmahnungen und verbessert Ihre Reichweite durch bessere Zugänglichkeit für alle Nutzer.</p>
</div>
<svg class="sectionWave wave-bottom" style="transform: scale(1,-1)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 20">
<path d="M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z" fill="#FFF"/>
</svg>
</section>
<section class="implementation">
<div class="container-10">
<h2>Wie wir Barrierefreiheit für Sie umsetzen</h2>
<p>
Mit Nuxt im Front-End und Strapi als flexiblem Headless CMS im Back-End, realisieren wir barrierefreie Webseiten, die sowohl technisch sauber als auch leicht wartbar sind.
Durch die Trennung von Front-End und Back-End können Inhalte effizient gepflegt und barrierefreie Standards systematisch umgesetzt werden.
</p>
<h3>Vorteile gegenüber klassischen CMS wie WordPress oder Contao</h3>
<ul class="check">
<li><b>Modulare Architektur:</b> Flexible Komponenten und wiederverwendbare Module erleichtern die Umsetzung von Barrierefreiheitsstandards.</li>
<li><b>Performance:</b> Nuxt generiert schnelle, serverseitig gerenderte Seiten, die für Nutzer und Suchmaschinen gleichermaßen optimal sind.</li>
<li><b>SEO & KI-Freundlichkeit:</b> Durch gezielte Meta-Tags und strukturierte Daten unterstützen wir bessere Auffindbarkeit und Kompatibilität mit KI-Systemen.</li>
<li><b>Content-Management:</b> Strapi ermöglicht barrierefreie Inhalte mit benutzerfreundlichen Editoren und unterstützt mehrsprachige Seiten einfach.</li>
<li><b>Zukunftssicherheit:</b> Moderne Technologien garantieren nachhaltige Wartbarkeit und Erweiterbarkeit der Webseite.</li>
</ul>
<button v-if="false" class="mintBtn">Mehr zum Thema Headless CMS</button>
</div>
</section>
<FAQArea page-link="/leistungen/barrierefreie-webseiten" headline="Weitere Fragen zum Thema Barrierefreiheit im Web" />
</div>
</template>
<script setup>
import { useMainStore } from '@/stores/main';
definePageMeta({
alias: [
'/leistungen/barrierefreie-webseiten', // Deutsch
@ -21,11 +99,27 @@ definePageMeta({
'/servizi/accessibilita', // Italienisch
'/servicios/accesibilidad', // Spanisch
'/hizmetler/erisilebilirlik' // Türkisch
]
],
name: 'services-accessibility'
})
const mainStore = useMainStore();
const toggleContactBubble = () => mainStore.toggleContactBubble();
</script>
<style lang="sass">
.accessiblityPage
.legalBasis
background: linear-gradient(90deg, #39324A 0%, #403871 100%);
color: white
padding: 12vh 0
margin: 8vh 0
.targetGroup
img
float: left
width: 30%
max-width: 400px
margin: 1rem 2rem 2rem 0
.implementation
margin: 8vh 0
padding: 1vh 0 3vh 0
</style>

173
pages/services/ai/index.vue Normal file
View File

@ -0,0 +1,173 @@
<template>
<div class="aiPage">
<HeroBox
image="/uploads/DML_Service_Header_AI_639dd0d7b1.webp"
:aria-label="$t('pages.services.ai.hero.ariaLabel')"
:content="{
headline1: 'pages.services.ai.hero.headline1',
headline2: 'pages.services.ai.hero.headline2',
headline3: 'pages.services.ai.hero.headline3'
}"
overlay-image="/uploads/kirby_fly_3de66b2839.webp"
overlay-alt-text="ki roboter fliegt"
:overlay-width="'32%'"
:overlay-position="{ top: '28%', right: '3vw' }"
:dark-background="true"
/>
<section class="whyAI">
<div class="container-10">
<NuxtImg
provider="strapi"
src="/uploads/AI_robot_67b96e6a17.webp"
alt="roboter sitzt auf mouse"
aria-hidden="true"
/>
<h2>{{ $t('pages.services.ai.whyAI.headline') }}</h2>
<p>{{ $t('pages.services.ai.whyAI.text1') }}</p>
<p>{{ $t('pages.services.ai.whyAI.text2') }}</p>
</div>
</section>
<section class="targetGroup">
<svg class="sectionWave wave-top" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 20">
<path d="M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z" fill="#FFF"/>
</svg>
<div class="container-10">
<h2>{{ $t('pages.services.ai.targetGroup.headline') }}</h2>
<ul class="check">
<li>
<strong>{{ $t('pages.services.ai.targetGroup.point1_headline') }}</strong><br>
{{ $t('pages.services.ai.targetGroup.point1_description') }}
</li>
<li>
<strong>{{ $t('pages.services.ai.targetGroup.point2_headline') }}</strong><br>
{{ $t('pages.services.ai.targetGroup.point2_description') }}
</li>
<li>
<strong>{{ $t('pages.services.ai.targetGroup.point3_headline') }}</strong><br>
{{ $t('pages.services.ai.targetGroup.point3_description') }}
</li>
<li>
<strong>{{ $t('pages.services.ai.targetGroup.point4_headline') }}</strong><br>
{{ $t('pages.services.ai.targetGroup.point4_description') }}
</li>
</ul>
</div>
<svg class="sectionWave wave-bottom" style="transform: scale(1,-1)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 20">
<path d="M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z" fill="#FFF"/>
</svg>
</section>
<section class="howItWorks">
<div class="container-10">
<h2>{{ $t('pages.services.ai.howItWorks.headline') }}</h2>
</div>
<div class="coderWrap">
<div class="text">
<p>{{ $t('pages.services.ai.howItWorks.pretext') }}</p>
<ul class="check">
<li>{{ $t('pages.services.ai.howItWorks.point1') }}</li>
<li>{{ $t('pages.services.ai.howItWorks.point2') }}</li>
<li>{{ $t('pages.services.ai.howItWorks.point3') }}</li>
<li>{{ $t('pages.services.ai.howItWorks.point4') }}</li>
<li>{{ $t('pages.services.ai.howItWorks.point5') }}</li>
<li>{{ $t('pages.services.ai.howItWorks.point6') }}</li>
</ul>
<p>{{ $t('pages.services.ai.howItWorks.posttext') }}</p>
</div>
<div class="coderArea">
<SchemaCoder />
</div>
</div>
</section>
<section class="howWeDo">
<svg class="sectionWave wave-top" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 20">
<path d="M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z" fill="#FFF"/>
</svg>
<div class="container-10">
<h2>{{ $t('pages.services.ai.howWeDo.headline') }}</h2>
<p>{{ $t('pages.services.ai.howWeDo.text') }}</p>
<ul class="check">
<li>{{ $t('pages.services.ai.howWeDo.point1') }}</li>
<li>{{ $t('pages.services.ai.howWeDo.point2') }}</li>
<li>{{ $t('pages.services.ai.howWeDo.point3') }}</li>
<li>{{ $t('pages.services.ai.howWeDo.point4') }}</li>
<li>{{ $t('pages.services.ai.howWeDo.point5') }}</li>
</ul>
</div>
<svg class="sectionWave wave-bottom" style="transform: scale(-1,-1)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 20">
<path d="M 0 0 L 500 0 L 500 14 Q 354.4 -2.8 250 11 Q 145.6 24.8 0 14 L 0 0 Z" fill="#FFF"/>
</svg>
</section>
<FAQArea
page-link="/leistungen/ki-kompatible-webseiten"
:headline="$t('pages.services.ai.faq.headline')"
/>
</div>
</template>
<script setup>
import { useMainStore } from '@/stores/main'
const mainStore = useMainStore()
definePageMeta({
alias: [
'/leistungen/ki-kompatible-webseiten', // Deutsch
'/services/ai-compatible-websites', // Englisch
'/services/sites-compatibles-ia', // Französisch
'/servizi/siti-compatibili-ai', // Italienisch
'/servicios/sitios-compatibles-ai', // Spanisch
'/hizmetler/ai-uyumlu-websiteler' // Türkisch
],
name: 'services-ai'
})
</script>
<style lang="sass">
.aiPage
.targetGroup
background: linear-gradient(90deg, #3A283E 0%, #6A3385 100%);
color: white
padding: 12vh 0
margin: 8vh 0
li
line-height: 1.5rem
margin-bottom: 1.5rem
strong
font-size: 110%
.howWeDo
background: linear-gradient(90deg, #6A3385 0%, #3A283E 100%);
color: white
padding: 12vh 0
margin: 8vh 0
.whyAI
padding: 3% 0
img
width: 30%
max-width: 280px
float: left
margin: 1% 6vw 5% 0
.howItWorks
overflow: hidden
.coderWrap
display: flex
align-items: center
justify-content: flex-start
padding-left: 10%
position: relative
.text
flex: 0 0 auto
width: 50%
margin-left: 2%
.coderArea
flex: 0 0 auto
width: 60%
transform: translateX(10%)
overflow: hidden
</style>

View File

@ -0,0 +1,32 @@
<template>
<div class="cmsPage">
<HeroBox
image="/uploads/DML_Service_Header_CMS_a438599970.webp"
:aria-label="$t('pages.services.cms.hero.ariaLabel')"
:content="{
headline1: 'pages.services.cms.hero.headline1',
headline2: 'pages.services.cms.hero.headline2',
headline3: 'pages.services.cms.hero.headline3'
}"
:dark-background="true"
/>
</div>
</template>
<script setup>
definePageMeta({
alias: [
'/leistungen/headless-cms', // Deutsch
'/services/headless-cms', // Englisch
'/services/headless-cms', // Französisch
'/servizi/headless-cms', // Italienisch
'/servicios/headless-cms', // Spanisch
'/hizmetler/headless-cms' // Türkisch
],
name: 'services-cms'
})
</script>
<style lang="sass">
</style>

View File

@ -1,5 +1,5 @@
<template>
<div>
<div class="servicesPage">
<section class="heroBox_service" aria-labelledby="hero-heading">
<NuxtImg
provider="strapi"
@ -102,7 +102,7 @@ class="pinkBtn" role="button"
<MarqueeBanner
:items="projectItems"
:logoHeight="200"
:logo-height="200"
:title="$t('pages.services.marquee.title')"
link="projekt"
:aria-label="$t('pages.services.marquee.title')"
@ -112,7 +112,7 @@ class="pinkBtn" role="button"
<CallToActionBox
:headline="$t('pages.services.ctaBox.headline')"
:text="$t('pages.services.ctaBox.text')"
:buttonText="$t('pages.services.ctaBox.button')"
:button-text="$t('pages.services.ctaBox.button')"
/>
</div>
@ -253,179 +253,180 @@ watchEffect(() => {
</script>
<style lang="sass">
.heroBox_service
position: relative
min-height: 35rem
height: 70vh
display: flex
align-items: center
justify-content: center
overflow: hidden
.hero-bg
position: absolute
inset: 0
width: 100%
height: 100%
object-fit: cover
object-position: center bottom
z-index: 0
.container-10, h1, h2, h3
.servicesPage
.heroBox_service
position: relative
z-index: 1
h1
margin-top: 3rem
z-index: 2
color: mix(black, $pink, 2%)
font-size: clamp(1rem, .8rem + 1vw, 1.2rem)
line-height: 1.5
margin-bottom: 0
max-width: 70%
@media (max-width: $breakPointMD)
max-width: 100%
h2
z-index: 2
font-size: clamp(1.4rem, .8rem + 2vw, 2.4rem)
line-height: 1.5
margin-top: 1rem
max-width: 55%
font-family: 'Comfortaa'
@media (max-width: $breakPointMD)
max-width: 90%
h3
max-width: 55%
line-height: 1.5
font-size: clamp(1rem, .8rem + 1vw, 1.6rem)
@media (max-width: $breakPointMD)
max-width: 90%
&::after
content: ''
position: absolute
top: 0
left: 0
width: 50%
height: 100%
background-image: linear-gradient(to right, rgba(white, .3), transparent)
z-index: 1
.container
z-index: 2
.webStrategy
padding: 4rem 0 3.5rem 0
h2
font-size: clamp(1.6rem, 1rem + 2vw, 1.8rem)
line-height: 150%
font-family: 'Comfortaa'
margin-left: 30%
h3
font-size: clamp(1.1rem, .75rem + 1vw, 1.25rem)
line-height: 150%
margin-left: 30%
p
margin-left: 30%
button
margin-left: 30%
&::after
content: ''
position: absolute
top: 5%
left: -36vw
width: 60vw
height: 90%
min-height: 550px
max-height: 800px
background-image: url('https://strapi.digimedialoop.de/uploads/Net_f1020a2216.png')
background-repeat: no-repeat
background-position: center right
background-size: cover
border-radius: 42% 49% 52% 48% / 53% 38% 62% 47%
animation: bubble-wobble 25s infinite ease alternate, gradient-animation 70s infinite alternate ease-in-out
box-shadow: $innerShadow
@media(max-width: $breakPointMD)
right: -50vw
.canDo
margin: 12vh 0
h2
margin-bottom: 3.5rem
font-size: clamp(1.6rem, 1rem + 2vw, 1.8rem)
.canDoGrid
display: grid
gap: 2rem
min-height: 35rem
height: 70vh
display: flex
align-items: center
justify-content: center
align-items: stretch
grid-template-columns: 1fr // Default: 1 Spalte
overflow: hidden
@media (min-width: $breakPointMD)
grid-template-columns: repeat(2, 1fr)
@media (min-width: $breakPointXL)
grid-template-columns: repeat(4, 1fr)
.canDoBox
.hero-bg
position: absolute
inset: 0
width: 100%
display: flex
flex-direction: column
align-items: center
justify-content: flex-start
background-image: linear-gradient(to bottom right, transparent , white)
box-shadow: 3px 3px 8px 1px $lightgrey
border-bottom-right-radius: 1rem
padding: 1rem 1.5rem
border-right: 1px solid lighten($beige, 0%)
border-bottom: 1px solid lighten($beige, 0%)
height: 100%
object-fit: cover
object-position: center bottom
z-index: 0
.canDoItem
width: 100%
.container-10, h1, h2, h3
position: relative
z-index: 1
.imageBox
margin: 0rem auto 1rem auto
h1
margin-top: 3rem
z-index: 2
color: mix(black, $pink, 2%)
font-size: clamp(1rem, .8rem + 1vw, 1.2rem)
line-height: 1.5
margin-bottom: 0
max-width: 70%
@media (max-width: $breakPointMD)
max-width: 100%
h2
z-index: 2
font-size: clamp(1.4rem, .8rem + 2vw, 2.4rem)
line-height: 1.5
margin-top: 1rem
max-width: 55%
font-family: 'Comfortaa'
@media (max-width: $breakPointMD)
max-width: 90%
h3
max-width: 55%
line-height: 1.5
font-size: clamp(1rem, .8rem + 1vw, 1.6rem)
@media (max-width: $breakPointMD)
max-width: 90%
&::after
content: ''
position: absolute
top: 0
left: 0
width: 50%
height: 100%
background-image: linear-gradient(to right, rgba(white, .3), transparent)
z-index: 1
.container
z-index: 2
.webStrategy
padding: 4rem 0 3.5rem 0
h2
font-size: clamp(1.6rem, 1rem + 2vw, 1.8rem)
line-height: 150%
font-family: 'Comfortaa'
margin-left: 30%
h3
font-size: clamp(1.1rem, .75rem + 1vw, 1.25rem)
line-height: 150%
margin-left: 30%
p
margin-left: 30%
button
margin-left: 30%
&::after
content: ''
position: absolute
top: 5%
left: -36vw
width: 60vw
height: 90%
min-height: 550px
max-height: 800px
background-image: url('https://strapi.digimedialoop.de/uploads/Net_f1020a2216.png')
background-repeat: no-repeat
background-position: center right
background-size: cover
border-radius: 42% 49% 52% 48% / 53% 38% 62% 47%
animation: bubble-wobble 25s infinite ease alternate, gradient-animation 70s infinite alternate ease-in-out
box-shadow: $innerShadow
@media(max-width: $breakPointMD)
right: -50vw
.canDo
margin: 12vh 0
h2
margin-bottom: 3.5rem
font-size: clamp(1.6rem, 1rem + 2vw, 1.8rem)
.canDoGrid
display: grid
gap: 2rem
justify-content: center
align-items: stretch
grid-template-columns: 1fr // Default: 1 Spalte
@media (min-width: $breakPointMD)
grid-template-columns: repeat(2, 1fr)
@media (min-width: $breakPointXL)
grid-template-columns: repeat(4, 1fr)
.canDoBox
width: 100%
max-width: 180px
aspect-ratio: 5 / 4
object-fit: cover
border-radius: 1rem
display: block
display: flex
flex-direction: column
align-items: center
justify-content: flex-start
background-image: linear-gradient(to bottom right, transparent , white)
box-shadow: 3px 3px 8px 1px $lightgrey
border-bottom-right-radius: 1rem
padding: 1rem 1.5rem
border-right: 1px solid lighten($beige, 0%)
border-bottom: 1px solid lighten($beige, 0%)
height: 100%
&:nth-child(1)
border-radius: $loopShape1
&:nth-child(2)
border-radius: $loopShape2
&:nth-child(3)
border-radius: $loopShape3
&:nth-child(4)
border-radius: $loopShape4
.canDoItem
width: 100%
h3
font-size: 1.2rem
line-height: 1.5rem
text-align: center
font-family: 'Mainfont-Bold'
color: darken($pink, 10%)
text-transform: uppercase
margin-bottom: .5rem
.imageBox
margin: 0rem auto 1rem auto
width: 100%
max-width: 180px
aspect-ratio: 5 / 4
object-fit: cover
border-radius: 1rem
display: block
p
font-size: .9rem
text-align: left !important
&:nth-child(1)
border-radius: $loopShape1
&:nth-child(2)
border-radius: $loopShape2
&:nth-child(3)
border-radius: $loopShape3
&:nth-child(4)
border-radius: $loopShape4
h3
font-size: 1.2rem
line-height: 1.5rem
text-align: center
font-family: 'Mainfont-Bold'
color: darken($pink, 10%)
text-transform: uppercase
margin-bottom: .5rem
p
font-size: .9rem
text-align: left !important
.targetGroup
background-image: url('https://strapi.digimedialoop.de/uploads/smartphone_Contacts_40ae56a178.jpg')
background-repeat: no-repeat
background-size: cover
background-position: center top
min-height: 70vh
display: flex
align-items: center
justify-content: center
position: relative
padding: 3rem 0
h2
font-size: clamp(1.6rem, 1rem + 2vw, 1.8rem)
h3
font-size: clamp(1.1rem, .8rem + 1vw, 1.2rem)
line-height: 150%
.targetGroup
background-image: url('https://strapi.digimedialoop.de/uploads/smartphone_Contacts_40ae56a178.jpg')
background-repeat: no-repeat
background-size: cover
background-position: center top
min-height: 70vh
display: flex
align-items: center
justify-content: center
position: relative
padding: 3rem 0
h2
font-size: clamp(1.6rem, 1rem + 2vw, 1.8rem)
h3
font-size: clamp(1.1rem, .8rem + 1vw, 1.2rem)
line-height: 150%
</style>

View File

@ -1,5 +1,5 @@
<template>
<div class="accessiblityPage">
<div class="seoPage">
<HeroBox
image="/uploads/DML_Service_Header_SEO_b11ae8940a.webp"
:aria-label="$t('pages.services.seo.hero.ariaLabel')"
@ -8,6 +8,7 @@
headline2: 'pages.services.seo.hero.headline2',
headline3: 'pages.services.seo.hero.headline3'
}"
:dark-background="true"
/>
</div>
</template>
@ -21,7 +22,8 @@ definePageMeta({
'/servizi/ottimizzazione-motori-di-ricerca',// Italienisch
'/servicios/optimizacion-motores-busqueda', // Spanisch
'/hizmetler/arama-motoru-optimizasyonu' // Türkisch
]
],
name: 'services-seo'
})
</script>

View File

@ -10,9 +10,9 @@
<p v-html="$t('pages.webagency.hero.text3')" />
<button
class="my-4 pinkBtn"
@click.prevent="toggleContactBubble"
role="button"
aria-label="Kontaktformular öffnen"
@click.prevent="toggleContactBubble"
>
{{ $t('pages.webagency.hero.button') }}
</button>
@ -75,9 +75,9 @@
<button
class="pinkBtn mt-4"
@click.prevent="toggleContactBubble"
role="button"
aria-label="Kontaktformular öffnen"
@click.prevent="toggleContactBubble"
>
{{ $t('pages.webagency.team.button') }}
</button>
@ -91,9 +91,9 @@
<h2>{{ $t('pages.webagency.grafiker.title') }}</h2>
<button
class="mintBtn"
@click.prevent="navigateTo(designerLink)"
role="button"
aria-label="Zum Angebot für Kreative"
@click.prevent="navigateTo(designerLink)"
>
{{ $t('pages.webagency.grafiker.button') }}
</button>

View File

@ -111,6 +111,8 @@ export const useMainStore = defineStore('main', {
contactBoxOpen: false,
scrollPosition: 0,
screenWidth: 1440,
heroBoxHeight: 0,
darkHeroBack: false,
companyinfo: null as CompanyInfo | null,
pages: [] as Page[],
customers: [] as Customer[],
@ -162,6 +164,12 @@ export const useMainStore = defineStore('main', {
setScreenWidth(width: number) {
this.screenWidth = width
},
setHeroBoxHeight(height: number) {
this.heroBoxHeight = height
},
setDarkHeroBack(value: boolean) {
this.darkHeroBack = value
},
// SEND CONTACT REQUEST TO STRAPI