fix(andreknie.de): normalize SEO metadata and document structure #4
@@ -4,16 +4,19 @@
|
|||||||
<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 – Digitale Souveränität und KI</title>
|
<title data-seo-fallback>Dr. André Knie – Digitale Souveränität und KI</title>
|
||||||
<meta name="description" content="Dr. André Knie unterstützt Organisationen dabei, digitale Souveränität zu erlangen und sicher von KI zu profitieren." />
|
<meta data-seo-fallback 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/" />
|
<link data-seo-fallback rel="canonical" href="https://andreknie.de/" />
|
||||||
<meta property="og:type" content="website" />
|
<meta data-seo-fallback property="og:type" content="website" />
|
||||||
<meta property="og:url" content="https://andreknie.de/" />
|
<meta data-seo-fallback property="og:url" content="https://andreknie.de/" />
|
||||||
<meta property="og:title" content="Dr. André Knie – Digitale Souveränität und KI" />
|
<meta data-seo-fallback 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 data-seo-fallback 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 data-seo-fallback property="og:image" content="https://andreknie.de/images/andre-knie.webp" />
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta data-seo-fallback name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:image" content="https://andreknie.de/images/andre-knie.webp" />
|
<meta data-seo-fallback name="twitter:url" content="https://andreknie.de/" />
|
||||||
|
<meta data-seo-fallback name="twitter:title" content="Dr. André Knie – Digitale Souveränität und KI" />
|
||||||
|
<meta data-seo-fallback name="twitter:description" content="Dr. André Knie unterstützt Organisationen dabei, digitale Souveränität zu erlangen und sicher von KI zu profitieren." />
|
||||||
|
<meta data-seo-fallback name="twitter:image" content="https://andreknie.de/images/andre-knie.webp" />
|
||||||
<!-- Umami Analytics -->
|
<!-- Umami Analytics -->
|
||||||
<script
|
<script
|
||||||
defer
|
defer
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import React, { useEffect } 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'
|
import { absoluteUrl, BRAND_SUFFIX, DEFAULT_DESCRIPTION, DEFAULT_IMAGE, DEFAULT_TITLE, normalizeTitle } from './seo.js'
|
||||||
|
|
||||||
@@ -9,6 +9,10 @@ export default function SEOHead({ title, description, url, image, type = 'websit
|
|||||||
const canonicalUrl = absoluteUrl(url || '/', '/')
|
const canonicalUrl = absoluteUrl(url || '/', '/')
|
||||||
const imageUrl = absoluteUrl(image, DEFAULT_IMAGE)
|
const imageUrl = absoluteUrl(image, DEFAULT_IMAGE)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.head.querySelectorAll('[data-seo-fallback]').forEach(node => node.remove())
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{siteTitle}</title>
|
<title>{siteTitle}</title>
|
||||||
|
|||||||
@@ -25,6 +25,25 @@ describe('SEOHead', () => {
|
|||||||
expect(document.head.querySelector('link[rel="canonical"]')?.getAttribute('href')).toBe('https://andreknie.de/artikel')
|
expect(document.head.querySelector('link[rel="canonical"]')?.getAttribute('href')).toBe('https://andreknie.de/artikel')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('replaces static fallback metadata instead of duplicating it', () => {
|
||||||
|
document.head.insertAdjacentHTML('beforeend', `
|
||||||
|
<title data-seo-fallback>Fallback</title>
|
||||||
|
<meta data-seo-fallback name="description" content="Fallback description">
|
||||||
|
<link data-seo-fallback rel="canonical" href="https://andreknie.de/">
|
||||||
|
`)
|
||||||
|
|
||||||
|
render(
|
||||||
|
<HelmetProvider>
|
||||||
|
<SEOHead title="Artikel" description="Article description" url="/artikel" />
|
||||||
|
</HelmetProvider>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(document.head.querySelectorAll('[data-seo-fallback]')).toHaveLength(0)
|
||||||
|
expect(document.head.querySelectorAll('title')).toHaveLength(1)
|
||||||
|
expect(document.head.querySelectorAll('meta[name="description"]')).toHaveLength(1)
|
||||||
|
expect(document.head.querySelectorAll('link[rel="canonical"]')).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
it('normalizes relative and absolute image URLs', () => {
|
it('normalizes relative and absolute image URLs', () => {
|
||||||
const { rerender } = render(
|
const { rerender } = render(
|
||||||
<HelmetProvider>
|
<HelmetProvider>
|
||||||
|
|||||||
@@ -12,7 +12,13 @@ export default function ArticleSingle() {
|
|||||||
if (!post) {
|
if (!post) {
|
||||||
return (
|
return (
|
||||||
<main 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>
|
<SEOHead
|
||||||
|
title="Artikel nicht gefunden"
|
||||||
|
description="Der angeforderte Artikel wurde nicht gefunden."
|
||||||
|
url={`/artikel/${slug}`}
|
||||||
|
noindex
|
||||||
|
/>
|
||||||
|
<h1>Artikel nicht gefunden</h1>
|
||||||
<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>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
@@ -27,7 +33,7 @@ export default function ArticleSingle() {
|
|||||||
<SEOHead
|
<SEOHead
|
||||||
url={`/artikel/${post.slug}`}
|
url={`/artikel/${post.slug}`}
|
||||||
type="article"
|
type="article"
|
||||||
title={`${post.title} | Dr. André Knie`}
|
title={post.title}
|
||||||
description={post.excerpt || post.title}
|
description={post.excerpt || post.title}
|
||||||
/>
|
/>
|
||||||
<div className="app-container" style={{ maxWidth: '800px', margin: '0 auto', paddingTop: '120px' }}>
|
<div className="app-container" style={{ maxWidth: '800px', margin: '0 auto', paddingTop: '120px' }}>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default function Articles() {
|
|||||||
<main className="page-container articles-page">
|
<main className="page-container articles-page">
|
||||||
<SEOHead
|
<SEOHead
|
||||||
url="/artikel"
|
url="/artikel"
|
||||||
title="Artikel & Beiträge | Dr. André Knie"
|
title="Artikel & Beiträge"
|
||||||
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."
|
||||||
/>
|
/>
|
||||||
<header className="page-header">
|
<header className="page-header">
|
||||||
|
|||||||
Reference in New Issue
Block a user