Package Exports
- @startnext/chrome
- @startnext/chrome/render
Readme
@startnext/chrome
Startnext Header/Footer Web Components
Wiederverwendbare Header- und Footer-Components fuer alle Startnext Microservices.
Installation
npm install @startnext/chrome
# or
pnpm add @startnext/chromeQuick Start
Es gibt zwei Integrationsmodi:
- Client-Side Rendering — Web Component fetcht Daten selbst (einfach, kein Server noetig)
- Server-Side Rendering (SSR) — Server holt fertig gerendertes HTML von der API (sofort sichtbar ohne JS, bessere Performance + SEO)
Client-Side: Via CDN (PHP, Vanilla HTML)
<script type="module" src="https://unpkg.com/@startnext/chrome@latest/dist/index.js"></script>
<startnext-header api-url="https://scs-api.vercel.app"></startnext-header>
<startnext-footer api-url="https://scs-api.vercel.app"></startnext-footer>Client-Side: Modern JavaScript (React, Vue, etc.)
import '@startnext/chrome';<startnext-header api-url="https://scs-api.vercel.app"></startnext-header>
<startnext-footer api-url="https://scs-api.vercel.app"></startnext-footer>Server-Side Rendering (empfohlen)
SSR-HTML von der API holen und direkt einbinden. Die Web Component JS hydriert anschliessend (haengt Events an, statt DOM neu zu rendern). Die API-Daten werden als JSON im HTML eingebettet — der Component hat sofort echte Daten ohne Client-Fetch.
# SSR-HTML holen (z.B. serverseitig per fetch, file_get_contents, ESI)
curl "https://scs-api.vercel.app/api/header/render?lang=de&large-animation"
curl "https://scs-api.vercel.app/api/footer/render?lang=de"Siehe Framework-spezifische Beispiele: React / Next.js, Vue / Nuxt, PHP / Vanilla
API
<startnext-header>
Attributes:
| Attribute | Type | Default | Description |
|---|---|---|---|
lang |
string |
'de' |
Sprache ('de', 'en') |
light |
boolean |
false |
Heller Header (weisse Schrift/Icons ueber dunklem Hero, wechselt automatisch zu dunkel beim Scrollen). Standard ohne Attribut: dunkler Header |
large-animation |
boolean |
false |
Grosse Lottie-Logo-Animation mit Claim und horizontaler Navigation |
authenticated |
boolean |
false |
Zeigt User-Avatar statt "Anmelden" Button |
user-name |
string |
- | Name des eingeloggten Users |
user-avatar |
string |
- | Avatar-URL des Users |
color-mode |
'light' | 'dark' |
auto | Farbmodus. Ohne Attribut: Header scannt <html> nach Klasse dark/light. Ohne Klasse: Default dark (dunkle Seite, heller Header). Toggle-Button wechselt <html>-Klasse und Header-Darstellung |
hide-color-mode |
boolean |
false |
Versteckt den Color-Mode Toggle-Button |
hide-lang |
boolean |
false |
Versteckt den Language-Switcher |
hide-login |
boolean |
false |
Versteckt den Login/Avatar-Button |
show-back-link |
boolean |
false |
Zeigt einen "Zurueck zu Startnext"-Link (Text kommt von der API) |
back-url |
string |
- | Ueberschreibt die Back-Link-URL |
back-label |
string |
- | Ueberschreibt den Back-Link-Text |
api-url |
string |
- | API-Endpoint-URL fuer Live-Daten aus Notion (nicht noetig bei SSR) |
Events:
| Event | Detail | Description |
|---|---|---|
burger-menu-toggle |
{ open: boolean } |
Burger-Menue geoeffnet/geschlossen |
user-menu-toggle |
{ open: boolean } |
User-Menue geoeffnet/geschlossen |
navigation-click |
{ item: NavigationItem } |
Navigation-Link geklickt |
cta-click |
{ url: string } |
"Projekt starten" Button geklickt |
logout |
{} |
Abmelden geklickt |
language-change |
{ language: string } |
Sprache gewechselt |
scroll-state-change |
{ scrolled: boolean, slideUp: boolean } |
Scroll-Zustand geaendert |
color-mode-change |
{ mode: 'light' | 'dark' } |
Farbmodus gewechselt (per Toggle-Button) |
Usage Examples:
<!-- Default: dunkler Header (dunkle Schrift fuer helle Seiten) -->
<startnext-header></startnext-header>
<!-- Heller Header (weisse Schrift ueber dunklem Hero) -->
<startnext-header light></startnext-header>
<!-- Heller Header + grosse Animation (z.B. Startseite) -->
<startnext-header light large-animation></startnext-header>
<!-- Heller Header + gross + eingeloggt -->
<startnext-header
light
large-animation
authenticated
user-name="Elias Groesel"
user-avatar="https://..."
></startnext-header><!-- Color Mode: Header scannt <html> automatisch -->
<!-- html.dark → heller Header, html.light → dunkler Header -->
<!-- Toggle-Button (Sun/Moon) im Header wechselt den Modus -->
<html class="dark">
<startnext-header light large-animation></startnext-header>
</html><!-- Embedded / Microservice-Modus: Login ausblenden, Back-Link anzeigen -->
<startnext-header
hide-login
show-back-link
back-url="https://www.startnext.com"
back-label="Zurueck zu Startnext"
api-url="https://scs-api.vercel.app"
></startnext-header>const header = document.querySelector('startnext-header');
header.addEventListener('navigation-click', (e) => {
console.log(e.detail.item);
// e.preventDefault() to handle navigation yourself
});
header.addEventListener('language-change', (e) => {
// Footer synchronisiert Sprache automatisch (lang-sync ist default true).
// Manuelle Synchronisation nur noetig wenn lang-sync="false" gesetzt ist:
// document.querySelector('startnext-footer').setAttribute('lang', e.detail.language);
});
header.addEventListener('logout', () => {
// Handle logout
});
header.addEventListener('color-mode-change', (e) => {
console.log(`Color mode: ${e.detail.mode}`); // 'light' | 'dark'
// <html>-Klasse wird automatisch vom Header gesetzt
});<startnext-footer>
Attributes:
| Attribute | Type | Default | Description |
|---|---|---|---|
lang |
string |
'de' |
Sprache ('de', 'en') |
api-url |
string |
- | API-Endpoint-URL fuer Live-Daten (nicht noetig bei SSR) |
lang-sync |
boolean |
true |
Synchronisiert Sprache automatisch wenn der Header ein language-change Event emittiert. Mit lang-sync="false" deaktivieren |
Events:
| Event | Detail | Description |
|---|---|---|
navigation-click |
{ item: { url, label } } |
Footer-Link geklickt |
Theming
Override CSS Custom Properties:
startnext-header {
--primary-color: #0066FF;
--btn-primary-bg: #0066FF;
--header-bg-scrolled: #FAFAFA;
}
startnext-footer {
--footer-bg: #111111;
--footer-link-hover: #0066FF;
}Fonts
Standardmaessig nutzen die Components System-Fonts als Fallback. Fuer eigene Schriftarten @font-face im eigenen CSS deklarieren und --font-family setzen:
@font-face {
font-family: "PFDin";
font-weight: 400;
font-style: normal;
font-display: swap;
src: url("/fonts/PFDin-subset.woff2") format("woff2");
}
startnext-header, startnext-footer {
--font-family: "PFDin", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}Server-Side Rendering (SSR)
Die API liefert fertig gerendertes HTML mit Declarative Shadow DOM (<template shadowrootmode="open">) und eingebetteten API-Daten als JSON (<script data-ssr-props>). Der Browser zeigt den Content sofort an — ohne JavaScript. Die Web Component JS hydriert anschliessend (Events anbinden, kein Re-Render noetig).
Wie es funktioniert
- Server fetcht SSR-HTML von
/api/header/renderbzw./api/footer/render - HTML wird direkt in die Seite eingebunden
- Browser parst DSD → Shadow DOM existiert sofort, Content ist sichtbar
- Web Component JS laedt → erkennt bestehenden
shadowRoot, setztisHydrating = true render()ueberspringtinnerHTML, ruft nurattachEvents()aufparseSsrProps()liest eingebettetes JSON → populiert API-Cache mit echten Daten- Attribut-Aenderungen (z.B.
color-mode,large-animation) funktionieren sofort — kein Client-Fetch noetig
<!-- Server liefert fertiges HTML (vereinfacht) -->
<startnext-header lang="de" light large-animation>
<template shadowrootmode="open">
<style>/* headerCSS */</style>
<div><!-- Shadow DOM HTML --></div>
</template>
<script type="application/json" data-ssr-props>{"mainNavigation":[...],...}</script>
</startnext-header>
<!-- JS hydriert automatisch -->
<script type="module" src="@startnext/chrome"></script>Render-Endpoints (Query-Parameter)
Header: GET /api/header/render
| Parameter | Typ | Beschreibung |
|---|---|---|
lang |
de | en |
Sprache |
light |
flag | Heller Header |
large-animation |
flag | Grosse Logo-Animation |
authenticated |
flag | Eingeloggt |
user-name |
string | User-Name |
user-avatar |
string | Avatar-URL |
hide-color-mode |
flag | Color-Mode Toggle verstecken |
hide-lang |
flag | Language-Switcher verstecken |
hide-login |
flag | Login-Button verstecken |
show-back-link |
flag | Back-Link anzeigen |
back-url |
string | Back-Link URL |
back-label |
string | Back-Link Text |
Footer: GET /api/footer/render
| Parameter | Typ | Beschreibung |
|---|---|---|
lang |
de | en |
Sprache |
Beispiel: GET /api/header/render?lang=de&large-animation&hide-login&show-back-link
api-url bei SSR nicht noetig
Bei SSR werden die API-Daten als JSON im HTML eingebettet (<script data-ssr-props>). Der Component liest das beim Hydrating und hat sofort echte Daten im Cache. api-url ist daher nicht noetig — der Component rendert und reagiert auf Attribut-Aenderungen ohne zusaetzlichen Fetch.
api-url ist nur fuer Client-Side-Only Rendering relevant, wenn kein SSR-HTML verfuegbar ist.
Framework-Integration (SSR)
React / Next.js (App Router)
React's dangerouslySetInnerHTML nutzt innerHTML, das DSD nicht parsed. Nach der Hydration wird setHTMLUnsafe() als Fallback aufgerufen.
Layout (Server Component):
// app/[locale]/layout.tsx
async function fetchChromeHtml(path: string): Promise<string> {
try {
const res = await fetch(`${process.env.NEXT_PUBLIC_SCS_API_URL}${path}`, {
next: { revalidate: 60 },
});
if (!res.ok) return "";
return await res.text();
} catch {
return "";
}
}
export default async function Layout({ children, params }) {
const { locale } = await params;
const [headerHtml, footerHtml] = await Promise.all([
fetchChromeHtml(`/api/header/render?lang=${locale}&large-animation`),
fetchChromeHtml(`/api/footer/render?lang=${locale}`),
]);
return (
<html lang={locale}>
<body>
<ChromeHeader ssrHtml={headerHtml} />
<main>{children}</main>
<ChromeFooter ssrHtml={footerHtml} />
</body>
</html>
);
}Header (Client Component):
// components/ChromeHeader.tsx
"use client";
import { useEffect, useRef, useState } from "react";
export function ChromeHeader({ ssrHtml }: { ssrHtml?: string }) {
const containerRef = useRef<HTMLDivElement>(null);
const [mounted, setMounted] = useState(false);
useEffect(() => {
const container = containerRef.current;
if (container && ssrHtml) {
const header = container.querySelector("startnext-header");
if (!header?.shadowRoot && "setHTMLUnsafe" in Element.prototype) {
container.setHTMLUnsafe(ssrHtml);
}
}
import("@startnext/chrome");
setMounted(true);
}, [ssrHtml]);
// Attribut-Sync nach Mount (z.B. color-mode, large-animation)
useEffect(() => {
const el = containerRef.current?.querySelector("startnext-header");
if (!el || !mounted) return;
// Beispiel: el.setAttribute("color-mode", resolvedTheme);
}, [mounted]);
if (ssrHtml) {
return (
<div ref={containerRef} style={{ display: "contents" }}
suppressHydrationWarning
dangerouslySetInnerHTML={{ __html: ssrHtml }}
/>
);
}
// Fallback ohne SSR
return <startnext-header api-url={process.env.NEXT_PUBLIC_SCS_API_URL} />;
}Footer (Client Component):
// components/ChromeFooter.tsx
"use client";
import { useEffect, useRef } from "react";
export function ChromeFooter({ ssrHtml }: { ssrHtml?: string }) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const container = containerRef.current;
if (container && ssrHtml) {
const footer = container.querySelector("startnext-footer");
if (!footer?.shadowRoot && "setHTMLUnsafe" in Element.prototype) {
container.setHTMLUnsafe(ssrHtml);
}
}
import("@startnext/chrome");
}, [ssrHtml]);
if (ssrHtml) {
return (
<div ref={containerRef} style={{ display: "contents" }}
suppressHydrationWarning
dangerouslySetInnerHTML={{ __html: ssrHtml }}
/>
);
}
return <startnext-footer api-url={process.env.NEXT_PUBLIC_SCS_API_URL} />;
}Vue / Nuxt 3
Vue parsed DSD korrekt via v-html — kein setHTMLUnsafe() Workaround noetig.
Server Plugin (SSR-HTML fetchen):
// server/plugins/chrome.ts
export default defineEventHandler(async (event) => {
// Wird automatisch von useAsyncData / useFetch aufgerufen
});Layout:
<!-- layouts/default.vue -->
<script setup lang="ts">
const { locale } = useI18n();
const config = useRuntimeConfig();
const { data: headerHtml } = await useFetch(
() => `${config.public.scsApiUrl}/api/header/render?lang=${locale.value}&large-animation`,
{ key: `chrome-header-${locale.value}` }
);
const { data: footerHtml } = await useFetch(
() => `${config.public.scsApiUrl}/api/footer/render?lang=${locale.value}`,
{ key: `chrome-footer-${locale.value}` }
);
onMounted(() => {
import('@startnext/chrome');
});
</script>
<template>
<div v-if="headerHtml" v-html="headerHtml" style="display: contents" />
<startnext-header v-else :lang="locale" :api-url="config.public.scsApiUrl" />
<main>
<slot />
</main>
<div v-if="footerHtml" v-html="footerHtml" style="display: contents" />
<startnext-footer v-else :lang="locale" :api-url="config.public.scsApiUrl" />
</template>Nuxt Config (Custom Elements registrieren):
// nuxt.config.ts
export default defineNuxtConfig({
vue: {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('startnext-'),
},
},
runtimeConfig: {
public: {
scsApiUrl: process.env.SCS_API_URL || 'https://scs-api.vercel.app',
},
},
});PHP / Vanilla HTML
<?php
$apiUrl = 'https://scs-api.vercel.app';
$lang = 'de';
$headerHtml = @file_get_contents("$apiUrl/api/header/render?lang=$lang&large-animation");
$footerHtml = @file_get_contents("$apiUrl/api/footer/render?lang=$lang");
?>
<!DOCTYPE html>
<html lang="<?= $lang ?>">
<head>
<script type="module" src="https://unpkg.com/@startnext/chrome@latest/dist/index.js"></script>
</head>
<body>
<?= $headerHtml ?: '<startnext-header api-url="' . $apiUrl . '"></startnext-header>' ?>
<main><!-- Content --></main>
<?= $footerHtml ?: '<startnext-footer api-url="' . $apiUrl . '"></startnext-footer>' ?>
</body>
</html>Edge Side Includes (ESI)
<!-- Varnish / CDN ESI -->
<esi:try>
<esi:attempt>
<esi:include src="/api/header/render?lang=de&large-animation" />
</esi:attempt>
<esi:except>
<startnext-header api-url="https://scs-api.vercel.app"></startnext-header>
</esi:except>
</esi:try>Render Entry Point (Server-seitig)
Das Paket exportiert einen server-sicheren Subpath @startnext/chrome/render mit reinen String-Funktionen (keine Browser-Abhaengigkeiten):
import {
renderHeader, renderBurgerItems, renderUserItems,
renderFooter,
headerCSS, footerCSS,
toHeaderRenderData, toFooterRenderData,
getUiString, getIcon,
} from '@startnext/chrome/render';| Export | Beschreibung |
|---|---|
renderHeader(props) |
Header Shadow DOM HTML |
renderBurgerItems(data, expandedSections) |
Burger-Menue Items HTML |
renderUserItems(data, expandedSections) |
User-Menue Items HTML |
renderFooter(data) |
Footer Shadow DOM HTML |
headerCSS, footerCSS, resetCSS |
CSS als Strings |
getIcon(name, size?) |
SVG-Icon als String |
getUiString(key, lang) |
UI-String (ARIA Labels etc.) |
toHeaderRenderData(apiData) |
API-Daten -> HeaderData (fuegt stub theme hinzu) |
toFooterRenderData(apiData) |
API-Daten -> FooterData (fuegt stub theme hinzu) |
renderHeaderCrawlerNav(data) |
Crawler-<nav> fuer Header (optional, fuer Client-Side Rendering) |
renderFooterCrawlerNav(data) |
Crawler-<nav> fuer Footer (optional, fuer Client-Side Rendering) |
Browser Support
- Chrome/Edge 90+ (Declarative Shadow DOM: Chrome 90+, Firefox 123+, Safari 16.4+)
- Firefox 88+ (123+ fuer SSR/Hydration)
- Safari 14+ (16.4+ fuer SSR/Hydration)
Development
# Install deps (from monorepo root)
pnpm install
# Dev mode (watch)
pnpm --filter @startnext/chrome dev
# Build
pnpm --filter @startnext/chrome build
# Lint
pnpm --filter @startnext/chrome lintDemo-Seite oeffnen: demo/index.html
Architecture
Jede Component ist modular in drei Dateien aufgeteilt:
src/
├── components/
│ ├── base/
│ │ ├── BaseComponent.ts # Abstrakte Basis (Shadow DOM, DSD-Hydration, Events, Theming)
│ │ ├── icons.ts # SVG-Icon-Registry
│ │ └── styles.ts # Shared Reset-CSS
│ ├── header/
│ │ ├── StartnextHeader.ts # Logik (Scroll, Drawers, Lottie, Events, Hydration)
│ │ ├── header.css.ts # CSS als Template-Literal-Export
│ │ └── header.template.ts # Render-Funktionen (HTML-Strings)
│ └── footer/
│ ├── StartnextFooter.ts # Logik (Render, Events, Hydration)
│ ├── footer.css.ts # CSS als Template-Literal-Export
│ └── footer.template.ts # Render-Funktion (HTML-String)
├── data/
│ ├── mockData.ts # Mock-Daten
│ └── uiStrings.ts # UI-Strings (ARIA Labels, de/en)
├── types/
│ └── index.ts # Alle TypeScript-Interfaces
├── index.ts # Browser-Entry (Custom Elements registrieren)
└── render.ts # Server-Entry (reine String-Funktionen, kein Browser)Konventionen:
- CSS in
*.css.tsals exportierter Template-Literal-String (kein extra Build-Plugin noetig) - Templates in
*.template.tsals reine Funktionen die HTML-Strings zurueckgeben (server-safe) - Logik in der Hauptdatei: Event-Handling, State, Lifecycle, Hydration
- BEM-Naming fuer CSS-Klassen (
headbar__left,drawer__item--highlighted) - Shadow DOM (open mode), kein Virtual DOM, Declarative Shadow DOM fuer SSR
Zwei Entry Points:
src/index.ts->dist/index.js(ESM) +dist/index.umd.js(UMD) — Browser, registriert Custom Elementssrc/render.ts->dist/render.js(ESM) — Server, reine String-Funktionen ohne Browser-Abhaengigkeiten
Monorepo Structure
packages/
scs-web-component/ <- dieses Paket
scs-api/ <- Express API Service (nutzt @startnext/chrome/render fuer SSR)