feat(skills): add create-website skill for zero-build responsive SPAs and showcases

This commit is contained in:
Moritz Graf
2026-09-28 07:37:36 +02:00
parent 7336f7bfff
commit 2bde04617f
7 changed files with 681 additions and 0 deletions
+6
View File
@@ -84,6 +84,12 @@ Instant chart and diagram generation for chat and Telegram.
- **Delivery:** Generates local PNGs and outputs `MEDIA:<path>` for native Telegram image rendering.
- **Tools:** Standalone CLI at `scripts/chart.py`.
### [create-website](skills/create-website/SKILL.md)
Creative Web Director and Senior Frontend Engineer for single-page applications and interactive showcases.
- **Features:** Builds fancy, responsive, zero-build SPAs (Tailwind CSS Play CDN, Alpine.js reactivity, Lucide icons, Google Fonts) with anti-AI-slop design archetypes (Dark Obsidian, Editorial Warmth, Neo-Brutalism, Modern Clean SaaS).
- **Hosting:** Direct validation and upload to `https://hosting.haumdaucher.de/showcase/...` via the `hosting` skill pipeline, with native Telegram document delivery and QR code generation.
- **Tools:** Standalone validation & publisher CLI at `scripts/publish_website.py`.
### [grill-me](skills/grill-me/SKILL.md)
Relentless, structured interview to stress-test and sharpen plans, architectures, decisions, or ideas before writing code.
- **Features:** Decision tree & frontier exploration, Telegram/mobile-optimized batching (1-3 questions per round), facts-first environment research, anti-sycophancy challenge, and final specification/ADR synthesis.
+150
View File
@@ -0,0 +1,150 @@
---
name: create-website
description: Build and host fancy, production-grade, interactive single-page websites, SPAs, and HTML showcases using modern Zero-Build technologies (Tailwind CSS, Alpine.js, Lucide Icons). Use when the user asks to create, build, design, or show a website, landing page, interactive calculator, showcase, recipe app, dashboard, or web tool.
---
# Create Website & Interactive Single-Page Applications 🌐✨
This skill empowers OpenClaw to act as a **Creative Web Director and Senior Frontend Engineer**. It generates visually stunning, responsive, fully interactive single-page applications (SPAs) contained within a single HTML file—without requiring complex build steps or Node.js compilers.
---
## ⚡ Core Philosophy: The Zero-Build Reactive Engine
1. **Self-Contained Single-File**: HTML markup, Tailwind CSS utility styling, Alpine.js reactivity, and Lucide icons reside in a single standalone file.
2. **Zero-Build Latency**: Runs directly in the browser via fast CDNs. Zero compile time, 100% reliable execution across desktop, tablet, and mobile (iOS Safari, Telegram in-app browser).
3. **Anti-AI-Slop Aesthetics**: Reject generic AI defaults (no plain Inter font, no purple-to-blue gradients, no boring centered 3-card monotony). Every site must have an intentional aesthetic direction with strong typography, depth, and micro-interactions.
---
## 🎨 1. Choose a Visual Archetype
Before writing code, commit strictly to one of the 4 design archetypes (detailed in [references/archetypes.md](references/archetypes.md)):
* **Dark Obsidian & Cyber-Luxury**: Deep dark canvas (`#090A0F`), glassmorphism cards (`backdrop-blur-xl border border-white/10`), electric cyan/violet/emerald glows. Fonts: *Syne* + *Plus Jakarta Sans*. (Ideal for tech showcases, tools, AI, fitness trackers).
* **Editorial Warmth & Swiss Typographic**: Linen/ivory background (`#FDFBF7`), hairline borders (`border-stone-200`), warm terracotta/forest green accents. Fonts: *Fraunces* (Serif) + *Instrument Sans*. (Ideal for research dossiers, recipes, long-form stories).
* **Neo-Brutalism & High-Energy**: High contrast, thick black borders (`border-2 border-black`), offset hard drop shadows (`shadow-[4px_4px_0px_0px_#000]`), vivid canary yellow and lime accents. Fonts: *Space Grotesk* + *JetBrains Mono*. (Ideal for events, party invites, challenges, games).
* **Modern Clean SaaS & Dynamic Dashboard**: Crisp slate canvas, automatic dark/light mode toggle via Alpine, KPI cards, filterable tables. Fonts: *Outfit* + *Plus Jakarta Sans*. (Ideal for data apps, comparisons, calculators).
---
## 🛠️ 2. The Standard CDN Skeleton
Use this bulletproof template structure for all generated websites:
```html
<!DOCTYPE html>
<html lang="de" x-data="{ darkMode: localStorage.getItem('theme') === 'dark' }" :class="{ 'dark': darkMode }">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Titel der Website</title>
<!-- 1. Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&family=Syne:wght@600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<!-- 2. Tailwind CSS v3 Play CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
heading: ['Syne', 'sans-serif'],
sans: ['Plus Jakarta Sans', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace'],
}
}
}
}
</script>
<!-- 3. Alpine.js for snappy reactivity -->
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<!-- 4. Lucide Icons CDN -->
<script src="https://unpkg.com/lucide@latest"></script>
<!-- 5. Optional Confetti for Micro-Delight -->
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.2/dist/confetti.browser.min.js"></script>
</head>
<body class="bg-[#090A0F] text-zinc-100 font-sans min-h-screen flex flex-col antialiased selection:bg-cyan-500/30 selection:text-cyan-200">
<!-- Main Application Content (Tabs, Filters, Interactive Cards, Calculators) -->
<main class="flex-1 max-w-6xl w-full mx-auto px-4 py-8 md:py-16">
<!-- Component Code Here -->
</main>
<!-- 6. Lucide Icons Initialization (Always at the end of body) -->
<script>
document.addEventListener('DOMContentLoaded', () => {
lucide.createIcons();
});
</script>
</body>
</html>
```
---
## ⚡ 3. Add Interactivity with Alpine.js
Review [references/alpine-patterns.md](references/alpine-patterns.md) for pre-built patterns:
* **Dynamic Tabs**: Seamless view switching without page reloads.
* **Instant Search & Filter**: Real-time filtering of cards/recipes/items based on text input or category pills.
* **Reactive Calculators / Sliders**: Live computation (portions, prices, nutrition, splits) with instant UI updates.
* **Modal Dialogs & Drawers**: Smooth transitions with backdrop blur and escape key handling.
* **Celebration Effects**: Triggering confetti on successful calculations or milestones (`confetti({ particleCount: 80, spread: 70 })`).
---
## 🚀 4. Publishing & Sharing Workflow
Once the website HTML has been generated and saved locally (e.g. to `/tmp/websites/<slug>.html`), publish it to public hosting:
### Step 1: Run the Publisher Script
```bash
uv run /home/node/mop-skills/mop-skills/skills/create-website/scripts/publish_website.py \
--file /tmp/websites/<slug>.html \
--category showcase \
--slug <slug>
```
This validates the HTML and uploads it to MinIO, returning the public HTTPS URL:
`https://hosting.haumdaucher.de/showcase/{YYYYMMDD}-{slug}.html`
### Step 2: Present to User in Telegram
Always deliver both the clickable link and the local file for offline access:
```markdown
🚀 **Deine Website ist live!**
🔗 https://hosting.haumdaucher.de/showcase/20260928-ninja-creami-calc.html
*(Die Datei ist zum lokalen Öffnen ebenfalls beigefügt)*
MEDIA:/tmp/websites/<slug>.html
```
### Step 3: Offer QR Code
Immediately trigger `ask_user` to offer generating a scannable QR code:
```json
{
"question": "Möchtest du für diesen Link einen QR-Code generieren?",
"options": ["Ja, QR-Code erstellen", "Nein, danke"]
}
```
If approved, execute the `qr-code` skill:
```bash
uv run /home/node/mop-skills/mop-skills/skills/qr-code/scripts/generate_qr.py "https://hosting.haumdaucher.de/showcase/20260928-ninja-creami-calc.html"
```
and send via `MEDIA:<path>`.
---
## 🛡️ Quality Gate & Anti-Slop Checklist
Always check the site against [references/anti-slop-checklist.md](references/anti-slop-checklist.md) before publishing:
1. Are font pairings distinct and imported via Google Fonts?
2. Are all buttons responsive with hover and active states (`active:scale-95`)?
3. Does the layout scale gracefully on small mobile screens (`grid-cols-1 md:grid-cols-2 lg:grid-cols-3`)?
4. Are Lucide icons properly rendered via `lucide.createIcons()`?
@@ -0,0 +1,210 @@
# Alpine.js Core Patterns for Single-Page Applications ⚡
These zero-build Alpine.js patterns provide reactive state management directly in HTML. Use these snippets to make your single-page websites snappy, interactive, and responsive.
---
## 1. Dark/Light Mode Switcher with Persistence
Place this in `<html x-data="{ darkMode: localStorage.getItem('theme') === 'dark' }" :class="{ 'dark': darkMode }">`:
```html
<button
@click="darkMode = !darkMode; localStorage.setItem('theme', darkMode ? 'dark' : 'light')"
class="p-2.5 rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all cursor-pointer"
title="Toggle Theme">
<i x-show="!darkMode" data-lucide="moon" class="w-5 h-5"></i>
<i x-show="darkMode" data-lucide="sun" class="w-5 h-5"></i>
</button>
```
---
## 2. Dynamic Tab Navigation
Switch between multiple views without page reloads:
```html
<div x-data="{ activeTab: 'overview' }" class="w-full">
<!-- Tab Bar -->
<div class="flex gap-2 p-1.5 bg-zinc-100 dark:bg-zinc-900 rounded-xl max-w-md mx-auto mb-8 border border-zinc-200 dark:border-zinc-800">
<button
@click="activeTab = 'overview'"
:class="activeTab === 'overview' ? 'bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white shadow-sm' : 'text-zinc-500 hover:text-zinc-900 dark:hover:text-white'"
class="flex-1 py-2 px-4 rounded-lg font-medium text-sm transition-all flex items-center justify-center gap-2">
<i data-lucide="layout-grid" class="w-4 h-4"></i> Overview
</button>
<button
@click="activeTab = 'calculator'"
:class="activeTab === 'calculator' ? 'bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white shadow-sm' : 'text-zinc-500 hover:text-zinc-900 dark:hover:text-white'"
class="flex-1 py-2 px-4 rounded-lg font-medium text-sm transition-all flex items-center justify-center gap-2">
<i data-lucide="calculator" class="w-4 h-4"></i> Calculator
</button>
</div>
<!-- Tab Contents with Transitions -->
<div x-show="activeTab === 'overview'" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 translate-y-2" x-transition:enter-end="opacity-100 translate-y-0">
<!-- Overview View Content -->
</div>
<div x-show="activeTab === 'calculator'" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 translate-y-2" x-transition:enter-end="opacity-100 translate-y-0">
<!-- Calculator View Content -->
</div>
</div>
```
---
## 3. Instant Search & Category Filter
Real-time filtering of cards or list items:
```html
<div x-data="{
search: '',
activeCategory: 'all',
items: [
{ title: 'Pistazien Gelato', category: 'creami', protein: '28g', calories: 340, desc: 'Reichhaltiges Gelato mit echter sizilianischer Pistazienpaste.' },
{ title: 'Protein Erdbeer Sorbet', category: 'protein', protein: '35g', calories: 210, desc: 'Frisches Erdbeersorbet mit Clear Whey Isolat.' },
{ title: 'Mango Maracuja Slush', category: 'fruit', protein: '2g', calories: 140, desc: 'Perfekte Sommererfrischung mit Mango-Chunks.' }
],
get filteredItems() {
return this.items.filter(item => {
const matchesSearch = item.title.toLowerCase().includes(this.search.toLowerCase()) || item.desc.toLowerCase().includes(this.search.toLowerCase());
const matchesCategory = this.activeCategory === 'all' || item.category === this.activeCategory;
return matchesSearch && matchesCategory;
});
}
}">
<!-- Search & Pills -->
<div class="flex flex-col sm:flex-row gap-4 mb-8">
<div class="relative flex-1">
<i data-lucide="search" class="w-4 h-4 absolute left-3.5 top-3.5 text-zinc-400"></i>
<input
x-model="search"
type="text"
placeholder="Rezepte durchsuchen..."
class="w-full pl-10 pr-4 py-2.5 rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-zinc-900 dark:text-white placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-cyan-500 text-sm">
</div>
<div class="flex gap-2 overflow-x-auto pb-1">
<template x-for="cat in ['all', 'creami', 'protein', 'fruit']" :key="cat">
<button
@click="activeCategory = cat"
:class="activeCategory === cat ? 'bg-cyan-500 text-white font-semibold' : 'bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 hover:bg-zinc-200'"
class="px-4 py-2 rounded-xl text-xs uppercase tracking-wider capitalize whitespace-nowrap transition-all cursor-pointer"
x-text="cat === 'all' ? 'Alle Rezepte' : cat">
</button>
</template>
</div>
</div>
<!-- Cards Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<template x-for="item in filteredItems" :key="item.title">
<div class="p-6 rounded-2xl bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 hover:border-cyan-500/50 shadow-sm hover:shadow-xl transition-all">
<h3 class="text-lg font-bold text-zinc-900 dark:text-white mb-2" x-text="item.title"></h3>
<p class="text-sm text-zinc-500 dark:text-zinc-400 mb-4" x-text="item.desc"></p>
<div class="flex justify-between items-center text-xs font-semibold text-cyan-600 dark:text-cyan-400">
<span x-text="item.protein + ' Protein'"></span>
<span x-text="item.calories + ' kcal'"></span>
</div>
</div>
</template>
</div>
</div>
```
---
## 4. Live Reactive Slider & Calculator
Instant calculations with automatic UI updates:
```html
<div x-data="{
pints: 1,
baseMilk: 300,
baseWhey: 35,
basePistachio: 30,
get milk() { return this.baseMilk * this.pints; },
get whey() { return this.baseWhey * this.pints; },
get pistachio() { return this.basePistachio * this.pints; },
get proteinTotal() { return Math.round(this.whey * 0.8 + (this.milk * 0.034)); },
celebrate() {
if (typeof confetti === 'function') {
confetti({ particleCount: 75, spread: 60, origin: { y: 0.8 } });
}
}
}" class="p-6 rounded-2xl bg-zinc-900 border border-white/10 text-white">
<div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-bold font-heading">Portions-Rechner</h3>
<span class="px-3 py-1 bg-cyan-500/20 text-cyan-400 rounded-full text-xs font-semibold" x-text="pints + (pints === 1 ? ' Pint' : ' Pints')"></span>
</div>
<div class="mb-6">
<label class="block text-xs uppercase tracking-wider text-zinc-400 mb-2">Anzahl Ninja CREAMi Deluxe Pints (680ml)</label>
<input type="range" min="1" max="4" step="1" x-model.number="pints" class="w-full accent-cyan-400 cursor-pointer">
</div>
<!-- Computed Ingredients -->
<div class="space-y-3 mb-6">
<div class="flex justify-between py-2 border-b border-white/5 text-sm">
<span class="text-zinc-400">Milch 1.5% oder Hafermilch</span>
<span class="font-bold text-white" x-text="milk + ' ml'"></span>
</div>
<div class="flex justify-between py-2 border-b border-white/5 text-sm">
<span class="text-zinc-400">Proteinpulver (Whey/Casein)</span>
<span class="font-bold text-cyan-400" x-text="whey + ' g'"></span>
</div>
<div class="flex justify-between py-2 border-b border-white/5 text-sm">
<span class="text-zinc-400">Pistazienmus (100%)</span>
<span class="font-bold text-emerald-400" x-text="pistachio + ' g'"></span>
</div>
</div>
<button @click="celebrate()" class="w-full py-3 bg-gradient-to-r from-cyan-500 to-blue-600 rounded-xl font-bold text-sm hover:brightness-110 active:scale-95 transition-all">
Zutaten vorbereitet! 🎉
</button>
</div>
```
---
## 5. Modal Dialog with Escape & Backdrop Blur
Accessible modal overlay:
```html
<div x-data="{ open: false }">
<button @click="open = true" class="px-4 py-2 bg-zinc-900 text-white rounded-xl text-sm font-medium">
Details öffnen
</button>
<div
x-show="open"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
style="display: none;">
<div
@click.outside="open = false"
class="bg-white dark:bg-zinc-900 rounded-2xl max-w-lg w-full p-6 border border-zinc-200 dark:border-zinc-800 shadow-2xl"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold text-zinc-900 dark:text-white">Modal Titel</h3>
<button @click="open = false" class="text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-6">
Hier befindet sich detaillierter Inhalt mit Reaktivität oder Formularen.
</p>
<div class="flex justify-end gap-3">
<button @click="open = false" class="px-4 py-2 text-sm text-zinc-500 hover:text-zinc-800 dark:hover:text-white">Schließen</button>
<button @click="open = false" class="px-4 py-2 text-sm font-medium bg-cyan-500 text-white rounded-xl">Bestätigen</button>
</div>
</div>
</div>
</div>
```
@@ -0,0 +1,38 @@
# Anti-AI-Slop Quality Checklist 🛡️
Before finalizing and publishing any website or HTML artifact, verify every item on this checklist. This ensures the output feels designed by a human senior product designer rather than a generic LLM.
---
## ❌ Forbidden Trappings of "AI Slop"
1. **Never use generic default fonts**:
* 🚫 No raw Inter, Roboto, Arial, or unstyled system-ui.
* ✅ Always import characterful fonts (e.g. *Syne*, *Cabinet Grotesk*, *Fraunces*, *Space Grotesk*, *Plus Jakarta Sans*).
2. **Never use the generic purple-to-indigo gradient**:
* 🚫 `bg-gradient-to-r from-purple-600 to-indigo-600` on white background.
* ✅ Use intentional palettes: deep obsidian with electric cyan accents, warm ivory with terracotta, neo-brutalist yellow with black borders, or slate with emerald glow.
3. **Never generate static, boring centered 3-card rows**:
* 🚫 Three identical white boxes with light gray text centered on a plain gray canvas.
* ✅ Vary card sizes, emphasize a primary card with a border glow or badge, use grid spans (`md:col-span-2`), or add asymmetry.
4. **Never forget interactive micro-interactions**:
* 🚫 Dead buttons that don't react to hover or click.
* ✅ Always add `hover:-translate-y-0.5 hover:shadow-lg transition-all duration-200` and `active:scale-95`.
5. **Never rely on broken external images**:
* 🚫 Using fake URLs like `https://example.com/image.jpg` or placeholder services that fail.
* ✅ Use SVG patterns, Lucide icons, gradient mesh backgrounds, or reliable Unsplash source links with explicit fallback styling.
---
## ✅ Mandatory Pre-Flight Verification
| Check | Requirement | Verified? |
|---|---|:---:|
| **Viewport & Encoding** | `<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">` | [ ] |
| **Tailwind CDN** | `<script src="https://cdn.tailwindcss.com"></script>` included in `<head>` | [ ] |
| **Alpine.js CDN** | `<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>` in `<head>` | [ ] |
| **Lucide Icons** | `<script src="https://unpkg.com/lucide@latest"></script>` + `lucide.createIcons()` at bottom of `<body>` | [ ] |
| **Google Fonts** | Preconnect + Google Font `<link>` + custom `<style>` setting `font-family` | [ ] |
| **Touch Targets** | Buttons and clickable elements have at least `py-2.5 px-4` and `cursor-pointer` | [ ] |
| **Dark Mode Consistency** | If dark mode is supported, all cards, borders, text, and inputs have `dark:` variants | [ ] |
| **Mobile Responsiveness** | Layout uses `grid-cols-1 md:grid-cols-2 lg:grid-cols-3` and responsive paddings (`p-4 md:p-8`) | [ ] |
| **Content Richness** | Real, realistic domain content (no repeated "Lorem Ipsum" filler) | [ ] |
@@ -0,0 +1,122 @@
# Design Archetypes & Visual Systems 🎨
This document defines the 4 core design archetypes used by the `create-website` skill. Every generated website or single-page application must commit fully to one of these visual directions to ensure high aesthetic distinction and eliminate generic AI aesthetics.
---
## 1. Dark Obsidian & Cyber-Luxury 🌌
*Best for: Tech products, developer tools, AI showcases, crypto/finance, dark-mode dashboards, interactive apps.*
### Google Fonts Embed
```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&family=Syne:wght@600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
body { font-family: 'Plus Jakarta Sans', sans-serif; }
h1, h2, h3, .font-heading { font-family: 'Syne', sans-serif; }
code, pre, .font-mono { font-family: 'JetBrains Mono', monospace; }
</style>
```
### Color Palette & Classes
* **Canvas Background**: `bg-[#090A0F]` with subtle radial gradients:
`bg-[radial-gradient(ellipse_80%_80%_at_50%_-20%,rgba(120,119,198,0.15),rgba(255,255,255,0))]`
* **Card Surface (Glassmorphism)**: `bg-white/[0.03] backdrop-blur-xl border border-white/10 hover:border-white/20 transition-all`
* **Secondary Surface**: `bg-[#12151E] border border-white/5`
* **Text Hierarchy**: Headings `text-white font-bold tracking-tight`, Body `text-zinc-400`, Subtle `text-zinc-500`
* **Accent Highlights**:
* Electric Cyan: `text-cyan-400 bg-cyan-500/10 border-cyan-500/20 shadow-[0_0_20px_rgba(6,182,212,0.15)]`
* Neon Violet: `text-purple-400 bg-purple-500/10 border-purple-500/20`
* Emerald Glow: `text-emerald-400 bg-emerald-500/10 border-emerald-500/20`
* **Buttons**:
* Primary: `bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 text-white font-medium px-5 py-2.5 rounded-xl shadow-lg shadow-cyan-500/20 active:scale-95 transition-all`
* Secondary: `bg-white/5 hover:bg-white/10 text-white border border-white/10 px-5 py-2.5 rounded-xl active:scale-95 transition-all`
---
## 2. Editorial Warmth & Swiss Typographic 📰
*Best for: In-depth research dossiers, long-form journalism, recipe collections, personal manifestos, architectural notes, knowledge collections.*
### Google Fonts Embed
```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..700;1,9..144,300..700&family=Instrument+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Instrument Sans', sans-serif; }
h1, h2, h3, .font-heading { font-family: 'Fraunces', serif; }
</style>
```
### Color Palette & Classes
* **Canvas Background**: Warm Ivory `bg-[#FDFBF7]` or Linen `bg-[#F7F4EE]`
* **Card Surface**: Paper White `bg-white border border-stone-200/80 shadow-sm rounded-2xl`
* **Text Hierarchy**: Headings `text-stone-900 font-semibold tracking-tight`, Body `text-stone-600 leading-relaxed`, Captions `text-stone-500 text-sm`
* **Accent Highlights**:
* Heritage Rust / Terracotta: `text-[#C2410C] bg-[#FFF7ED] border-[#FDBA74]`
* Deep Forest: `text-[#166534] bg-[#F0FDF4] border-[#BBF7D0]`
* Warm Amber: `text-[#B45309] bg-[#FEF3C7] border-[#FDE68A]`
* **Styling Rules**:
* Generous margins and breathing room (`py-16 md:py-24 max-w-4xl mx-auto`).
* Elegant divider rules (`border-t border-stone-200`).
* Dropcaps or serif quotes with subtle italic styling (`italic text-stone-700`).
---
## 3. Neo-Brutalism & High-Energy ⚡
*Best for: Interactive party invites, festival schedules, fitness challenges, hackathon pitches, playful calculators, game companions.*
### Google Fonts Embed
```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;700&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Space Grotesk', sans-serif; }
code, pre, .font-mono { font-family: 'JetBrains Mono', monospace; }
</style>
```
### Color Palette & Classes
* **Canvas Background**: High-contrast Pale Pastel `bg-[#FAF8F5]` or Off-Yellow `bg-[#FEFCE8]`
* **Borders & Shadows**:
* Signature Shadow: `border-2 border-black shadow-[4px_4px_0px_0px_#000] hover:shadow-[2px_2px_0px_0px_#000] hover:translate-x-[2px] hover:translate-y-[2px] transition-all`
* Card: `bg-white border-2 border-black shadow-[5px_5px_0px_0px_#000] rounded-xl p-6`
* **Color Accents**:
* Electric Canary: `bg-[#FDE047]`
* Acid Lime: `bg-[#A3E635]`
* Bright Coral: `bg-[#FB7185]`
* Cyan Pill: `bg-[#38BDF8]`
* **Interactive Elements**:
* Badges: `inline-block px-3 py-1 text-xs font-bold uppercase tracking-wider bg-black text-white rounded-md`
* Buttons: `bg-[#FDE047] hover:bg-[#FACC15] text-black font-bold px-6 py-3 border-2 border-black shadow-[4px_4px_0px_0px_#000] active:translate-x-[4px] active:translate-y-[4px] active:shadow-none transition-all rounded-xl cursor-pointer`
---
## 4. Modern Clean SaaS & Dynamic Dashboard 📊
*Best for: Multi-tab trackers, data analytics, KPI summaries, filterable catalogues, interactive calculators, recipe managers.*
### Google Fonts Embed
```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@400;500;600&display=swap" rel="stylesheet">
<style>
body { font-family: 'Plus Jakarta Sans', sans-serif; }
h1, h2, h3, .font-heading { font-family: 'Outfit', sans-serif; }
</style>
```
### Color Palette & Classes
* **Canvas Background**: Adaptive Light/Dark:
`bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-100 transition-colors duration-200`
* **Card Surface**:
`bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-2xl shadow-sm hover:shadow-md transition-all`
* **KPI Metrics**:
* Value: `text-3xl md:text-4xl font-bold font-heading tracking-tight text-slate-900 dark:text-white`
* Label: `text-xs font-medium uppercase tracking-wider text-slate-500 dark:text-slate-400 mt-1`
* **Interactive Controls**:
* Inputs: `bg-slate-100 dark:bg-slate-800 border-none rounded-xl px-4 py-2 text-sm focus:ring-2 focus:ring-indigo-500`
* Active Tabs: `bg-indigo-600 text-white rounded-lg px-4 py-2 font-medium shadow-sm`
* Inactive Tabs: `text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white px-4 py-2 rounded-lg`
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
# /// script
# dependencies = [
# "boto3>=1.34.0",
# ]
# ///
"""
Publish single-page websites to hosting.haumdaucher.de with validation and clean URL reporting.
Integrates with the existing 'hosting' skill publisher.
"""
import argparse
import os
import re
import subprocess
import sys
from pathlib import Path
def validate_html(content: str) -> list[str]:
"""Basic pre-flight checks to ensure the generated website isn't malformed."""
warnings = []
content_lower = content.lower()
if "<!doctype html>" not in content_lower:
warnings.append("Missing <!DOCTYPE html> declaration.")
if "viewport" not in content_lower:
warnings.append("Missing responsive viewport meta tag.")
if "cdn.tailwindcss.com" not in content_lower and "tailwindcss" not in content_lower:
warnings.append("Tailwind CSS CDN not detected in HTML.")
if "alpine" not in content_lower:
warnings.append("Alpine.js script not detected (interactivity might be missing).")
if "lucide" in content_lower and "lucide.createicons" not in content_lower:
warnings.append("Lucide icons included, but 'lucide.createIcons()' initialization is missing.")
return warnings
def resolve_hosting_script() -> Path:
"""Finds the publish_file.py script relative to this script or in standard locations."""
current_dir = Path(__file__).resolve().parent
# Standard relative location in mop-skills
sibling_hosting = current_dir.parent.parent / "hosting" / "scripts" / "publish_file.py"
if sibling_hosting.exists():
return sibling_hosting
# Fallback in container
pod_hosting = Path("/home/node/mop-skills/mop-skills/skills/hosting/scripts/publish_file.py")
if pod_hosting.exists():
return pod_hosting
raise FileNotFoundError(f"Cannot find publish_file.py in {sibling_hosting} or {pod_hosting}")
def main():
parser = argparse.ArgumentParser(description="Publish websites to hosting.haumdaucher.de")
parser.add_argument("--file", "-f", help="Local HTML file to upload")
parser.add_argument("--content", "-c", help="Inline HTML content string")
parser.add_argument("--slug", "-s", required=True, help="Slug identifier for URL (e.g. creami-calculator)")
parser.add_argument("--category", default="showcase", help="Category folder (default: showcase)")
parser.add_argument("--skip-validation", action="store_true", help="Skip HTML quality checks")
parser.add_argument("--url-only", action="store_true", help="Print only public URL")
args = parser.parse_args()
if not args.file and args.content is None:
parser.error("You must provide either --file or --content.")
# Read content for validation
html_content = ""
if args.file:
file_path = Path(args.file)
if not file_path.exists():
sys.exit(f"Error: File not found: {args.file}")
html_content = file_path.read_text(encoding="utf-8", errors="replace")
else:
html_content = args.content
# Quality validation
if not args.skip_validation:
warnings = validate_html(html_content)
if warnings:
print("⚠️ Pre-flight HTML Warnings:", file=sys.stderr)
for w in warnings:
print(f" - {w}", file=sys.stderr)
# Delegate to hosting/scripts/publish_file.py
publisher_script = resolve_hosting_script()
cmd = [
sys.executable,
str(publisher_script),
"--category", args.category,
"--slug", args.slug,
"--ext", "html",
]
if args.file:
cmd.extend(["--file", str(args.file)])
else:
cmd.extend(["--content", args.content])
if args.url_only:
cmd.append("--url-only")
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
sys.stderr.write(result.stderr)
sys.exit(result.returncode)
print(result.stdout.strip())
if __name__ == "__main__":
main()
@@ -0,0 +1,45 @@
import unittest
from pathlib import Path
import sys
# Add scripts directory to path
script_dir = Path(__file__).resolve().parent.parent / "scripts"
sys.path.insert(0, str(script_dir))
from publish_website import validate_html, resolve_hosting_script
class TestPublishWebsite(unittest.TestCase):
def test_validate_html_valid(self):
valid_html = """
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://unpkg.com/lucide@latest"></script>
</head>
<body>
<i data-lucide="sparkles"></i>
<script>lucide.createIcons();</script>
</body>
</html>
"""
warnings = validate_html(valid_html)
self.assertEqual(warnings, [])
def test_validate_html_warnings(self):
broken_html = "<html><body>Hello</body></html>"
warnings = validate_html(broken_html)
self.assertIn("Missing <!DOCTYPE html> declaration.", warnings)
self.assertIn("Missing responsive viewport meta tag.", warnings)
self.assertIn("Tailwind CSS CDN not detected in HTML.", warnings)
self.assertIn("Alpine.js script not detected (interactivity might be missing).", warnings)
def test_resolve_hosting_script(self):
hosting_script = resolve_hosting_script()
self.assertTrue(hosting_script.exists())
self.assertEqual(hosting_script.name, "publish_file.py")
if __name__ == "__main__":
unittest.main()