JSPM

  • Created
  • Published
  • Downloads 12
  • Score
    100M100P100Q113243F
  • 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 fuer 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 (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
});

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

  1. Server fetcht SSR-HTML von /api/header/render bzw. /api/footer/render
  2. HTML wird direkt in die Seite eingebunden
  3. Browser parst DSD → Shadow DOM existiert sofort, Content ist sichtbar
  4. Web Component JS laedt → erkennt bestehenden shadowRoot, setzt isHydrating = true
  5. render() ueberspringt innerHTML, ruft nur attachEvents() auf
  6. parseSsrProps() liest eingebettetes JSON → populiert API-Cache mit echten Daten
  7. 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 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.ts -> dist/index.js (ESM) + dist/index.umd.js (UMD) — Browser, registriert Custom Elements
  • src/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)