Skip to content

Commit b6ca93b

Browse files
raifdmuellerclaude
andcommitted
feat: add HMZE podcast reference (footer badge + DE About video)
- Footer: new "As seen on / Zu Gast bei" row with HMZE logo linking to https://www.youtube.com/watch?v=rQj-B3VTx48; shown on both languages, labelled "(DE)" since the podcast is German. - DE About page: click-to-load YouTube facade above "Was sind Semantic Anchors?". Only the static thumbnail loads until the user clicks; after activation the iframe uses youtube-nocookie.com. EN About stays unchanged — a 60-minute German explainer does not belong as the primary intro on the English page. - youtube-facade.js utility: small hydrator scanning doc content for button.youtube-facade[data-video-id]. Keeps the embed DSGVO-safe without consent banners. - New i18n keys: footer.asSeenOn, footer.hmzeAlt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8956814 commit b6ca93b

9 files changed

Lines changed: 218 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ node_modules/
22
*.png
33
!website/public/logo.png
44
!website/public/icon.png
5+
!website/public/hmze-logo.png
56
!docs/workflow-diagram.svg
67
.playwright-mcp/
78
*:Zone.Identifier

docs/about.de.adoc

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,38 @@
22
:toc:
33
:toc-placement: preamble
44

5+
++++
6+
<figure class="youtube-facade-wrapper">
7+
<button
8+
type="button"
9+
class="youtube-facade"
10+
data-video-id="rQj-B3VTx48"
11+
data-video-title="HMZE Podcast Episode 58: Beyond Vibe Coding — Semantic Anchors als Magic Spells"
12+
aria-label="Video laden und abspielen: HMZE Podcast Episode 58 — Beyond Vibe Coding. Erst beim Klick wird YouTube geladen."
13+
>
14+
<img
15+
src="https://i.ytimg.com/vi/rQj-B3VTx48/maxresdefault.jpg"
16+
alt=""
17+
loading="lazy"
18+
/>
19+
<span class="youtube-facade-play">
20+
<span class="youtube-facade-play-icon">
21+
<svg fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
22+
<path d="M8 5v14l11-7z"/>
23+
</svg>
24+
</span>
25+
</span>
26+
<span class="youtube-facade-title">
27+
<strong>HMZE Podcast #58 — Beyond Vibe Coding: Semantic Anchors als Magic Spells</strong>
28+
<em>Erst beim Klick wird YouTube geladen (DSGVO)</em>
29+
</span>
30+
</button>
31+
<figcaption>
32+
Ralf D. Müller erklärt Semantic Anchors im HMZE-Podcast (April 2026)
33+
</figcaption>
34+
</figure>
35+
++++
36+
537
== Was sind Semantic Anchors?
638

739
*Semantic Anchors* (semantische Anker) sind klar definierte Begriffe, Methodologien und Frameworks, die als Referenzpunkte bei der Kommunikation mit Large Language Models (LLMs) dienen. Sie fungieren als gemeinsames Vokabular, das spezifische, kontextreiche Wissensbereiche im Trainingsdatensatz eines LLMs aktiviert.

website/public/hmze-logo.png

7.01 KB
Loading

website/src/components/doc-page.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { i18n } from '../i18n.js'
2+
import { hydrateYouTubeFacades } from '../utils/youtube-facade.js'
23

34
/**
45
* Render a documentation page shell (content loaded async)
@@ -59,6 +60,10 @@ export async function loadDocContent(docPath) {
5960
link.setAttribute('target', '_blank')
6061
link.setAttribute('rel', 'noopener noreferrer')
6162
})
63+
64+
// Attach click-to-load handlers for any YouTube placeholders in the doc.
65+
// Keeps us DSGVO-compliant: YouTube is only contacted after user consent.
66+
hydrateYouTubeFacades(contentEl)
6267
} catch (error) {
6368
console.error('Failed to load documentation:', error)
6469
contentEl.innerHTML = `

website/src/components/footer.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,26 @@ export function renderFooter(version) {
6464
<span class="text-gray-300 dark:text-gray-600">|</span>
6565
<span class="text-xs text-[var(--color-text-secondary)]">v${version}</span>
6666
</div>
67+
<div class="flex flex-wrap items-center justify-center gap-2 pt-1">
68+
<span class="text-xs text-[var(--color-text-secondary)]" data-i18n="footer.asSeenOn">${i18n.t('footer.asSeenOn')}</span>
69+
<a
70+
href="https://www.youtube.com/watch?v=rQj-B3VTx48"
71+
target="_blank"
72+
rel="noopener noreferrer"
73+
class="inline-flex items-center gap-1.5 hover:opacity-80 transition-opacity"
74+
title="${i18n.t('footer.hmzeAlt')}"
75+
>
76+
<img
77+
src="${import.meta.env.BASE_URL}hmze-logo.png"
78+
alt="HMZE Podcast"
79+
width="28"
80+
height="24"
81+
class="h-6 w-auto"
82+
loading="lazy"
83+
/>
84+
<span class="text-xs text-[var(--color-text-secondary)]">HMZE <span class="opacity-60">(DE)</span></span>
85+
</a>
86+
</div>
6787
</div>
6888
</div>
6989
</footer>

website/src/styles/main.css

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,3 +406,117 @@ body {
406406
.sub-anchor-link:hover {
407407
text-decoration: underline;
408408
}
409+
410+
/* ---- YouTube click-to-load facade ---- */
411+
.youtube-facade-wrapper {
412+
margin: 1.5rem auto;
413+
max-width: 640px;
414+
}
415+
416+
.youtube-facade {
417+
position: relative;
418+
display: block;
419+
width: 100%;
420+
aspect-ratio: 16 / 9;
421+
border: 0;
422+
border-radius: 0.75rem;
423+
overflow: hidden;
424+
background: #000;
425+
cursor: pointer;
426+
padding: 0;
427+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2);
428+
}
429+
430+
.youtube-facade:focus-visible {
431+
outline: 2px solid var(--color-primary);
432+
outline-offset: 2px;
433+
}
434+
435+
.youtube-facade img {
436+
width: 100%;
437+
height: 100%;
438+
object-fit: cover;
439+
transition: transform 0.3s ease;
440+
}
441+
442+
.youtube-facade:hover img {
443+
transform: scale(1.02);
444+
}
445+
446+
.youtube-facade-play {
447+
position: absolute;
448+
inset: 0;
449+
display: flex;
450+
align-items: center;
451+
justify-content: center;
452+
}
453+
454+
.youtube-facade-play-icon {
455+
display: flex;
456+
align-items: center;
457+
justify-content: center;
458+
width: 5rem;
459+
height: 5rem;
460+
border-radius: 9999px;
461+
background: rgba(220, 38, 38, 0.9);
462+
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
463+
transition: background 0.2s ease;
464+
}
465+
466+
.youtube-facade:hover .youtube-facade-play-icon {
467+
background: rgb(220, 38, 38);
468+
}
469+
470+
.youtube-facade-play-icon svg {
471+
width: 2.5rem;
472+
height: 2.5rem;
473+
color: #fff;
474+
transform: translateX(3px);
475+
}
476+
477+
.youtube-facade-title {
478+
position: absolute;
479+
left: 0;
480+
right: 0;
481+
bottom: 0;
482+
padding: 2rem 1rem 0.75rem;
483+
background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent);
484+
color: #fff;
485+
text-align: left;
486+
}
487+
488+
.youtube-facade-title strong {
489+
display: block;
490+
font-size: 0.95rem;
491+
font-weight: 500;
492+
}
493+
494+
.youtube-facade-title em {
495+
display: block;
496+
font-size: 0.75rem;
497+
font-style: normal;
498+
opacity: 0.75;
499+
margin-top: 0.25rem;
500+
}
501+
502+
.youtube-facade-wrapper figcaption {
503+
margin-top: 0.5rem;
504+
text-align: center;
505+
font-size: 0.875rem;
506+
color: var(--color-text-secondary);
507+
}
508+
509+
.youtube-loaded {
510+
position: relative;
511+
aspect-ratio: 16 / 9;
512+
border-radius: 0.75rem;
513+
overflow: hidden;
514+
}
515+
516+
.youtube-loaded iframe {
517+
position: absolute;
518+
inset: 0;
519+
width: 100%;
520+
height: 100%;
521+
border: 0;
522+
}

website/src/translations/de.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
"footer.rejectedProposals": "Abgelehnte Vorschläge",
3838
"footer.evaluations": "Evaluierungen",
3939
"footer.augmentedPatterns": "Augmented Coding Patterns",
40+
"footer.asSeenOn": "Zu Gast bei",
41+
"footer.hmzeAlt": "HMZE Podcast — Beyond Vibe Coding",
4042
"categories.communication-presentation": "Kommunikation & Präsentation",
4143
"categories.design-principles": "Design-Prinzipien & Muster",
4244
"categories.development-workflow": "Entwicklungs-Workflow",

website/src/translations/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
"footer.rejectedProposals": "Rejected Proposals",
3838
"footer.evaluations": "Evaluations",
3939
"footer.augmentedPatterns": "Augmented Coding Patterns",
40+
"footer.asSeenOn": "As seen on",
41+
"footer.hmzeAlt": "HMZE Podcast — Beyond Vibe Coding (German)",
4042
"categories.communication-presentation": "Communication & Presentation",
4143
"categories.design-principles": "Design Principles & Patterns",
4244
"categories.development-workflow": "Development Workflow",
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* YouTube click-to-load facade.
3+
*
4+
* Finds <button class="youtube-facade" data-video-id="..."> elements in a
5+
* container and replaces them with a real YouTube iframe on click. Until the
6+
* user clicks, the browser never contacts youtube.com or youtube-nocookie.com,
7+
* so no tracking cookies are set. This keeps the embed DSGVO-compliant.
8+
*/
9+
10+
const VIDEO_ID_RE = /^[A-Za-z0-9_-]{6,20}$/
11+
12+
export function hydrateYouTubeFacades(root = document) {
13+
const buttons = root.querySelectorAll('button.youtube-facade[data-video-id]')
14+
buttons.forEach((button) => {
15+
if (button.dataset.hydrated === 'true') return
16+
button.dataset.hydrated = 'true'
17+
button.addEventListener('click', () => swapWithIframe(button), { once: true })
18+
})
19+
}
20+
21+
function swapWithIframe(button) {
22+
const videoId = button.dataset.videoId
23+
if (!videoId || !VIDEO_ID_RE.test(videoId)) return
24+
25+
const title = button.dataset.videoTitle || 'YouTube video'
26+
const iframe = document.createElement('iframe')
27+
// youtube-nocookie.com does not set tracking cookies unless playback starts.
28+
// Autoplay is safe here because activation is a user gesture (the click).
29+
iframe.src = `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&rel=0`
30+
iframe.title = title
31+
iframe.allow =
32+
'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'
33+
iframe.setAttribute('allowfullscreen', '')
34+
iframe.className = 'absolute inset-0 w-full h-full border-0'
35+
36+
const wrapper = document.createElement('div')
37+
wrapper.className = button.className.replace('youtube-facade', 'youtube-loaded')
38+
wrapper.style.position = 'relative'
39+
wrapper.appendChild(iframe)
40+
41+
button.replaceWith(wrapper)
42+
}

0 commit comments

Comments
 (0)