feat: implement Dynamic Data Binding system and tags registry
All checks were successful
Production Build & Deploy / build-and-deploy (push) Successful in 1m0s

This commit is contained in:
Daniel S
2026-08-09 12:54:31 +02:00
parent b039aceb21
commit 074d314897
4 changed files with 96 additions and 12 deletions

View File

@@ -1,11 +1,17 @@
---
import type { SiteConfig, UIComponent } from '../../utils/configLoader';
import { resolveDynamicBinding } from '../../utils/dynamicDataBinding';
interface Props {
config: SiteConfig['hero'];
contextData?: Record<string, any>;
}
const { config } = Astro.props;
const { config, contextData = {} } = Astro.props;
const heroTitle = resolveDynamicBinding(config.title, contextData);
const heroSubtitle = resolveDynamicBinding(config.subtitle, contextData);
const heroBadge = resolveDynamicBinding(config.badge, contextData);
const elements = config.elements || [];
const badgeElements = elements.filter((el: UIComponent) => el.slotId === 'hero-badge-slot');
@@ -21,41 +27,41 @@ const customElements = elements.filter((el: UIComponent) => !el.slotId || el.slo
<!-- Slot 1: Badge Slot -->
<div id="hero-badge-slot" class="inline-flex flex-wrap items-center justify-center gap-2">
{config.badge && (
{heroBadge && (
<div class="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full border border-sky-400/20 bg-sky-400/10 text-sky-400 text-xs font-mono tracking-wide backdrop-blur-md">
<span>{config.badge}</span>
<span>{heroBadge}</span>
</div>
)}
{badgeElements.map(el => (
<span class={`inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full border border-sky-400/30 bg-sky-400/20 text-sky-300 text-xs font-mono ${el.animation0 ? `anim-${el.animation0}` : ''}`}>
{el.content}
{resolveDynamicBinding(el.content, contextData)}
</span>
))}
</div>
<h1 class="text-4xl sm:text-6xl font-extrabold tracking-tight text-white leading-tight">
{config.title}
{heroTitle}
</h1>
<p class="text-slate-400 text-xs sm:text-lg max-w-2xl mx-auto leading-relaxed font-normal">
{config.subtitle}
{heroSubtitle}
</p>
<!-- Slot 2: Action Buttons Slot -->
<div id="hero-actions-slot" class="flex flex-wrap items-center justify-center gap-4 pt-4">
{config.ctaPrimaryText && (
<a href={config.ctaPrimaryLink} class="px-8 py-3.5 rounded-xl bg-sky-400 text-slate-950 font-bold text-xs hover:bg-white transition-all duration-300 shadow-lg shadow-sky-400/20 active:scale-95">
{config.ctaPrimaryText}
{resolveDynamicBinding(config.ctaPrimaryText, contextData)}
</a>
)}
{config.ctaSecondaryText && (
<a href={config.ctaSecondaryLink} class="px-8 py-3.5 rounded-xl glass-panel text-white font-semibold text-xs glass-panel-hover active:scale-95">
{config.ctaSecondaryText}
{resolveDynamicBinding(config.ctaSecondaryText, contextData)}
</a>
)}
{actionElements.map(el => (
<a href={el.link || '#'} class={`px-8 py-3.5 rounded-xl font-bold text-xs transition-all active:scale-95 ${el.style === 'glass' ? 'glass-panel text-white' : 'bg-sky-400 text-slate-950 hover:bg-white'} ${el.animation0 ? `anim-${el.animation0}` : ''}`}>
{el.content}
{resolveDynamicBinding(el.content, contextData)}
</a>
))}
</div>
@@ -66,7 +72,7 @@ const customElements = elements.filter((el: UIComponent) => !el.slotId || el.slo
{customElements.map(el => (
<div class="p-4 rounded-xl glass-panel text-xs text-slate-300">
<span class="font-bold text-sky-400 block mb-1 uppercase text-[10px]">{el.type} [ID: {el.id}]</span>
{el.content}
{resolveDynamicBinding(el.content, contextData)}
</div>
))}
</div>

View File

@@ -223,8 +223,22 @@ const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('u
</div>
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Inhalt / Label</label>
<input type="text" id="new-el-content" placeholder="z.B. Jetzt Anfragen" class="w-full glass-panel bg-slate-900 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-400" />
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Dynamische Daten-Verknüpfung (Dynamic Tag)</label>
<select id="new-el-dynamic-tag" onchange="if(this.value){ document.getElementById('new-el-content').value = '{{' + this.value + '}}'; }" class="w-full glass-panel bg-slate-900 rounded-lg p-2 text-xs text-sky-400 focus:outline-none focus:border-sky-400 font-mono mb-2">
<option value="">-- Statischer Text (Kein Tag) --</option>
<option value="site.siteName">site.siteName (Website Name)</option>
<option value="site.metaDescription">site.metaDescription (Beschreibung)</option>
<option value="site.contactEmail">site.contactEmail (Kontakt Email)</option>
<option value="site.phone">site.phone (Telefonnummer)</option>
<option value="post.title">post.title (Beitragstitel)</option>
<option value="post.excerpt">post.excerpt (Beitragsauszug)</option>
<option value="author.name">author.name (Autor Name)</option>
</select>
</div>
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Inhalt / Label / Tag</label>
<input type="text" id="new-el-content" placeholder="z.B. Jetzt Anfragen oder {{site.siteName}}" class="w-full glass-panel bg-slate-900 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-400 font-mono" />
</div>
<button type="button" id="add-el-btn" class="w-full py-2.5 rounded-lg bg-sky-400/20 text-sky-400 border border-sky-400/30 font-bold text-xs uppercase tracking-wider hover:bg-sky-400 hover:text-slate-950 transition-all">

View File

@@ -0,0 +1,49 @@
export interface DynamicFieldDefinition {
key: string; // e.g. 'site.siteName', 'post.title', 'custom.clientName'
label: string;
category: 'site' | 'post' | 'author' | 'custom';
defaultValue: string;
}
export const DYNAMIC_FIELDS_REGISTRY: DynamicFieldDefinition[] = [
{ key: 'site.siteName', label: 'Website Name', category: 'site', defaultValue: 'N&D IT Solutions' },
{ key: 'site.metaDescription', label: 'Website Beschreibung', category: 'site', defaultValue: 'High-Performance Webdesign & Digital Solutions' },
{ key: 'site.contactEmail', label: 'Kontakt E-Mail', category: 'site', defaultValue: 'kontakt@ndsolutions.de' },
{ key: 'site.phone', label: 'Telefonnummer', category: 'site', defaultValue: '+49 (0) 123 456789' },
{ key: 'post.title', label: 'Beitragstitel (Dynamisch)', category: 'post', defaultValue: 'Beispiel Artikel' },
{ key: 'post.excerpt', label: 'Beitrags Auszug', category: 'post', defaultValue: 'Ein kurzer Auszug des Artikels...' },
{ key: 'author.name', label: 'Autor Name', category: 'author', defaultValue: 'N&D Team' }
];
/**
* Replaces dynamic binding tags like `{{site.siteName}}` or evaluates binding keys against a data context.
*/
export function resolveDynamicBinding(
content: string,
contextData: Record<string, any>
): string {
if (!content) return content;
return content.replace(/\{\{([^}]+)\}\}/g, (_, key) => {
const trimmedKey = key.trim();
const keys = trimmedKey.split('.');
let val: any = contextData;
for (const k of keys) {
if (val && typeof val === 'object' && k in val) {
val = val[k];
} else {
val = undefined;
break;
}
}
if (val !== undefined && val !== null) {
return String(val);
}
// Fallback to registry default if available
const regItem = DYNAMIC_FIELDS_REGISTRY.find(f => f.key === trimmedKey);
return regItem ? regItem.defaultValue : `{{${trimmedKey}}}`;
});
}