JSPM

  • Created
  • Published
  • Downloads 12
  • Score
    100M100P100Q113615F
  • License MIT

Startnext Chrome Web Components

Package Exports

  • @startnext/chrome
  • @startnext/chrome/render

Readme

@startnext/chrome

Startnext Header/Footer Web Components

Wiederverwendbare Header- und Footer-Components für alle Startnext Microservices.

Installation

npm install @startnext/chrome
# or
pnpm add @startnext/chrome

Quick Start

Es gibt zwei Integrationsmodi:

  1. Client-Side Rendering — Web Component fetcht Daten selbst (einfach, kein Server noetig)
  2. Server-Side Rendering — 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). Siehe Abschnitt SSR mit React / Next.js oder SSR — PHP / ESI weiter unten.

# 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"

API

<startnext-header>

Attributes:

Attribute Type Default Description
lang string 'de' Sprache ('de', 'en')
light boolean false Heller Header (weiße Schrift/Icons über dunklem Hero, wechselt automatisch zu dunkel beim Scrollen). Standard ohne Attribut: dunkler Header
large-animation boolean false Große 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

Events:

Event Detail Description
burger-menu-toggle { open: boolean } Burger-Menü geöffnet/geschlossen
user-menu-toggle { open: boolean } User-Menü geöffnet/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 geändert
color-mode-change { mode: 'light' | 'dark' } Farbmodus gewechselt (per Toggle-Button)

Usage Examples:

<!-- Default: dunkler Header (dunkle Schrift für helle Seiten) -->
<startnext-header></startnext-header>

<!-- Heller Header (weiße Schrift über dunklem Hero) -->
<startnext-header light></startnext-header>

<!-- Heller Header + große Animation (z.B. Startseite) -->
<startnext-header light large-animation></startnext-header>

<!-- Heller Header + groß + 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
});

Attributes:

Attribute Type Default Description
lang string 'de' Sprache ('de', 'en')
api-url string - API-Endpoint-URL fuer Live-Daten
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

Standardmäßig nutzen die Components System-Fonts als Fallback. Für 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)

Das Paket exportiert einen server-sicheren Subpath @startnext/chrome/render mit reinen String-Funktionen (keine Browser-Abhaengigkeiten). Damit kann der API-Server fertig gerendertes HTML per Declarative Shadow DOM erzeugen.

Render Entry Point

import {
  renderHeader, renderBurgerItems, renderUserItems,
  renderFooter,
  headerCSS, footerCSS,
  renderHeaderCrawlerNav, renderFooterCrawlerNav,
  toHeaderRenderData, toFooterRenderData,
  getUiString, getIcon,
} from '@startnext/chrome/render';

Exports

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 (Light DOM, SEO)
renderFooterCrawlerNav(data) Crawler-<nav> fuer Footer (Light DOM, SEO)

Hydration

Wenn der Server HTML mit Declarative Shadow DOM (<template shadowrootmode="open">) liefert, erkennt die Web Component den bestehenden shadowRoot und hydriert statt neu zu rendern:

  1. Browser parst DSD → shadowRoot existiert vor dem Custom-Element-Constructor
  2. BaseComponent erkennt bestehenden shadowRoot, setzt isHydrating = true
  3. render() ueberspringt innerHTML, ruft nur attachEvents() auf
  4. connectedCallback() erkennt bestehenden [data-crawler-nav], ueberspringt renderLightDomNav()
  5. loadApiData() holt frische Daten → loest danach ein vollstaendiges Re-Render aus
<!-- Server liefert fertiges HTML -->
<startnext-header lang="de" light large-animation>
  <template shadowrootmode="open">
    <style>/* headerCSS */</style>
    <div><!-- Shadow DOM HTML --></div>
  </template>
  <nav data-crawler-nav aria-hidden="true" style="display:none">
    <!-- SEO-Links -->
  </nav>
</startnext-header>

<!-- JS hydriert automatisch -->
<script type="module" src="@startnext/chrome"></script>

SSR mit React / Next.js (App Router)

React's dangerouslySetInnerHTML nutzt intern innerHTML, das Declarative Shadow DOM nicht parsed. Daher muss nach der Hydration setHTMLUnsafe() als Fallback aufgerufen werden.

1. Layout (Server Component) — SSR HTML von der API fetchen:

// 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>
  );
}

2. Client Component — SSR HTML rendern + DSD Fallback + Hydration:

// 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) {
      // Falls DSD nicht geparsed wurde (innerHTML parsed kein DSD),
      // setHTMLUnsafe() als Fallback nutzen
      const header = container.querySelector("startnext-header");
      if (!header?.shadowRoot && "setHTMLUnsafe" in Element.prototype) {
        container.setHTMLUnsafe(ssrHtml);
      }
    }
    import("@startnext/chrome"); // Web Component JS laden → Hydration
    setMounted(true);
  }, [ssrHtml]);

  if (ssrHtml) {
    return (
      <div
        ref={containerRef}
        style={{ display: "contents" }}
        suppressHydrationWarning
        dangerouslySetInnerHTML={{ __html: ssrHtml }}
      />
    );
  }

  // Fallback: Client-Side Rendering (wenn SSR HTML nicht verfuegbar)
  return <startnext-header api-url={process.env.NEXT_PUBLIC_SCS_API_URL} />;
}

Wichtig: dangerouslySetInnerHTML sorgt dafuer, dass das SSR-HTML im initialen HTML-Stream enthalten ist (fuer Crawler und First Paint). setHTMLUnsafe() stellt sicher, dass DSD auch nach React-Hydration oder Client-Side-Navigation korrekt geparsed wird.

Render-Endpoints (Query-Parameter):

Parameter Typ Beschreibung
lang de | en Sprache
light flag Heller Header
large-animation flag Grosse Logo-Animation
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

Beispiel: GET /api/header/render?lang=de&large-animation&hide-login&show-back-link

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 lint

Demo-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.ts als exportierter Template-Literal-String (kein extra Build-Plugin noetig)
  • Templates in *.template.ts als 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.tsdist/index.js (ESM) + dist/index.umd.js (UMD) — Browser, registriert Custom Elements
  • src/render.tsdist/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)