article carousel

This commit is contained in:
Sabrina Hennrich 2025-06-23 17:53:46 +02:00
parent 51d9f75853
commit 8a72eafe4b
13 changed files with 731 additions and 198 deletions

112
components/ArticleCard.vue Normal file
View File

@ -0,0 +1,112 @@
<!-- components/ArticleCard.vue -->
<template>
<div class="article">
<NuxtLinkLocale
:to="link"
class="article-link"
>
<div class="image-wrapper">
<NuxtImg
v-if="image?.url"
:src="image.url"
provider="strapi"
:alt="image.alternativeText"
format="webp"
class="article-image"
/>
<div class="overlay">
<h2>{{ header }}</h2>
</div>
<button class="btn mintBtn">{{ readmoreText }}</button>
</div>
</NuxtLinkLocale>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
header: String,
image: Object,
link: String,
readmoreText: {
type: String,
default: 'Weiterlesen'
}
})
</script>
<style scoped lang="sass">
.article
width: 100%
max-width: 500px
border: 1px solid $beige
background: linear-gradient(to bottom right, white, $lightgrey)
border-radius: 1rem
position: relative
display: flex
flex-direction: column
align-items: flex-start
overflow: hidden
cursor: pointer
transition: transform 0.3s ease, opacity 0.3s ease
&:hover
transform: scale(1.05)
.article-link
position: relative
display: block
color: white
text-decoration: none
.image-wrapper
position: relative
width: 100%
height: 220px
border: 1px solid $lightgrey
.article-image
width: 100%
min-height: 500px
object-fit: cover
border-top-left-radius: 1rem
border-top-right-radius: 1rem
opacity: .6
button
position: absolute
bottom: .6rem
right: 0rem
border: 1px solid $darkgrey
font-size: 1rem
box-shadow: 1px 1px 4px 2px rgba(black, .2)
background-color: lighten($darkgrey, 10%)
letter-spacing: .05rem
.overlay
position: absolute
top: 0
left: 0
width: 80%
height: auto
min-height: 80%
background-image: linear-gradient(to bottom right, rgba(darken(white, 0), 1), rgba(darken($beige, 0), 0.9))
margin: 0 20% 7rem 0
display: flex
flex-direction: column
align-items: center
justify-content: flex-start
padding: 1rem
border-top-left-radius: 1rem
border-bottom-right-radius: 50%
box-shadow: 2px 2px 5px 3px rgba(black, .2)
h2
color: $darkgrey
font-size: 1rem
line-height: 140%
font-family: 'Mainfont-Bold'
margin: .2rem 1rem
hyphens: auto
</style>

View File

@ -0,0 +1,171 @@
<!-- components/ArticleCarousel.vue -->
<template>
<div class="carousel-wrapper">
<button class="nav-button left" @click="prev"></button>
<div class="carousel">
<transition-group
:name="directionClass"
tag="div"
class="carousel-inner"
>
<div
v-for="(article, index) in visibleItems"
:key="article.id + '-' + currentIndex"
class="carousel-item"
>
<ArticleCard
:header="article.header"
:image="article.image"
:link="localePath({ name: 'article-link', params: { link: article.slug } })"
:readmore-text="$t('pages.magazin.readmore')"
/>
</div>
</transition-group>
</div>
<button class="nav-button right" @click="next"></button>
</div>
</template>
<script setup lang="ts">
import ArticleCard from './ArticleCard.vue'
import { useLocalePath } from '#i18n'
const props = defineProps({
articles: {
type: Array,
required: true
}
})
const localePath = useLocalePath()
const currentIndex = ref(0)
const itemsPerView = ref(3)
const direction = ref<'left' | 'right'>('right')
const directionClass = computed(() =>
direction.value === 'left' ? 'slide-left' : 'slide-right'
)
const updateItemsPerView = () => {
const width = window.innerWidth
if (width < 600) itemsPerView.value = 1
else if (width < 1024) itemsPerView.value = 2
else itemsPerView.value = 3
}
onMounted(() => {
updateItemsPerView()
window.addEventListener('resize', updateItemsPerView)
})
const visibleItems = computed(() => {
const result = []
for (let i = 0; i < itemsPerView.value; i++) {
const index = (currentIndex.value + i) % props.articles.length
result.push(props.articles[index])
}
return result
})
function prev() {
direction.value = 'left'
currentIndex.value = (currentIndex.value - 1 + props.articles.length) % props.articles.length
}
function next() {
direction.value = 'right'
currentIndex.value = (currentIndex.value + 1) % props.articles.length
}
</script>
<style lang="sass">
.carousel-wrapper
position: relative
display: flex
align-items: center
justify-content: center
gap: 1rem
.nav-button
all: unset
cursor: pointer
font-size: 4rem
color: lighten($darkgrey, 20%)
font-weight: bold
padding: 0.5rem 1rem
border-radius: 1rem
background-image: radial-gradient(white, transparent)
transition: 0.3s
&:hover
background-image: radial-gradient($beige, transparent)
.carousel
overflow-x: clip
overflow-y: visible
max-width: 100%
flex: 1
.carousel-inner
display: flex
gap: 2rem
transition: all 0.5s ease
align-items: stretch
.carousel-item
flex: 1 0 calc(100% / 3)
min-width: 0
transition: transform 0.3s ease
display: flex
// Slide nach rechts (Nächste)
.slide-right-enter-active
transition: transform 0.6s ease, opacity 0.6s ease
.slide-right-enter-from
transform: translateX(100%) scale(0.8)
opacity: 0
.slide-right-enter-to
transform: translateX(0) scale(1)
opacity: 1
.slide-right-leave-active
transition: transform 0.4s ease
.slide-right-leave-from
transform: translateX(0)
opacity: 1
.slide-right-leave-to
transform: translateX(-100%)
opacity: 1
// Slide nach links (Vorherige)
.slide-left-enter-active
transition: transform 0.6s ease, opacity 0.6s ease
.slide-left-enter-from
transform: translateX(-100%) scale(0.8)
opacity: 0
.slide-left-enter-to
transform: translateX(0) scale(1)
opacity: 1
.slide-left-leave-active
transition: transform 0.4s ease
.slide-left-leave-from
transform: translateX(0)
opacity: 1
.slide-left-leave-to
transform: translateX(100%)
opacity: 1
</style>

View File

@ -121,11 +121,11 @@
},
"webagency": {
"hero": {
"title": "Ihre Webagentur für strategische Webentwicklung und funktionales Webdesign in Herrsching am Ammersee",
"subtitle": "Wir entwickeln Webseiten, die aus Besuchern Kunden machen!",
"text1": "Wir stehen für professionelle, innovative und strategische Weblösungen und kombinieren technisches Know-how mit einem tiefen Verständnis für digitale Kommunikation, um Unternehmen online erfolgreich zu positionieren",
"text2": "Unser Ansatz ist immer individuell: Jedes Projekt wird mit Sorgfalt, Weitblick und den neuesten Technologien realisiert. Dabei setzen wir auf eine enge Zusammenarbeit und maßgeschneiderte Lösungen, die zu unseren Kunden passen. Wir begleiten Unternehmen aus unterschiedlichsten Branchen von kleinen Betrieben bis hin zu größeren Firmen auf ihrem Weg zu einer erfolgreichen Online-Präsenz.",
"text3": "Lassen Sie uns gemeinsam Ihre digitale Präsenz optimieren!",
"title": "Ihre Webagentur in Herrsching am Ammersee",
"subtitle": "Wir entwickeln strategische Webseiten mit funktionalem Design, die aus Besuchern Kunden machen!",
"text1": "Digitale Sichtbarkeit beginnt mit einer Website, die mehr kann als gut aussehen: Sie wird zum zentralen Marketinginstrument: Vom MVP (Minimal Viable Product) bis hin zum ausgereiften Lead-Generator.",
"text2": "Mit modernen Technologien wie Nuxt 3 und einem Headless CMS entstehen Weblösungen, die flexibel wachsen, effizient im Team gepflegt werden können und perfekt auf Kommunikations- und Marketingprozesse abgestimmt sind.",
"text3": "Ob Unternehmen oder Agentur im Fokus steht immer eine Lösung, die Ziele erreichbar macht: sichtbar werden, Vertrauen aufbauen, Kunden gewinnen.",
"button": "Ihr Kontakt zu uns!"
},
"team": {

View File

@ -1,50 +1,59 @@
<template>
<div v-if="article" class="article">
<SideBarNaviSlider link="/magazin">
{{ $t('pages.article.artikelUebersicht') }}
</SideBarNaviSlider>
<div class="articlePage">
<NuxtImg
v-if="article.image?.url"
:src="article.image.url"
:alt="article.image.alternativeText || article.header"
class="img_background"
provider="strapi"
role="img"
/>
<div v-if="article" class="article">
<SideBarNaviSlider link="/magazin">
{{ $t('pages.article.artikelUebersicht') }}
</SideBarNaviSlider>
<div class="paper">
<p class="articleInfo">
<span><b>{{ $t('pages.article.autor') }}</b> {{ author }}</span> <span><b>{{ $t('pages.article.date') }}</b> {{ formattedDate }}</span>
</p>
<h1>{{ article.header }}</h1>
<NuxtImg
v-if="article.image?.url && screenWidth > 800"
:src="article.image.url"
:alt="article.image.alternativeText || article.header"
class="img_article"
provider="strapi"
role="img"
/>
<p class="teaser">{{ article.teaser }}</p>
<NuxtImg
v-if="article.image?.url"
:src="article.image.url"
:alt="article.image.alternativeText || article.header"
class="img_background"
provider="strapi"
role="img"
/>
<div class="paper">
<p class="articleInfo">
<span><b>{{ $t('pages.article.autor') }}</b> {{ author }}</span> <span><b>{{ $t('pages.article.date') }}</b> {{ formattedDate }}</span>
</p>
<h1>{{ article.header }}</h1>
<NuxtImg
v-if="article.image?.url && screenWidth > 800"
:src="article.image.url"
:alt="article.image.alternativeText || article.header"
class="img_article"
provider="strapi"
role="img"
/>
<p class="teaser">{{ article.teaser }}</p>
<div class="content" v-html="htmlContent(article.content)"></div>
<div class="content" v-html="htmlContent(article.content)"></div>
<button
class="btn pinkBtn"
role="button"
aria-label="Kontakt aufnehmen"
@click.prevent="toggleContactBubble"
>
{{ $t('pages.article.buttonText') }}
</button>
<button
class="btn pinkBtn"
role="button"
aria-label="Kontakt aufnehmen"
@click.prevent="toggleContactBubble"
>
{{ $t('pages.article.buttonText') }}
</button>
</div>
</div>
</div>
<section v-else class="container topSpace">
<p>{{ $t('pages.article.ladenOderNichtGefunden') }}</p>
</section>
<section v-else class="container topSpace">
<p>{{ $t('pages.article.ladenOderNichtGefunden') }}</p>
</section>
<div class="container-5">
<h3>Vielleicht sind diese Artikel auch Interessant?</h3>
<ArticleCarousel :articles="recommendedArticles" />
</div>
</div>
</template>
@ -63,11 +72,20 @@
const runtimeConfig = useRuntimeConfig();
const route = useRoute()
const slug = route.params.link
console.log(slug)
const slug = ref(route.params.link)
watch(() => route.params.link, (newSlug) => {
slug.value = newSlug
})
const mainStore = useMainStore()
const { articles } = storeToRefs(mainStore)
const { articles, getRecommendedArticles } = storeToRefs(mainStore)
const recommendedArticles = computed(() => {
if (!article.value) return []
return getRecommendedArticles.value(article.value)
})
const { t } = useI18n()
@ -76,7 +94,7 @@
// Artikel suchen
const article = computed(() => {
if (!articles.value) return null
return articles.value.find(item => item.slug === slug) ?? null
return articles.value.find(item => item.slug === slug.value) ?? null
})
const { convertToHTML } = useHtmlConverter()
@ -182,7 +200,7 @@
.paper
background-color: rgba(white, .98)
width: 88%
margin: 10rem 6% 15vh 6%
margin: 10rem 6% 3rem 6%
position: relative
z-index: 1
border-radius: 1rem

View File

@ -33,32 +33,16 @@
class="grid"
appear
>
<div
<ArticleCard
v-for="article in filteredArticles"
:key="article.id"
class="article"
>
<NuxtLinkLocale
:to="localePath({ name: 'article-link', params: { link: article.slug } })"
class="article-link"
>
<div class="image-wrapper">
<NuxtImg
v-if="article.image?.url"
:src="article.image.url"
provider="strapi"
:alt="article.image.alternativeText"
format="webp"
class="article-image"
/>
<div class="overlay">
<h2>{{ article.header }}</h2>
</div>
<button class="btn mintBtn">{{ $t('pages.magazin.readmore') }}</button>
</div>
</NuxtLinkLocale>
</div>
:header="article.header"
:image="article.image"
:link="localePath({ name: 'article-link', params: { link: article.slug } })"
:readmore-text="$t('pages.magazin.readmore')"
/>
</transition-group>
</section>
</div>
@ -66,6 +50,14 @@
<script setup lang="ts">
definePageMeta({
sitemap: {
lastmod: '2025-06-23', // ISO 8601 oder dynamisch generieren
changefreq: 'daily', // optional
priority: 1 // optional
}
})
import { useMainStore } from '@/stores/main'
import { storeToRefs } from 'pinia'
import { useLocalePath } from '#i18n'
@ -178,22 +170,7 @@ watch(articles, (newVal) => {
margin: 0 auto
transition: all 0.3s ease-in-out // weichere Umordnung der Items
.article
width: 100%
max-width: 500px
border: 1px solid $beige
background: linear-gradient(to bottom right, white, $lightgrey)
border-radius: 1rem
position: relative
display: flex
flex-direction: column
align-items: flex-start
overflow: hidden
cursor: pointer
transition: transform 0.3s ease, opacity 0.3s ease
&:hover
transform: scale(1.05)
.article-enter-from,
.article-leave-to
@ -205,76 +182,5 @@ watch(articles, (newVal) => {
transition: all 0.3s ease
.article-link
position: relative
display: block
color: white
text-decoration: none
.image-wrapper
position: relative
width: 100%
height: 220px
border: 1px solid $lightgrey
.article-image
width: 100%
height: 500px
object-fit: cover
border-top-left-radius: 1rem
border-top-right-radius: 1rem
opacity: .6
button
position: absolute
bottom: .6rem
right: 0rem
border: 1px solid $darkgrey
font-size: 1rem
box-shadow: 1px 1px 4px 2px rgba(black, .2)
background-color: lighten($darkgrey, 10%)
letter-spacing: .05rem
.overlay
position: absolute
top: 0
left: 0
width: 80%
height: auto
min-height: 80%
background-image: linear-gradient(to bottom right, rgba(darken(white, 0), 1), rgba(darken($beige, 0), 0.9) )
margin: 0 20% 7rem 0
display: flex
flex-direction: column
align-items: center
justify-content: flex-start
padding: 1rem
border-top-left-radius: 1rem
border-bottom-right-radius: 50%
//border: 1px solid darken($beige, 10%)
//border-radius: .5rem
text-align: left
transition: .3s
box-shadow: 2px 2px 5px 3px rgba(black, .2)
h2
color: $darkgrey
font-size: 1rem
line-height: 140%
font-family: 'Mainfont-Bold'
margin: .2rem 1rem
//text-transform: uppercase
hyphens: auto
.mintBtn
background-color: $primaryColor
color: white
font-size: 0.9rem
font-family: 'Mainfont-Bold'
border: none
padding: 0.4rem 1rem
border-radius: 0.3rem
cursor: pointer
</style>

View File

@ -18,21 +18,53 @@
alt=""
/>
<div class="textBox">
<h2>Sie möchten sich eine neue Webseite für Ihre Schule oder Ihren Kindergarten erstellen lassen?</h2>
<h2>Sie benötigen eine neue Webseite für Ihre Schule oder Ihren Kindergarten?</h2>
<p> Wir entwickeln moderne, barrierefreie Webseiten, die Eltern, Schüler und Mitarbeitende klar informieren und sich ganz einfach pflegen lassen. </p>
<p>Erfahren Sie mehr in unseren beiden Fachartikeln:
<ul>
<li><a href="/artikel/auf-was-man-bei-der-erstellung-von-schulwebseiten-achten-sollte" target="_blank">Worauf es bei Webseiten im Bildungsbereich ankommt</a></li>
<li><a href="/artikel/digitale-elternkommunikation-welche-rolle-spielt-die-schulhomepage" target="_blank">Digitale Elternkommunikation: Welche Rolle spielt die Schulhomepage?</a></li>
</ul>
</p>
<p> <b>Dann sind Sie hier goldrichtig!</b> Denn wir erstellen moderne, barrierefreie Webseiten, die Eltern, Schüler und Mitarbeitende klar informieren und sich ganz einfach pflegen lassen. </p>
</div>
</div>
</div>
</section>
<section class="forWhom">
<NuxtImg
provider="strapi"
src="/uploads/watercolor_da8a37ce48.webp"
alt=""
class="accessibility-bg"
sizes="100vw"
priority
/>
<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-20">
<h2>Unsere Weblösungen richten sich unter anderem an folgende Bildungseinrichtungen:</h2>
<ul class="check">
<li>Grundschulen</li>
<li>weiterführende Schulen (z.B. Gymnasien, Realschulen, Mittelschulen, Gesamtschule)</li>
<li>Förderschulen</li>
<li>Berufsschulen und Berufsfachschulen</li>
<li>Kindergärten und Kitas</li>
<li>Horte und Ganztagseinrichtungen</li>
<li>Volkshochschulen</li>
<li>Musikschulen</li>
<li>Familienzentren</li>
<li>Bildungsträger und Akademien</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="articleBox container-5">
<h3>Wissenswertes rund um die Webseite für Bildungseinrichtungen</h3>
<ArticleCarousel :articles="recommendedArticles" />
</section>
<section class="accessibility">
<NuxtImg
provider="strapi"
@ -46,7 +78,7 @@
<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="paper">
<h2>Barrierefreiheit ist für Bildungseinrichtungen in Deutschland Pflicht!</h2>
<h2>Schon gewusst? - Barrierefreiheit ist für Bildungseinrichtungen in Deutschland Pflicht!</h2>
<p>
Wir erstellen Ihre Schulhomepage von Beginn an nach den <b>Anforderungen der Barrierefreiheit (BITV 2.0)</b> und sorgen dafür, dass Ihre Seite auch dann barrierefrei bleibt, wenn Inhalte später von Ihnen oder dem Kollegium selbst eingepflegt werden.
</p>
@ -154,7 +186,14 @@
</template>
<script setup>
import { useMainStore } from '@/stores/main'
import { storeToRefs } from 'pinia'
const mainStore = useMainStore()
const { getArticlesByCategoryIds } = storeToRefs(mainStore)
const recommendedArticles = computed(() => {
return getArticlesByCategoryIds.value([5])
})
</script>
<style lang="sass">
@ -163,6 +202,8 @@
hyphens: auto
.weKnow
position: relative
padding-top: 2rem
padding-bottom: 2rem
img
position: absolute
top: -5%
@ -172,7 +213,7 @@
z-index: 4
.textBox
z-index: 5
margin-left: 25%
margin-left: 20%
max-width: 700px
width: 55%
h2
@ -187,33 +228,43 @@
line-height: 140%
font-size: 1.3rem
.forWhom
margin: 3rem auto
padding-top: 4rem
padding-bottom: 4rem
.articleBox
margin-top: 5rem
margin-bottom: 6rem
h3
text-align: center
.accessibility
position: relative
padding: 5rem 0
padding: 4rem 0
overflow: hidden
.accessibility-bg
position: absolute
top: 0
left: 0
width: 100%
height: 100%
object-fit: cover
object-position: center
opacity: 0.15 // sehr sanft, nicht störend
z-index: -1
pointer-events: none
.paper
padding: 2rem 5%
padding: 1rem 5%
background-color: rgba(white, .8)
margin: 4rem 10%
margin: 1rem 10%
transform: rotate(-1deg)
border: 1px solid $lightgrey
box-shadow: 2px 2px 10px 2px #EEE
h2
font-size: 1.8rem
.accessibility-bg
position: absolute
top: 0
left: 0
width: 100%
height: 100%
object-fit: cover
object-position: center
opacity: 0.15 // sehr sanft, nicht störend
z-index: -1
pointer-events: none
.cmsAdmin
text-align: center
.textBox

View File

@ -80,13 +80,24 @@ class="btn pinkBtn" role="button"
</section>
<CallToActionBox
headline="Sie brauchen einen Barrierefreiheitscheck?"
text="Kein Problem! Wir schauen uns Ihre Seite gerne an..."
button-text="Kontaktieren Sie uns!"
/>
<section class="articleBox container-5">
<h3>Lesen Sie jetzt mehr zum Thema Barrierefreiheit und Datenschutz im Web</h3>
<ArticleCarousel :articles="recommendedArticles" />
</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
@ -96,14 +107,31 @@ definePageMeta({
'/servicios/accesibilidad', // Spanisch
'/hizmetler/erisilebilirlik' // Türkisch
],
name: 'services-accessibility'
name: 'services-accessibility',
sitemap: {
lastmod: '2025-06-23', // ISO 8601 oder dynamisch generieren
changefreq: 'monthly', // optional
priority: 1 // optional
}
})
import { useMainStore } from '@/stores/main'
import { storeToRefs } from 'pinia'
const mainStore = useMainStore()
const { getArticlesByCategoryIds } = storeToRefs(mainStore)
const recommendedArticles = computed(() => {
return getArticlesByCategoryIds.value([1])
})
const mainStore = useMainStore();
const toggleContactBubble = () => mainStore.toggleContactBubble();
</script>
<style lang="sass">
.accessiblityPage
.articleBox
margin-top: 5rem
margin-bottom: 6rem
h3
text-align: center
.legalBasis
background: linear-gradient(90deg, #39324A 0%, #403871 100%);
color: white

View File

@ -105,6 +105,17 @@
</svg>
</section>
<CallToActionBox
headline="Sich wollen eine KI-kompatible Webseite?"
text="Lassen Sie uns gemeinsam durchstarten."
button-text="Wir beraten Sie gerne!"
/>
<section class="articleBox container-5">
<h3>Lesen Sie jetzt mehr zur Suchmaschinen-Optimierung in unserem Fachmagazin</h3>
<ArticleCarousel :articles="recommendedArticles" />
</section>
<FAQArea
page-link="/leistungen/ki-kompatible-webseiten"
:headline="$t('pages.services.ai.faq.headline')"
@ -113,8 +124,7 @@
</template>
<script setup>
import { useMainStore } from '@/stores/main'
const mainStore = useMainStore()
definePageMeta({
alias: [
'/leistungen/ki-kompatible-webseiten', // Deutsch
@ -124,12 +134,31 @@ definePageMeta({
'/servicios/sitios-compatibles-ai', // Spanisch
'/hizmetler/ai-uyumlu-websiteler' // Türkisch
],
name: 'services-ai'
name: 'services-ai',
sitemap: {
lastmod: '2025-06-23', // ISO 8601 oder dynamisch generieren
changefreq: 'monthly', // optional
priority: 1 // optional
}
})
import { useMainStore } from '@/stores/main'
import { storeToRefs } from 'pinia'
const mainStore = useMainStore()
const { getArticlesByCategoryIds } = storeToRefs(mainStore)
const recommendedArticles = computed(() => {
return getArticlesByCategoryIds.value([4])
})
</script>
<style lang="sass">
.aiPage
.articleBox
margin-top: 5rem
margin-bottom: 6rem
h3
text-align: center
.targetGroup
background: linear-gradient(90deg, #3A283E 0%, #6A3385 100%);
color: white

View File

@ -140,7 +140,12 @@
<script setup>
definePageMeta({
name: 'services-cms'
name: 'services-cms',
sitemap: {
lastmod: '2025-06-23', // ISO 8601 oder dynamisch generieren
changefreq: 'monthly', // optional
priority: 1 // optional
}
})
import { useMainStore } from '@/stores/main';

View File

@ -120,6 +120,11 @@
button-text="Sprechen Sie mit uns!"
/>
<section class="articleBox container-5">
<h3>Lesen Sie jetzt mehr zur Suchmaschinen-Optimierung in unserem Fachmagazin</h3>
<ArticleCarousel :articles="recommendedArticles" />
</section>
<FAQArea
page-link="/leistungen/suchmaschinenoptimierung"
headline="Fragen und Antworten zur Suchmaschinen-Optimierung"
@ -138,13 +143,31 @@ definePageMeta({
'/servicios/optimizacion-motores-busqueda', // Spanisch
'/hizmetler/arama-motoru-optimizasyonu' // Türkisch
],
name: 'services-seo'
name: 'services-seo',
sitemap: {
lastmod: '2025-06-23', // ISO 8601 oder dynamisch generieren
changefreq: 'monthly', // optional
priority: 1 // optional
}
})
import { useMainStore } from '@/stores/main'
import { storeToRefs } from 'pinia'
const mainStore = useMainStore()
const { getArticlesByCategoryIds } = storeToRefs(mainStore)
const recommendedArticles = computed(() => {
return getArticlesByCategoryIds.value([2])
})
</script>
<style lang="sass">
.seoPage
.articleBox
margin-top: 5rem
margin-bottom: 6rem
h3
text-align: center
ul
padding-left: 1rem
li

View File

@ -0,0 +1,163 @@
<template>
<div class="contrastChecker">
<div class="input-section">
<label for="domain">Domain eingeben:</label>
<div class="row">
<input id="domain" v-model="domainInput" type="text" placeholder="z.B. example.com" />
<button :disabled="!isValidDomain" @click="loadDomain">Anzeigen</button>
</div>
<p v-if="domainInput && !isValidDomain" class="error">Ungültige Domain</p>
</div>
<iframe
v-if="iframeSrc"
:src="iframeSrc"
class="preview-frame"
sandbox="allow-same-origin allow-scripts"
></iframe>
<div class="color-buttons">
<button @click="pickColor('background')">Hintergrundfarbe wählen</button>
<button @click="pickColor('text')">Schriftfarbe wählen</button>
</div>
<div v-if="contrastRatio" class="result-box">
<p><strong>Kontrast:</strong> {{ contrastRatio.toFixed(2) }}</p>
<p><strong>WCAG Bewertung:</strong> {{ contrastLevel }}</p>
</div>
</div>
</template>
<script setup>
const domainInput = ref('')
const iframeSrc = ref('')
const bgColor = ref(null)
const textColor = ref(null)
const domainRegex = /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
const isValidDomain = computed(() => domainRegex.test(domainInput.value))
// Diese Funktionen hinzufügen
function hexToRgb(hex) {
if (!hex) return null
let c = hex.replace('#', '')
if (c.length === 3) {
c = c.split('').map((char) => char + char).join('')
}
const bigint = parseInt(c, 16)
return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255]
}
function luminance(r, g, b) {
const a = [r, g, b].map(v => {
v /= 255
return v <= 0.03928
? v / 12.92
: Math.pow((v + 0.055) / 1.055, 2.4)
})
return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2]
}
function getContrast(bgHex, fgHex) {
const bgRgb = hexToRgb(bgHex)
const fgRgb = hexToRgb(fgHex)
if (!bgRgb || !fgRgb) return null
const L1 = luminance(...bgRgb)
const L2 = luminance(...fgRgb)
return (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05)
}
function loadDomain() {
iframeSrc.value = 'https://' + domainInput.value
}
function pickColor(type) {
const input = document.createElement('input')
input.type = 'color'
input.style.display = 'none'
input.addEventListener('input', () => {
if (type === 'background') bgColor.value = input.value
if (type === 'text') textColor.value = input.value
})
document.body.appendChild(input)
input.click()
input.remove()
}
const contrastRatio = computed(() => {
if (!bgColor.value || !textColor.value) return null
try {
return getContrast(bgColor.value, textColor.value)
} catch (e) {
return null
}
})
const contrastLevel = computed(() => {
if (!contrastRatio.value) return 'unbekannt'
const ratio = contrastRatio.value
if (ratio >= 7) return 'AAA'
if (ratio >= 4.5) return 'AA'
if (ratio >= 3) return 'A'
return 'nicht ausreichend'
})
</script>
<style lang="sass">
.contrastChecker
max-width: 1200px
margin: 10vh auto 3rem auto
padding: 2rem
.input-section
margin-bottom: 1rem
.row
display: flex
gap: 0.5rem
margin-top: 0.5rem
input[type="text"]
flex: 1
padding: 0.5rem
border: 1px solid #ccc
border-radius: 4px
button
padding: 0.5rem 1rem
border: none
background-color: #007acc
color: white
cursor: pointer
border-radius: 4px
button:disabled
background-color: #ccc
cursor: not-allowed
.error
color: red
font-size: 0.9rem
margin-top: 0.25rem
.preview-frame
width: 100%
height: 300px
border: 1px solid #ccc
margin-top: 1rem
.color-buttons
margin-top: 1rem
display: flex
gap: 1rem
.result-box
margin-top: 1rem
padding: 1rem
border: 1px solid #ccc
border-radius: 4px
background: #f9f9f9
</style>

View File

@ -104,6 +104,14 @@
<script setup>
definePageMeta({
sitemap: {
lastmod: '2025-06-23', // ISO 8601 oder dynamisch generieren
changefreq: 'monthly', // optional
priority: 0.8 // optional
}
})
import { storeToRefs } from "pinia";
import { useMainStore } from "@/stores/main";
import { computed } from "vue";

View File

@ -151,6 +151,25 @@ export const useMainStore = defineStore('main', {
state.projects.find(project => project.link === link),
getArticleBySlug: (state) => (slug: string) =>
state.articles.find((a) => a.slug === slug),
getRecommendedArticles: (state) => (article: NewsArticle) => {
if (!article?.categories || !Array.isArray(state.articles)) return []
const categoryIds = article.categories.map(c => c.id)
return state.articles.filter(a => {
if (a.id === article.id) return false // sich selbst ausschließen
if (!a.categories) return false
return a.categories.some(cat => categoryIds.includes(cat.id))
})
},
getArticlesByCategoryIds: (state) => (categoryIds: number[]) => {
if (!Array.isArray(categoryIds)) return []
return state.articles.filter(article =>
article.categories?.some(cat => categoryIds.includes(cat.id))
)
}
},
actions: {