This commit is contained in:
Sabrina Hennrich 2025-05-20 13:57:41 +02:00
parent f1c1537fe4
commit 954a1febf7
21 changed files with 226 additions and 131 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -16,35 +16,35 @@ $breakPointXXL: 1400px
@font-face @font-face
font-family: 'Mainfont' font-family: 'Mainfont'
src: url('@/assets/fonts/montserrat/Montserrat-Light.otf') format("opentype") src: url('@/assets/fonts/woff2/Montserrat-Light.woff2') format("woff2")
font-weight: normal font-weight: normal
font-style: normal font-style: normal
font-display: swap font-display: swap
@font-face @font-face
font-family: 'Mainfont-Bold' font-family: 'Mainfont-Bold'
src: url('@/assets/fonts/montserrat/Montserrat-Medium.otf') format("opentype") src: url('@/assets/fonts/woff2/Montserrat-Medium.woff2') format("woff2")
font-weight: normal font-weight: normal
font-style: normal font-style: normal
font-display: swap font-display: swap
@font-face @font-face
font-family: 'Comfortaa' font-family: 'Comfortaa'
src: url('@/assets/fonts/Comfortaa-Light.ttf') format("truetype") src: url('@/assets/fonts/woff2/Comfortaa-Light.woff2') format("woff2")
font-weight: normal font-weight: normal
font-style: normal font-style: normal
font-display: swap font-display: swap
@font-face @font-face
font-family: 'Comfortaa-Bold' font-family: 'Comfortaa-Bold'
src: url('@/assets/fonts/Comfortaa-Bold.ttf') format("truetype") src: url('@/assets/fonts/woff2/Comfortaa-Bold.woff2') format("woff2")
font-weight: normal font-weight: normal
font-style: normal font-style: normal
font-display: swap font-display: swap
@font-face @font-face
font-family: 'Typewriter' font-family: 'Typewriter'
src: url('@/assets/fonts/JMH_Typewriter.ttf') format("truetype") src: url('@/assets/fonts/woff2/JMH_Typewriter.woff2') format("woff2")
font-weight: normal font-weight: normal
font-style: normal font-style: normal
font-display: swap font-display: swap

View File

@ -27,10 +27,12 @@
<NuxtImg <NuxtImg
v-if="screenWidth <= 768" v-if="screenWidth <= 768"
class="mobileAspBox" class="mobileAspBox"
:src="cmsUrl + companyinfo?.profileImage?.data?.attributes?.url" :src="companyinfo?.profileImage?.data?.attributes?.url"
alt="Sabrina Hennrich" alt="Sabrina Hennrich"
:width="400" :width="400"
format="webp" format="webp"
provider="strapi"
loading="lazy"
/> />
<h2 id="contactTitle">{{ $t('contactForm.yourcontact2us') }}</h2> <h2 id="contactTitle">{{ $t('contactForm.yourcontact2us') }}</h2>
<p class="my-4"> <p class="my-4">
@ -45,12 +47,16 @@
{{ companyinfo.company }}<br>{{ companyinfo.street }} <br>{{ companyinfo.postalcode }} {{ companyinfo.city }} {{ companyinfo.company }}<br>{{ companyinfo.street }} <br>{{ companyinfo.postalcode }} {{ companyinfo.city }}
</p> </p>
<p class="aspProf">{{ $t('contactForm.yourcontactperson') }} <b>Sabrina Hennrich</b></p> <p class="aspProf">{{ $t('contactForm.yourcontactperson') }} <b>Sabrina Hennrich</b></p>
<div class="aspBox"><NuxtImg <div class="aspBox">
:src="cmsUrl + companyinfo?.profileImage?.data?.attributes?.url" <NuxtImg
:src="companyinfo?.profileImage?.data?.attributes?.url"
alt="Ansprechpartner Sabrina Hennrich" alt="Ansprechpartner Sabrina Hennrich"
:width="150" :width="150"
format="webp" format="webp"
/></div> provider="strapi"
loading="lazy"
/>
</div>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
@ -151,9 +157,7 @@ const { t } = useI18n();
// Zugriff auf den Pinia-Store // Zugriff auf den Pinia-Store
const mainStore = useMainStore(); const mainStore = useMainStore();
const { companyinfo } = storeToRefs(mainStore); const { companyinfo } = storeToRefs(mainStore);
const config = useRuntimeConfig();
const cmsUrl = computed(() => config.public.cmsBaseUrl);
const isContactBubbleOpen = computed(() => mainStore.contactBoxOpen); const isContactBubbleOpen = computed(() => mainStore.contactBoxOpen);
const toggleContactBubble = () => mainStore.toggleContactBubble(); const toggleContactBubble = () => mainStore.toggleContactBubble();

View File

@ -23,7 +23,7 @@
</div> </div>
<!-- Slider --> <!-- Slider -->
<input v-model="sliderValue" type="range" min="0" max="100" class="slider" /> <input v-model="sliderValue" type="range" min="0" max="100" class="slider" >
<!-- Vertikale Trennlinie --> <!-- Vertikale Trennlinie -->
<div class="slider-line" :style="{ left: sliderValue + '%' }" /> <div class="slider-line" :style="{ left: sliderValue + '%' }" />

View File

@ -32,7 +32,7 @@
loading="lazy" loading="lazy"
/> />
</p> </p>
<p>{{ companyinfo?.contact }}</p> <p v-if="false">{{ companyinfo?.contact }}</p>
<p>{{ companyinfo?.street }}</p> <p>{{ companyinfo?.street }}</p>
<p> <p>
{{ companyinfo?.postalcode }} {{ companyinfo?.postalcode }}
@ -176,6 +176,8 @@ footer
margin-bottom: 0.2rem margin-bottom: 0.2rem
margin-top: .2rem margin-top: .2rem
i
font-size: .8rem
a a
cursor: pointer cursor: pointer
color: white color: white
@ -187,6 +189,9 @@ footer
background-color: rgba($primaryColor, .2) background-color: rgba($primaryColor, .2)
border-radius: 4px border-radius: 4px
&.router-link-active
color: lighten($pink, 15%) !important
.logo .logo
width: 10rem !important width: 10rem !important

View File

@ -1,39 +1,105 @@
// composables/usePageMeta.ts
import { watch, computed } from 'vue' import { watch, computed } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { useMainStore } from '@/stores/main' import { useMainStore } from '@/stores/main'
import { useHead } from '@unhead/vue' import { useHead } from '@unhead/vue'
import { storeToRefs } from 'pinia'
// Importiere die Seiten-Routen
import { i18nPages } from '../i18n/i18n-pages' // relativ zu deinem Composable-Pfad anpassen!
export function usePageMeta () { export function usePageMeta () {
const route = useRoute() const route = useRoute()
const mainStore = useMainStore() const mainStore = useMainStore()
const { companyinfo } = storeToRefs(mainStore)
// ► Der aktuelle Page-Eintrag als computed
const currentPage = computed(() => mainStore.getPageByLink(route.path)) const currentPage = computed(() => mainStore.getPageByLink(route.path))
// ► Reagiere auf Route- oder Store-Änderungen
watch( watch(
() => currentPage.value, // Quelle () => currentPage.value,
(page) => { // Callback (page) => {
if (!page) return if (!page || !companyinfo.value) return
const metaTitle = page.SEO?.pageTitle ?? 'Standard Title' const metaTitle = page.SEO?.pageTitle ?? 'digimedialoop'
const metaDescription = page.SEO?.seoDescription ?? 'Standard Description' const metaDescription = page.SEO?.seoDescription ?? 'Webdesign und Webentwicklung'
const metaImage = page.SEO?.seoImage?.url ?? '/default-image.jpg' const metaImage = page.SEO?.seoImage?.url ?? 'https://strapi.digimedialoop.de/uploads/DML_Logo_grey_2024_c51210b70c.svg'
// Canonical URL
const config = useRuntimeConfig()
const canonical = `${config.public.appUrl}${route.path}`
// Robots Meta Tag
const robotsContent = route.path === '/danke' ? 'noindex, nofollow' : 'index, follow'
// Prüfe, ob Route Home oder References in allen Sprachen ist
const isHomePage = Object.values(i18nPages.index).includes(route.path)
const isReferencesPage = Object.values(i18nPages.references).includes(route.path)
// Basis LocalBusiness JSON-LD
const baseJsonLd = {
'@context': 'https://schema.org',
'@type': 'LocalBusiness',
name: companyinfo.value.company,
image: metaImage,
url: config.public.appUrl,
telephone: companyinfo.value.phone,
email: companyinfo.value.email,
address: {
'@type': 'PostalAddress',
streetAddress: companyinfo.value.street,
addressLocality: companyinfo.value.city,
postalCode: companyinfo.value.postalcode,
addressCountry: 'DE'
},
openingHoursSpecification: [
{
'@type': 'OpeningHoursSpecification',
dayOfWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
opens: '09:00',
closes: '17:00'
}
]
}
// Falls Home oder References: ergänze das Rating (2 Bewertungen mit 5 Sternen)
let jsonLd = undefined
if (isHomePage || isReferencesPage) {
jsonLd = {
...baseJsonLd,
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: '5',
reviewCount: '2'
}
}
} else if (isHomePage) {
// Nur LocalBusiness ohne Rating z.B.
jsonLd = baseJsonLd
}
useHead({ useHead({
title: metaTitle, title: metaTitle,
meta: [ meta: [
{ name: 'description', content: metaDescription }, { name: 'description', content: metaDescription },
{ name: 'robots', content: robotsContent },
{ property: 'og:title', content: metaTitle }, { property: 'og:title', content: metaTitle },
{ property: 'og:description', content: metaDescription }, { property: 'og:description', content: metaDescription },
{ property: 'og:image', content: metaImage }, { property: 'og:image', content: metaImage },
{ name: 'twitter:title', content: metaTitle }, { name: 'twitter:title', content: metaTitle },
{ name: 'twitter:description', content: metaDescription }, { name: 'twitter:description', content: metaDescription },
{ name: 'twitter:image', content: metaImage } { name: 'twitter:image', content: metaImage }
],
link: [
{ rel: 'canonical', href: canonical }
],
script: jsonLd
? [
{
type: 'application/ld+json',
children: JSON.stringify(jsonLd)
}
] ]
: []
}) })
}, },
{ immediate: true } // Sofort beim ersten Aufruf ausführen { immediate: true }
) )
} }

66
i18n/i18n-pages.ts Normal file
View File

@ -0,0 +1,66 @@
export const i18nPages = {
index: {
de: '/',
en: '/home',
fr: '/accueil',
it: '/home',
es: '/inicio',
tr: '/anasayfa'
},
webagency: {
de: '/webagentur',
en: '/webagency',
fr: '/agence-web',
it: '/agenzia-web',
es: '/agencia-web',
tr: '/web-ajansi'
},
services: {
de: '/leistungen',
en: '/services',
fr: '/services',
it: '/servizi',
es: '/servicios',
tr: '/hizmetler'
},
references: {
de: '/referenzen',
en: '/references',
fr: '/références',
it: '/referenze',
es: '/referencias',
tr: '/referanslar'
},
imprint: {
de: '/impressum',
en: '/imprint',
fr: '/mentions-legales',
it: '/note-legali',
es: '/aviso-legal',
tr: '/künye'
},
privacy: {
de: '/datenschutz',
en: '/privacy',
fr: '/confidentialite',
it: '/privacy',
es: '/privacidad',
tr: '/gizlilik'
},
terms: {
de: '/agb',
en: '/terms',
fr: '/conditions',
it: '/termini',
es: '/condiciones',
tr: '/kosullar'
},
magazin: {
de: '/wissenswertes',
en: '/magazine',
fr: '/magazine',
it: '/magazine',
es: '/revista',
tr: '/dergi'
}
}

View File

@ -51,10 +51,10 @@
"heroBox": { "heroBox": {
"h1": "Ihre Agentur für individuelles Webdesign und professionelle Webentwicklung", "h1": "Ihre Agentur für individuelles Webdesign und professionelle Webentwicklung",
"h2": "Modulare Webseiten mit modernsten Technologien", "h2": "Modulare Webseiten mit modernsten Technologien",
"h3": "So ist Ihre Website schnell, effizient und zukunftssicher!" "h3": "Höchste Performanz - schnell, effizient und zukunftssicher!"
}, },
"solution": { "solution": {
"title": "Websites, die mehr können: Performance, Freiheit & KI-Power für Ihr Business", "title": "Webseiten, die mehr können: Performance, Freiheit & KI-Power",
"teaser": "Wir entwickeln maßgeschneiderte Webseiten mit JAMstack-Technologie, die perfekt auf Ihr Business abgestimmt sind und als leistungsstarkes Marketing- und Vertriebsinstrument für Ihren Erfolg sorgen.", "teaser": "Wir entwickeln maßgeschneiderte Webseiten mit JAMstack-Technologie, die perfekt auf Ihr Business abgestimmt sind und als leistungsstarkes Marketing- und Vertriebsinstrument für Ihren Erfolg sorgen.",
"text": "Durch die klare Trennung von Inhalt und Technik, unter Verwendung eines headless Content-Management-Systems, entstehen wartungsfreundliche, suchmaschinenoptimierte Lösungen, die nicht nur langfristig skalierbar sind, sondern auch Ihrem Marketing-Team die Arbeit erleichtern. Inhalte lassen sich ohne technische Hürden pflegen, neue Funktionen flexibel integrieren ganz ohne Plugin-Chaos oder Eingriffe ins Live-System. Dank sauberer semantischer Struktur sind unsere Lösungen zudem optimal auf AI-gestützte Suchsysteme vorbereitet und ermöglichen die einfache Integration in KI-gestützte Operator-Workflows.", "text": "Durch die klare Trennung von Inhalt und Technik, unter Verwendung eines headless Content-Management-Systems, entstehen wartungsfreundliche, suchmaschinenoptimierte Lösungen, die nicht nur langfristig skalierbar sind, sondern auch Ihrem Marketing-Team die Arbeit erleichtern. Inhalte lassen sich ohne technische Hürden pflegen, neue Funktionen flexibel integrieren ganz ohne Plugin-Chaos oder Eingriffe ins Live-System. Dank sauberer semantischer Struktur sind unsere Lösungen zudem optimal auf AI-gestützte Suchsysteme vorbereitet und ermöglichen die einfache Integration in KI-gestützte Operator-Workflows.",
"buttonText": "Erfahren Sie mehr über Headless CMS" "buttonText": "Erfahren Sie mehr über Headless CMS"

View File

@ -1,4 +1,17 @@
import { defineNuxtConfig } from 'nuxt/config' import { defineNuxtConfig } from 'nuxt/config'
import { i18nPages } from './i18n/i18n-pages'
// Hilfsfunktion, um Objekt mit locales auf reine Strings zu mappen
function flattenPages(pagesObj: Record<string, Record<string, string>>) {
const result: Record<string, string> = {}
for (const pageKey in pagesObj) {
for (const locale in pagesObj[pageKey]) {
// z.B. 'index.de' = '/'
result[`${pageKey}.${locale}`] = pagesObj[pageKey][locale]
}
}
return result
}
export default defineNuxtConfig({ export default defineNuxtConfig({
app: { app: {
@ -31,8 +44,6 @@ export default defineNuxtConfig({
modules: [ modules: [
'@nuxt/image', '@nuxt/image',
'@nuxt/eslint', '@nuxt/eslint',
'@nuxt/scripts',
'@nuxt/ui',
'@pinia/nuxt', '@pinia/nuxt',
'@nuxtjs/i18n', '@nuxtjs/i18n',
['@pinia/nuxt', { ['@pinia/nuxt', {
@ -54,6 +65,7 @@ export default defineNuxtConfig({
}, },
runtimeConfig: { runtimeConfig: {
public: { public: {
appUrl: process.env.APP_URL,
cmsBaseUrl: process.env.CMS_URL, cmsBaseUrl: process.env.CMS_URL,
cmsToken: process.env.CMS_TOKEN cmsToken: process.env.CMS_TOKEN
} }
@ -74,72 +86,7 @@ export default defineNuxtConfig({
{ code: 'tr', name: 'Türkçe', file: 'tr.json' } { code: 'tr', name: 'Türkçe', file: 'tr.json' }
], ],
customRoutes: 'config', customRoutes: 'config',
pages: { pages: flattenPages(i18nPages),
index: {
de: '/',
en: '/home',
fr: '/accueil',
it: '/home',
es: '/inicio',
tr: '/anasayfa'
},
webagency: {
de: '/webagentur',
en: '/webagency',
fr: '/agence-web',
it: '/agenzia-web',
es: '/agencia-web',
tr: '/web-ajansi'
},
services: {
de: '/leistungen',
en: '/services',
fr: '/services',
it: '/servizi',
es: '/servicios',
tr: '/hizmetler'
},
references: {
de: '/referenzen',
en: '/references',
fr: '/références',
it: '/referenze',
es: '/referencias',
tr: '/referanslar'
},
imprint: {
de: '/impressum',
en: '/imprint',
fr: '/mentions-legales',
it: '/note-legali',
es: '/aviso-legal',
tr: '/künye'
},
privacy: {
de: '/datenschutz',
en: '/privacy',
fr: '/confidentialite',
it: '/privacy',
es: '/privacidad',
tr: '/gizlilik'
},
terms: {
de: '/agb',
en: '/terms',
fr: '/conditions',
it: '/termini',
es: '/condiciones',
tr: '/kosullar'
},
magazin: {
de: '/wissenswertes',
en: '/magazine',
fr: '/magazine',
it: '/magazine',
es: '/revista',
tr: '/dergi'
}
},
bundle: { bundle: {
optimizeTranslationDirective: false optimizeTranslationDirective: false
} }

View File

@ -12,6 +12,7 @@
priority priority
loading="eager" loading="eager"
preload preload
fetchpriority="high"
/> />
<div class="container-10"> <div class="container-10">
<h1 id="hero-heading">{{ $t('home.heroBox.h1') }}</h1> <h1 id="hero-heading">{{ $t('home.heroBox.h1') }}</h1>
@ -76,7 +77,7 @@ class="pinkBtn" role="button"
<div class="row mb-5"> <div class="row mb-5">
<div class="col-xl-6"> <div class="col-xl-6">
<div class="row"> <div class="row">
<div class="col-md-6 my-5"> <div class="col-md-6 mb-5">
<div class="innerBox"> <div class="innerBox">
<div class="canDoItem"> <div class="canDoItem">
<NuxtImg <NuxtImg
@ -94,7 +95,7 @@ class="pinkBtn" role="button"
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6 my-5"> <div class="col-md-6 mb-5">
<div class="innerBox"> <div class="innerBox">
<div class="canDoItem"> <div class="canDoItem">
<NuxtImg <NuxtImg
@ -116,7 +117,7 @@ class="pinkBtn" role="button"
</div> </div>
<div class="col-xl-6"> <div class="col-xl-6">
<div class="row"> <div class="row">
<div class="col-md-6 my-5"> <div class="col-md-6 mb-5">
<div class="innerBox"> <div class="innerBox">
<div class="canDoItem" role="group" aria-labelledby="cando-title"> <div class="canDoItem" role="group" aria-labelledby="cando-title">
<NuxtImg <NuxtImg
@ -134,7 +135,7 @@ class="pinkBtn" role="button"
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6 my-5"> <div class="col-md-6 mb-5">
<div class="innerBox"> <div class="innerBox">
<div class="canDoItem"> <div class="canDoItem">
<NuxtImg <NuxtImg
@ -249,7 +250,7 @@ const logoItems = computed(() => {
width: 100% width: 100%
height: 100% height: 100%
object-fit: cover object-fit: cover
object-position: right bottom object-position: center bottom
z-index: 0 // liegt unter Content z-index: 0 // liegt unter Content
.container-10, h1, h2, h3 .container-10, h1, h2, h3
@ -272,6 +273,7 @@ const logoItems = computed(() => {
line-height: 1.5 line-height: 1.5
margin-top: 1rem margin-top: 1rem
max-width: 55% max-width: 55%
font-family: 'Comfortaa'
@media (max-width: $breakPointMD) @media (max-width: $breakPointMD)
max-width: 90% max-width: 90%
h3 h3
@ -311,10 +313,11 @@ const logoItems = computed(() => {
.webStrategy .webStrategy
padding: 4rem 0 3.5rem 0 padding: 4rem 0 3.5rem 0
h2 h2
font-size: clamp(1.6rem, 1rem + 1vw, 1.8rem) font-size: clamp(1.6rem, 1rem + 2vw, 1.8rem)
line-height: 150% line-height: 150%
font-family: 'Comfortaa'
h3 h3
font-size: clamp(1.2rem, .8rem + 1vw, 1.4rem) font-size: clamp(1.1rem, .75rem + 1vw, 1.25rem)
line-height: 150% line-height: 150%
img img
width: 80% width: 80%
@ -332,6 +335,8 @@ const logoItems = computed(() => {
justify-content: center justify-content: center
position: relative position: relative
padding: 3rem 0 padding: 3rem 0
h2
font-size: clamp(1.6rem, 1rem + 2vw, 1.8rem)
h3 h3
font-size: clamp(1.1rem, .8rem + 1vw, 1.2rem) font-size: clamp(1.1rem, .8rem + 1vw, 1.2rem)
line-height: 150% line-height: 150%
@ -352,22 +357,25 @@ const logoItems = computed(() => {
.compBox .compBox
background-image: linear-gradient(to bottom left, white, #FEDEE8, white) background-image: linear-gradient(to bottom left, white, #FEDEE8, white)
padding: 5rem 0 3rem 0 padding: 5rem 0 3rem 0
h2
font-family: 'Comfortaa'
h3 h3
line-height: 1.5 line-height: 1.5
p p
padding-right: 1rem padding-right: 1rem
.canDo .canDo
margin: 15vh 0 margin: 12vh 0
h2 h2
margin-bottom: 3.5rem margin-bottom: 3.5rem
font-size: clamp(1.6rem, 1rem + 2vw, 1.8rem)
.row .row
display: flex display: flex
flex-wrap: wrap flex-wrap: wrap
height: 100% height: 100%
align-items: stretch align-items: stretch
.innerBox .innerBox
width: 90% width: 100%
margin: 0 5% 0 5% margin: 0 5%
display: flex display: flex
flex-direction: column flex-direction: column
align-items: center align-items: center
@ -375,13 +383,12 @@ const logoItems = computed(() => {
background-image: linear-gradient(to bottom right, transparent , white ) background-image: linear-gradient(to bottom right, transparent , white )
box-shadow: 3px 3px 8px 1px $lightgrey box-shadow: 3px 3px 8px 1px $lightgrey
border-bottom-right-radius: 1rem border-bottom-right-radius: 1rem
padding: 0 2rem 1rem 2rem padding: 0 5% 1rem 5%
border-right: 1px solid lighten($beige, 0%) border-right: 1px solid lighten($beige, 0%)
border-bottom: 1px solid lighten($beige, 0%) border-bottom: 1px solid lighten($beige, 0%)
height: 100% height: 100%
.canDoItem .canDoItem
.imageBox .imageBox
margin: 2rem auto margin: 2rem auto
width: 100% width: 100%