fix(andreknie.de): normalize SEO metadata and document structure #4

Merged
ankn merged 2 commits from fix/andreknie-seo into master 2026-07-28 22:54:26 +02:00
21 changed files with 161 additions and 33 deletions
Showing only changes of commit 215aacf4a4 - Show all commits
+10 -2
View File
@@ -4,8 +4,16 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/logos/favicon.png" /> <link rel="icon" type="image/png" href="/logos/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dr. André Knie — KI, Transformation & Leadership</title> <title>Dr. André Knie Digitale Souveränität und KI</title>
<meta name="description" content="Nüchterne KI-Expertise für Mittelstand, Verwaltung und Hochschulen. Souverän, praktisch, evidenzbasiert." /> <meta name="description" content="Dr. André Knie unterstützt Organisationen dabei, digitale Souveränität zu erlangen und sicher von KI zu profitieren." />
<link rel="canonical" href="https://andreknie.de/" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://andreknie.de/" />
<meta property="og:title" content="Dr. André Knie Digitale Souveränität und KI" />
<meta property="og:description" content="Dr. André Knie unterstützt Organisationen dabei, digitale Souveränität zu erlangen und sicher von KI zu profitieren." />
<meta property="og:image" content="https://andreknie.de/images/andre-knie.webp" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://andreknie.de/images/andre-knie.webp" />
<!-- Umami Analytics --> <!-- Umami Analytics -->
<script <script
defer defer
@@ -25,7 +25,7 @@ function readContentDir(subdir) {
return { return {
...data, ...data,
slug, slug,
body: marked(content), body: renderMarkdownContent(content),
_source: `${subdir}/${filename}` _source: `${subdir}/${filename}`
} }
} }
@@ -41,6 +41,18 @@ function readContentDir(subdir) {
.filter(Boolean) .filter(Boolean)
} }
export function renderMarkdownContent(content) {
const tokens = marked.lexer(content)
const firstContentToken = tokens.findIndex(token => token.type !== 'space')
const firstToken = tokens[firstContentToken]
if (firstToken?.type === 'heading' && firstToken.depth === 1) {
tokens.splice(firstContentToken, 1)
}
return marked.parser(tokens)
}
function readMetaFile(filename) { function readMetaFile(filename) {
const filepath = join(CONTENT_DIR, 'meta', filename) const filepath = join(CONTENT_DIR, 'meta', filename)
if (!existsSync(filepath)) return null if (!existsSync(filepath)) return null
@@ -0,0 +1,19 @@
import { describe, expect, it } from 'vitest'
import { renderMarkdownContent } from './vite-plugin-content.js'
describe('markdown content rendering', () => {
it('removes only an initial level-one heading', () => {
const html = renderMarkdownContent('# Duplicate title\n\nIntro\n\n## Section')
expect(html).not.toContain('<h1>Duplicate title</h1>')
expect(html).toContain('<p>Intro</p>')
expect(html).toContain('<h2>Section</h2>')
})
it('keeps content when no initial level-one heading exists', () => {
const html = renderMarkdownContent('Intro\n\n# Deliberate heading')
expect(html).toContain('<p>Intro</p>')
expect(html).toContain('<h1>Deliberate heading</h1>')
})
})
@@ -1,33 +1,32 @@
import React from 'react' import React from 'react'
import { Helmet } from 'react-helmet-async' import { Helmet } from 'react-helmet-async'
import { absoluteUrl, BRAND_SUFFIX, DEFAULT_DESCRIPTION, DEFAULT_IMAGE, DEFAULT_TITLE, normalizeTitle } from './seo.js'
export default function SEOHead({ title, description, url, image, type = 'website' }) { export default function SEOHead({ title, description, url, image, type = 'website', noindex = false }) {
const defaultTitle = "Dr. André Knie - Experte für Digitale Souveränität" const pageTitle = normalizeTitle(title)
const defaultDescription = "Dr. André Knie unterstützt Organisationen dabei, digitale Souveränität zu erlangen und sicher von KI zu profitieren." const siteTitle = pageTitle === DEFAULT_TITLE ? pageTitle : `${pageTitle}${BRAND_SUFFIX}`
const siteDescription = description || DEFAULT_DESCRIPTION
const siteTitle = title ? `${title} | Dr. André Knie` : defaultTitle const canonicalUrl = absoluteUrl(url || '/', '/')
const siteDesc = description || defaultDescription const imageUrl = absoluteUrl(image, DEFAULT_IMAGE)
const siteUrl = url ? `https://andreknie.de${url}` : "https://andreknie.de"
const siteImage = image || "https://andreknie.de/images/og-image.webp"
return ( return (
<Helmet> <Helmet>
<title>{siteTitle}</title> <title>{siteTitle}</title>
<meta name="description" content={siteDesc} /> <meta name="description" content={siteDescription} />
<link rel="canonical" href={canonicalUrl} />
{/* Open Graph / Facebook */} {noindex && <meta name="robots" content="noindex,follow" />}
<meta property="og:type" content={type} />
<meta property="og:url" content={siteUrl} />
<meta property="og:title" content={siteTitle} />
<meta property="og:description" content={siteDesc} />
<meta property="og:image" content={siteImage} />
{/* Twitter */} <meta property="og:type" content={type} />
<meta property="twitter:card" content="summary_large_image" /> <meta property="og:url" content={canonicalUrl} />
<meta property="twitter:url" content={siteUrl} /> <meta property="og:title" content={siteTitle} />
<meta property="twitter:title" content={siteTitle} /> <meta property="og:description" content={siteDescription} />
<meta property="twitter:description" content={siteDesc} /> <meta property="og:image" content={imageUrl} />
<meta property="twitter:image" content={siteImage} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content={canonicalUrl} />
<meta name="twitter:title" content={siteTitle} />
<meta name="twitter:description" content={siteDescription} />
<meta name="twitter:image" content={imageUrl} />
</Helmet> </Helmet>
) )
} }
@@ -0,0 +1,43 @@
import React from 'react'
import { afterEach, describe, expect, it } from 'vitest'
import { render, cleanup } from '@testing-library/react'
import { HelmetProvider } from 'react-helmet-async'
import SEOHead from './SEOHead'
import { BRAND_SUFFIX } from './seo.js'
afterEach(() => {
cleanup()
document.head.querySelectorAll('[data-rh="true"]').forEach(node => node.remove())
})
describe('SEOHead', () => {
it('renders one branded title, description, and canonical link', () => {
render(
<HelmetProvider>
<SEOHead title={`Artikel${BRAND_SUFFIX}`} description="Synthetic description" url="/artikel" />
</HelmetProvider>,
)
expect(document.head.querySelectorAll('title')).toHaveLength(1)
expect(document.title).toBe(`Artikel${BRAND_SUFFIX}`)
expect(document.title.match(/Dr\. André Knie/g)).toHaveLength(1)
expect(document.head.querySelectorAll('meta[name="description"]')).toHaveLength(1)
expect(document.head.querySelector('link[rel="canonical"]')?.getAttribute('href')).toBe('https://andreknie.de/artikel')
})
it('normalizes relative and absolute image URLs', () => {
const { rerender } = render(
<HelmetProvider>
<SEOHead title="Test" image="/images/andre-knie.webp" />
</HelmetProvider>,
)
expect(document.head.querySelector('meta[property="og:image"]')?.getAttribute('content')).toBe('https://andreknie.de/images/andre-knie.webp')
rerender(
<HelmetProvider>
<SEOHead title="Test" image="https://cdn.example.invalid/image.webp" />
</HelmetProvider>,
)
expect(document.head.querySelector('meta[property="og:image"]')?.getAttribute('content')).toBe('https://cdn.example.invalid/image.webp')
})
})
@@ -0,0 +1,16 @@
export const SITE_URL = 'https://andreknie.de'
export const BRAND_SUFFIX = ' | Dr. André Knie'
export const DEFAULT_TITLE = 'Dr. André Knie Digitale Souveränität und KI'
export const DEFAULT_DESCRIPTION = 'Dr. André Knie unterstützt Organisationen dabei, digitale Souveränität zu erlangen und sicher von KI zu profitieren.'
export const DEFAULT_IMAGE = '/images/andre-knie.webp'
export function absoluteUrl(value, fallback) {
const candidate = value || fallback
if (/^https?:\/\//i.test(candidate)) return candidate
return new URL(candidate.startsWith('/') ? candidate : `/${candidate}`, SITE_URL).href
}
export function normalizeTitle(title) {
if (!title) return DEFAULT_TITLE
return title.endsWith(BRAND_SUFFIX) ? title.slice(0, -BRAND_SUFFIX.length) : title
}
@@ -2,6 +2,7 @@ import React from 'react'
import { useMeta } from '../hooks/useContent' import { useMeta } from '../hooks/useContent'
import { ExternalLink, Globe } from 'lucide-react' import { ExternalLink, Globe } from 'lucide-react'
import SpeakerCvRequest from '../components/SpeakerCvRequest' import SpeakerCvRequest from '../components/SpeakerCvRequest'
import SEOHead from '../components/SEOHead'
export default function About() { export default function About() {
const { profile, about } = useMeta() const { profile, about } = useMeta()
@@ -12,6 +13,7 @@ export default function About() {
return ( return (
<main style={{ paddingTop: '120px', minHeight: '100vh' }}> <main style={{ paddingTop: '120px', minHeight: '100vh' }}>
<SEOHead title="Über mich" description="Über Dr. André Knie, seine Arbeit, Stationen und Perspektive auf KI und digitale Souveränität." url="/ueber-mich" />
<div className="app-container" style={{ maxWidth: '800px' }}> <div className="app-container" style={{ maxWidth: '800px' }}>
<div className="section-header"> <div className="section-header">
<h1 className="section-title">Über <span className="text-gradient">mich</span></h1> <h1 className="section-title">Über <span className="text-gradient">mich</span></h1>
@@ -11,10 +11,10 @@ export default function ArticleSingle() {
if (!post) { if (!post) {
return ( return (
<div className="page-container app-container" style={{ textAlign: 'center', paddingTop: '150px' }}> <main className="page-container app-container" style={{ textAlign: 'center', paddingTop: '150px' }}>
<h2>Artikel nicht gefunden</h2> <h2>Artikel nicht gefunden</h2>
<Link to="/artikel" className="primary-button" style={{ marginTop: '20px' }}>Zurück zur Übersicht</Link> <Link to="/artikel" className="primary-button" style={{ marginTop: '20px' }}>Zurück zur Übersicht</Link>
</div> </main>
) )
} }
@@ -23,8 +23,10 @@ export default function ArticleSingle() {
: '' : ''
return ( return (
<div className="page-container article-single-page"> <main className="page-container article-single-page">
<SEOHead <SEOHead
url={`/artikel/${post.slug}`}
type="article"
title={`${post.title} | Dr. André Knie`} title={`${post.title} | Dr. André Knie`}
description={post.excerpt || post.title} description={post.excerpt || post.title}
/> />
@@ -48,6 +50,6 @@ export default function ArticleSingle() {
/> />
</article> </article>
</div> </div>
</div> </main>
) )
} }
@@ -20,8 +20,9 @@ export default function Articles() {
}) })
return ( return (
<div className="page-container articles-page"> <main className="page-container articles-page">
<SEOHead <SEOHead
url="/artikel"
title="Artikel & Beiträge | Dr. André Knie" title="Artikel & Beiträge | Dr. André Knie"
description="Aktuelle Artikel und LinkedIn-Beiträge zu den Themen Softwareentwicklung, Leadership und Technologie." description="Aktuelle Artikel und LinkedIn-Beiträge zu den Themen Softwareentwicklung, Leadership und Technologie."
/> />
@@ -102,6 +103,6 @@ export default function Articles() {
<p className="no-articles">Keine Artikel für diese Suche gefunden.</p> <p className="no-articles">Keine Artikel für diese Suche gefunden.</p>
)} )}
</section> </section>
</div> </main>
) )
} }
@@ -13,6 +13,8 @@ export default function Confirmation() {
<SEOHead <SEOHead
title={isSuccess ? "Vielen Dank" : "Ein Fehler ist aufgetreten"} title={isSuccess ? "Vielen Dank" : "Ein Fehler ist aufgetreten"}
description={isSuccess ? "Ihre Nachricht wurde erfolgreich gesendet." : "Es gab ein Problem beim Senden Ihrer Nachricht."} description={isSuccess ? "Ihre Nachricht wurde erfolgreich gesendet." : "Es gab ein Problem beim Senden Ihrer Nachricht."}
url="/bestaetigung"
noindex
/> />
<main style={{ paddingTop: '120px', minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <main style={{ paddingTop: '120px', minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div className="app-container" style={{ textAlign: 'center', maxWidth: '600px' }}> <div className="app-container" style={{ textAlign: 'center', maxWidth: '600px' }}>
@@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import { useContent, useMeta } from '../hooks/useContent' import { useContent, useMeta } from '../hooks/useContent'
import ServiceAccordion from '../components/ServiceAccordion' import ServiceAccordion from '../components/ServiceAccordion'
import SEOHead from '../components/SEOHead'
export default function Consulting() { export default function Consulting() {
const { items } = useContent('consulting', { pageSize: 10 }) const { items } = useContent('consulting', { pageSize: 10 })
@@ -8,6 +9,7 @@ export default function Consulting() {
return ( return (
<main style={{ paddingTop: '120px', minHeight: '100vh' }}> <main style={{ paddingTop: '120px', minHeight: '100vh' }}>
<SEOHead title="Beratung" description="Beratung zu KI-Strategie, digitaler Souveränität und praktischer Transformation." url="/consulting" />
<div className="app-container"> <div className="app-container">
<div className="section-header"> <div className="section-header">
<h1 className="section-title"><span className="text-gradient">Leistungen</span></h1> <h1 className="section-title"><span className="text-gradient">Leistungen</span></h1>
@@ -1,6 +1,7 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import { useFormSubmit } from '../hooks/useFormSubmit' import { useFormSubmit } from '../hooks/useFormSubmit'
import { validateContact } from '../utils/validation' import { validateContact } from '../utils/validation'
import SEOHead from '../components/SEOHead'
export default function Contact() { export default function Contact() {
const [form, setForm] = useState({ name: '', email: '', message: '', company_website: '' }) const [form, setForm] = useState({ name: '', email: '', message: '', company_website: '' })
@@ -18,6 +19,7 @@ export default function Contact() {
if (success) { if (success) {
return ( return (
<main style={{ paddingTop: '120px', minHeight: '100vh' }}> <main style={{ paddingTop: '120px', minHeight: '100vh' }}>
<SEOHead title="Kontakt" description="Nimm Kontakt zu Dr. André Knie auf." url="/kontakt" />
<div className="app-container" style={{ textAlign: 'center', maxWidth: '600px' }}> <div className="app-container" style={{ textAlign: 'center', maxWidth: '600px' }}>
<h1 style={{ marginBottom: '16px' }}>Nachricht gesendet</h1> <h1 style={{ marginBottom: '16px' }}>Nachricht gesendet</h1>
<p style={{ color: 'var(--text-secondary)' }}>Du erhältst eine Bestätigungs-E-Mail. Bitte klicke den Link darin, damit deine Nachricht weitergeleitet wird.</p> <p style={{ color: 'var(--text-secondary)' }}>Du erhältst eine Bestätigungs-E-Mail. Bitte klicke den Link darin, damit deine Nachricht weitergeleitet wird.</p>
@@ -28,6 +30,7 @@ export default function Contact() {
return ( return (
<main style={{ paddingTop: '120px', minHeight: '100vh' }}> <main style={{ paddingTop: '120px', minHeight: '100vh' }}>
<SEOHead title="Kontakt" description="Nimm Kontakt zu Dr. André Knie auf." url="/kontakt" />
<div className="app-container" style={{ maxWidth: '600px' }}> <div className="app-container" style={{ maxWidth: '600px' }}>
<div className="section-header"> <div className="section-header">
<h1 className="section-title"><span className="text-gradient">Kontakt</span></h1> <h1 className="section-title"><span className="text-gradient">Kontakt</span></h1>
@@ -1,8 +1,10 @@
import React from 'react' import React from 'react'
import SEOHead from '../components/SEOHead'
export default function Datenschutz() { export default function Datenschutz() {
return ( return (
<main style={{ paddingTop: '120px', minHeight: '100vh' }}> <main style={{ paddingTop: '120px', minHeight: '100vh' }}>
<SEOHead title="Datenschutz" description="Datenschutzerklärung von andreknie.de." url="/datenschutz" />
<div className="app-container" style={{ maxWidth: '700px' }}> <div className="app-container" style={{ maxWidth: '700px' }}>
<h1 style={{ marginBottom: '32px' }}>Datenschutzerklärung</h1> <h1 style={{ marginBottom: '32px' }}>Datenschutzerklärung</h1>
<div style={{ color: 'var(--text-secondary)', lineHeight: 1.8 }}> <div style={{ color: 'var(--text-secondary)', lineHeight: 1.8 }}>
@@ -6,6 +6,7 @@ import UpcomingEvents from '../components/UpcomingEvents'
import StatsCube from '../components/StatsCube' import StatsCube from '../components/StatsCube'
import DotCloudIcon from '../components/DotCloudIcon' import DotCloudIcon from '../components/DotCloudIcon'
import PhotoBand from '../components/PhotoBand' import PhotoBand from '../components/PhotoBand'
import SEOHead from '../components/SEOHead'
import './Home.css' import './Home.css'
const PHOTO_BAND_IMAGES = [ const PHOTO_BAND_IMAGES = [
@@ -21,6 +22,7 @@ export default function Home() {
return ( return (
<main> <main>
<SEOHead url="/" />
{/* Hero */} {/* Hero */}
<section className="hero"> <section className="hero">
<div className="app-container hero-content"> <div className="app-container hero-content">
@@ -1,8 +1,10 @@
import React from 'react' import React from 'react'
import SEOHead from '../components/SEOHead'
export default function Impressum() { export default function Impressum() {
return ( return (
<main style={{ paddingTop: '120px', minHeight: '100vh' }}> <main style={{ paddingTop: '120px', minHeight: '100vh' }}>
<SEOHead title="Impressum" description="Impressum von andreknie.de." url="/impressum" />
<div className="app-container" style={{ maxWidth: '700px' }}> <div className="app-container" style={{ maxWidth: '700px' }}>
<h1 style={{ marginBottom: '32px' }}>Impressum</h1> <h1 style={{ marginBottom: '32px' }}>Impressum</h1>
<div style={{ color: 'var(--text-secondary)', lineHeight: 1.8 }}> <div style={{ color: 'var(--text-secondary)', lineHeight: 1.8 }}>
@@ -4,6 +4,7 @@ import { Search } from 'lucide-react'
import { useContent } from '../hooks/useContent' import { useContent } from '../hooks/useContent'
import { formatDate } from '../utils/formatDate' import { formatDate } from '../utils/formatDate'
import { decodeHtml } from '../utils/decodeHtml' import { decodeHtml } from '../utils/decodeHtml'
import SEOHead from '../components/SEOHead'
export default function Kniepunkt() { export default function Kniepunkt() {
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
@@ -21,6 +22,7 @@ export default function Kniepunkt() {
return ( return (
<main style={{ paddingTop: '120px', minHeight: '100vh' }}> <main style={{ paddingTop: '120px', minHeight: '100vh' }}>
<SEOHead title="Kniepunkt" description="Die Kniepunkt-Kolumne von Dr. André Knie über KI, Technologie und die Absurditäten des digitalen Wandels." url="/kniepunkt" />
<div className="app-container"> <div className="app-container">
<div className="section-header"> <div className="section-header">
<h1 className="section-title"><span className="text-gradient">Kniepunkt</span></h1> <h1 className="section-title"><span className="text-gradient">Kniepunkt</span></h1>
@@ -2,6 +2,7 @@ import React from 'react'
import { useParams, Link } from 'react-router-dom' import { useParams, Link } from 'react-router-dom'
import { useContentItem } from '../hooks/useContent' import { useContentItem } from '../hooks/useContent'
import { formatDate } from '../utils/formatDate' import { formatDate } from '../utils/formatDate'
import SEOHead from '../components/SEOHead'
export default function KniepunktIssue() { export default function KniepunktIssue() {
const { slug } = useParams() const { slug } = useParams()
@@ -10,6 +11,7 @@ export default function KniepunktIssue() {
if (!issue) { if (!issue) {
return ( return (
<main style={{ paddingTop: '120px', minHeight: '100vh' }}> <main style={{ paddingTop: '120px', minHeight: '100vh' }}>
<SEOHead title="Kniepunkt-Ausgabe nicht gefunden" description="Die angeforderte Kniepunkt-Ausgabe wurde nicht gefunden." noindex url={`/kniepunkt/${slug}`} />
<div className="app-container" style={{ textAlign: 'center' }}> <div className="app-container" style={{ textAlign: 'center' }}>
<h1>Ausgabe nicht gefunden</h1> <h1>Ausgabe nicht gefunden</h1>
<p style={{ color: 'var(--text-secondary)' }}>Diese Kniepunkt-Ausgabe existiert nicht.</p> <p style={{ color: 'var(--text-secondary)' }}>Diese Kniepunkt-Ausgabe existiert nicht.</p>
@@ -21,6 +23,7 @@ export default function KniepunktIssue() {
return ( return (
<main style={{ paddingTop: '120px', minHeight: '100vh' }}> <main style={{ paddingTop: '120px', minHeight: '100vh' }}>
<SEOHead title={issue.title} description={issue.summary || issue.title} url={`/kniepunkt/${issue.slug}`} type="article" image={issue.image} />
<div className="app-container" style={{ maxWidth: '700px' }}> <div className="app-container" style={{ maxWidth: '700px' }}>
<Link to="/kniepunkt" style={{ color: 'var(--text-secondary)', fontSize: '0.9rem', marginBottom: '24px', display: 'inline-block' }}>&larr; Alle Ausgaben</Link> <Link to="/kniepunkt" style={{ color: 'var(--text-secondary)', fontSize: '0.9rem', marginBottom: '24px', display: 'inline-block' }}>&larr; Alle Ausgaben</Link>
<h1 style={{ marginBottom: '12px' }}>{issue.title}</h1> <h1 style={{ marginBottom: '12px' }}>{issue.title}</h1>
@@ -1,9 +1,11 @@
import React from 'react' import React from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import SEOHead from '../components/SEOHead'
export default function NotFound() { export default function NotFound() {
return ( return (
<main style={{ paddingTop: '120px', minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <main style={{ paddingTop: '120px', minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<SEOHead title="Seite nicht gefunden" description="Die angeforderte Seite wurde nicht gefunden." noindex url="/404" />
<div className="app-container" style={{ textAlign: 'center' }}> <div className="app-container" style={{ textAlign: 'center' }}>
<h1 style={{ fontSize: '6rem', marginBottom: '16px' }}><span className="text-gradient">404</span></h1> <h1 style={{ fontSize: '6rem', marginBottom: '16px' }}><span className="text-gradient">404</span></h1>
<p style={{ color: 'var(--text-secondary)', fontSize: '1.2rem', marginBottom: '32px' }}>Diese Seite existiert nicht.</p> <p style={{ color: 'var(--text-secondary)', fontSize: '1.2rem', marginBottom: '32px' }}>Diese Seite existiert nicht.</p>
@@ -2,6 +2,7 @@ import React from 'react'
import { useContent } from '../hooks/useContent' import { useContent } from '../hooks/useContent'
import { formatDate } from '../utils/formatDate' import { formatDate } from '../utils/formatDate'
import { ExternalLink } from 'lucide-react' import { ExternalLink } from 'lucide-react'
import SEOHead from '../components/SEOHead'
export default function Podcast() { export default function Podcast() {
const { items: allEpisodes } = useContent('podcast', { visibility: 'all', pageSize: 100 }) const { items: allEpisodes } = useContent('podcast', { visibility: 'all', pageSize: 100 })
@@ -13,6 +14,7 @@ export default function Podcast() {
return ( return (
<main style={{ paddingTop: '120px', minHeight: '100vh' }}> <main style={{ paddingTop: '120px', minHeight: '100vh' }}>
<SEOHead title="Podcast" description="Podcasts, Gastauftritte und Gespräche von und mit Dr. André Knie über KI, Transformation und Leadership." url="/podcast" />
<div className="app-container" style={{ maxWidth: '800px', margin: '0 auto' }}> <div className="app-container" style={{ maxWidth: '800px', margin: '0 auto' }}>
<div className="section-header"> <div className="section-header">
<h1 className="section-title"><span className="text-gradient">Podcast</span></h1> <h1 className="section-title"><span className="text-gradient">Podcast</span></h1>
@@ -2,6 +2,7 @@ import React, { useState } from 'react'
import { useContent } from '../hooks/useContent' import { useContent } from '../hooks/useContent'
import { useFormSubmit } from '../hooks/useFormSubmit' import { useFormSubmit } from '../hooks/useFormSubmit'
import { validateResourceDownload } from '../utils/validation' import { validateResourceDownload } from '../utils/validation'
import SEOHead from '../components/SEOHead'
const inputStyle = { const inputStyle = {
width: '100%', width: '100%',
@@ -80,6 +81,7 @@ export default function Resources() {
return ( return (
<main style={{ paddingTop: '120px', minHeight: '100vh' }}> <main style={{ paddingTop: '120px', minHeight: '100vh' }}>
<SEOHead title="Ressourcen" description="Ausgewählte Materialien und Profile von Dr. André Knie auf Anfrage." url="/resources" />
<div className="app-container"> <div className="app-container">
<div className="section-header"> <div className="section-header">
<h1 className="section-title"><span className="text-gradient">Ressourcen</span></h1> <h1 className="section-title"><span className="text-gradient">Ressourcen</span></h1>
@@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import { useContent, useMeta } from '../hooks/useContent' import { useContent, useMeta } from '../hooks/useContent'
import SpeakingAccordion from '../components/SpeakingAccordion' import SpeakingAccordion from '../components/SpeakingAccordion'
import SEOHead from '../components/SEOHead'
export default function Speaking() { export default function Speaking() {
const { items: allTalks } = useContent('talks', { visibility: 'all', pageSize: 100 }) const { items: allTalks } = useContent('talks', { visibility: 'all', pageSize: 100 })
@@ -9,6 +10,7 @@ export default function Speaking() {
return ( return (
<main style={{ paddingTop: '120px', minHeight: '100vh' }}> <main style={{ paddingTop: '120px', minHeight: '100vh' }}>
<SEOHead title="Speaking" description="Keynotes, Workshops und Panels von Dr. André Knie zu KI, Transformation und Leadership." url="/speaking" />
<div className="app-container"> <div className="app-container">
<div className="section-header"> <div className="section-header">
<h1 className="section-title"><span className="text-gradient">Speaking</span></h1> <h1 className="section-title"><span className="text-gradient">Speaking</span></h1>