2026-01-09 00:16:46 +02:00
<!DOCTYPE html>
< html lang = "en" >
2026-01-12 02:02:13 +02:00
< head >
< meta charset = "UTF-8" / >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" / >
< title > WriteKit — Blogging Platform for Developers< / title >
< link rel = "icon" type = "image/x-icon" href = "/assets/writekit-icon.ico" / >
< link rel = "icon" type = "image/svg+xml" href = "/assets/writekit-icon.svg" / >
< link
rel="icon"
type="image/png"
sizes="32x32"
href="/assets/favicon-32x32.png"
/>
< link
rel="icon"
type="image/png"
sizes="16x16"
href="/assets/favicon-16x16.png"
/>
< link
rel="apple-touch-icon"
sizes="180x180"
href="/assets/apple-touch-icon.png"
/>
< style >
*{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#fafafa;--text:#0a0a0a;--muted:#737373;--border:#e5e5e5;--accent:{{ACCENT}};--primary:#10b981}
body{font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);line-height:1.6}
.mono{font-family:'SF Mono','Fira Code','Consolas',monospace}
.layout{display:grid;grid-template-columns:200px 1fr;min-height:100vh}
aside{padding:2rem 1.5rem;border-right:1px solid var(--border);position:sticky;top:0;height:100vh;display:flex;flex-direction:column}
.sidebar-logo{font-family:'SF Mono','Fira Code',monospace;font-weight:600;font-size:14px;letter-spacing:-0.02em;margin-bottom:0.25rem}
.sidebar-tagline{font-family:'SF Mono','Fira Code',monospace;font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:2.5rem}
.sidebar-nav{display:flex;flex-direction:column;gap:0.25rem}
.sidebar-nav a{font-family:'SF Mono','Fira Code',monospace;font-size:12px;color:var(--muted);text-decoration:none;padding:0.5rem 0;transition:color 0.15s}
.sidebar-nav a:hover{color:var(--text)}
.sidebar-divider{height:1px;background:var(--border);margin:1rem 0}
.sidebar-label{font-family:'SF Mono','Fira Code',monospace;font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:0.5rem}
.sidebar-footer{margin-top:auto}
.env-badge{font-family:'SF Mono','Fira Code',monospace;display:inline-block;padding:0.35rem 0.75rem;border:1px solid var(--accent);font-size:10px;text-transform:uppercase;letter-spacing:0.05em;color:var(--accent)}
.version-badge{font-family:'SF Mono','Fira Code',monospace;font-size:10px;color:var(--muted);opacity:0.5;margin-top:0.5rem;cursor:default;transition:opacity 0.15s}
.version-badge:hover{opacity:1}
main{min-width:0}
.hero{position:relative;padding:5rem 3rem 4rem;border-bottom:1px solid var(--border);overflow:hidden}
.hero-canvas{position:absolute;top:0;right:0;width:45%;height:100%;opacity:0.6;mask-image:linear-gradient(to left,black,transparent);-webkit-mask-image:linear-gradient(to left,black,transparent)}
.hero>*:not(.hero-canvas){position:relative;z-index:1}
.hero-label{font-family:'SF Mono','Fira Code',monospace;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.15em;margin-bottom:1.5rem}
.hero h1{font-family:system-ui,-apple-system,sans-serif;font-size:clamp(2rem,4vw,3rem);font-weight:500;letter-spacing:-0.03em;line-height:1.15;margin-bottom:1.5rem;max-width:600px}
.hero-sub{font-family:'SF Mono','Fira Code',monospace;font-size:13px;color:var(--muted);max-width:550px;line-height:1.7}
.hero-sub code{color:var(--text);background:var(--border);padding:0.15em 0.4em;font-size:0.95em}
.section{padding:4rem 3rem;border-bottom:1px solid var(--border)}
.section-label{font-family:'SF Mono','Fira Code',monospace;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.15em;margin-bottom:2rem}
.hero-cta{margin-top:2.5rem}
.hero-cta .demo-btn{width:auto;padding:14px 32px;font-size:14px}
.hero-cta .demo-note{text-align:left}
.section-features{border-top:1px solid var(--border);border-bottom:1px solid var(--border);margin-top:-1px}
.features-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:1px;background:var(--border)}
.feature{background:var(--bg);padding:2rem}
.feature-num{font-family:'SF Mono','Fira Code',monospace;font-size:11px;color:var(--muted);margin-bottom:0.75rem}
.feature h3{font-size:1rem;font-weight:500;margin-bottom:0.5rem;letter-spacing:-0.01em}
.feature p{font-family:'SF Mono','Fira Code',monospace;font-size:11px;color:var(--muted);line-height:1.6}
.demo-btn{width:100%;margin-top:16px;padding:12px 24px;background:var(--text);color:var(--bg);border:none;font-family:'SF Mono','Fira Code',monospace;font-size:13px;cursor:pointer;transition:all 0.2s}
.demo-btn:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,0.15)}
.demo-btn:disabled{opacity:0.4;cursor:not-allowed}
.demo-note{margin-top:1rem;font-family:'SF Mono','Fira Code',monospace;font-size:11px;color:var(--muted);text-align:center}
.mission{max-width:650px}
.mission h2{font-size:1.5rem;font-weight:400;letter-spacing:-0.02em;line-height:1.5;margin-bottom:1.5rem}
.mission p{font-family:'SF Mono','Fira Code',monospace;font-size:13px;color:var(--muted);line-height:1.7}
.section-cta{text-align:center;padding:5rem 3rem;margin-top:-1px}
.cta-content h2{font-size:1.75rem;font-weight:500;letter-spacing:-0.02em;margin-bottom:0.75rem}
.cta-content p{font-family:'SF Mono','Fira Code',monospace;font-size:13px;color:var(--muted);margin-bottom:2rem}
.cta-buttons{display:flex;gap:1rem;justify-content:center;flex-wrap:wrap}
.cta-primary{padding:14px 32px;background:var(--text);color:var(--bg);text-decoration:none;font-family:'SF Mono','Fira Code',monospace;font-size:14px;transition:all 0.2s}
.cta-primary:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,0.15)}
.cta-secondary{padding:14px 32px;background:transparent;color:var(--text);border:1px solid var(--border);font-family:'SF Mono','Fira Code',monospace;font-size:14px;cursor:pointer;transition:all 0.2s}
.cta-secondary:hover{border-color:var(--text)}
footer{padding:4rem 3rem;background:var(--text);color:var(--bg);margin-top:-1px}
.footer-content{display:flex;justify-content:space-between;align-items:flex-start;gap:3rem;max-width:900px}
.footer-brand{flex:1}
.footer-logo{font-family:'SF Mono','Fira Code',monospace;font-weight:600;font-size:16px;margin-bottom:0.5rem}
.footer-tagline{font-family:'SF Mono','Fira Code',monospace;font-size:12px;color:var(--muted);margin-bottom:1.5rem}
.footer-copy{font-family:'SF Mono','Fira Code',monospace;font-size:11px;color:var(--muted)}
.footer-links{display:flex;gap:3rem}
.footer-col h4{font-family:'SF Mono','Fira Code',monospace;font-size:11px;text-transform:uppercase;letter-spacing:0.1em;color:var(--muted);margin-bottom:1rem;font-weight:normal}
.footer-col a{display:flex;align-items:center;gap:0.5rem;font-family:'SF Mono','Fira Code',monospace;font-size:13px;color:var(--bg);text-decoration:none;padding:0.35rem 0;transition:opacity 0.15s}
.footer-col a:hover{opacity:0.7}
.footer-col a svg{width:16px;height:16px}
.demo-modal{display:none;position:fixed;inset:0;z-index:1000;align-items:center;justify-content:center}
.demo-modal.active{display:flex}
.demo-modal-backdrop{position:absolute;inset:0;background:rgba(0,0,0,0.5);backdrop-filter:blur(4px)}
.demo-modal-content{position:relative;background:white;border:1px solid var(--border);width:100%;max-width:420px;margin:1rem;box-shadow:0 25px 50px -12px rgba(0,0,0,0.25)}
.demo-step{display:none;padding:2rem}
.demo-step.active{display:block;animation:fadeIn 0.3s ease}
.demo-step-header{margin-bottom:1.5rem}
.demo-step-indicator{font-family:'SF Mono','Fira Code',monospace;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.1em;display:block;margin-bottom:0.75rem}
.demo-step-header h2{font-size:1.25rem;font-weight:500;margin-bottom:0.5rem}
.demo-step-header p{font-family:'SF Mono','Fira Code',monospace;font-size:12px;color:var(--muted)}
.demo-name-input{width:100%;padding:14px 16px;border:1px solid var(--border);font-family:inherit;font-size:15px;outline:none;transition:border-color 0.15s}
.demo-name-input:focus{border-color:var(--text)}
.demo-step-footer{display:flex;justify-content:flex-end;gap:0.75rem;margin-top:1.5rem}
.demo-next,.demo-launch{padding:12px 24px;background:var(--text);color:var(--bg);border:none;font-family:'SF Mono','Fira Code',monospace;font-size:13px;cursor:pointer;transition:all 0.2s}
.demo-next:hover:not(:disabled),.demo-launch:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,0.15)}
.demo-next:disabled,.demo-launch:disabled{opacity:0.4;cursor:not-allowed}
.demo-back{padding:12px 24px;background:transparent;color:var(--muted);border:1px solid var(--border);font-family:'SF Mono','Fira Code',monospace;font-size:13px;cursor:pointer;transition:all 0.15s}
.demo-back:hover{border-color:var(--text);color:var(--text)}
.demo-launch{background:linear-gradient(135deg,#10b981,#06b6d4)}
.demo-launch:hover:not(:disabled){box-shadow:0 8px 24px rgba(16,185,129,0.3)}
.color-picker{display:flex;gap:0.75rem;flex-wrap:wrap}
.color-swatch{width:48px;height:48px;border:2px solid transparent;cursor:pointer;transition:all 0.15s;position:relative}
.color-swatch:hover{transform:scale(1.1)}
.color-swatch.selected{border-color:var(--text)}
.color-swatch.selected::after{content:'✓';position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:white;font-size:18px;text-shadow:0 1px 2px rgba(0,0,0,0.3)}
.launch-progress{margin-top:1.5rem;padding:1rem;background:#f5f5f5;border:1px solid var(--border)}
.launch-progress.active{display:block;animation:fadeIn 0.3s ease}
.progress-step{display:flex;align-items:center;gap:0.75rem;padding:0.5rem 0;font-family:'SF Mono','Fira Code',monospace;font-size:12px;color:var(--muted)}
.progress-step.active{color:var(--text)}
.progress-step.done{color:var(--primary)}
.progress-dot{width:8px;height:8px;background:var(--border);transition:all 0.3s}
.progress-step.active .progress-dot{background:var(--text);animation:pulse 1s infinite}
.progress-step.done .progress-dot{background:var(--primary)}
.launch-success{text-align:center;padding:2rem}
.launch-success.active{display:block;animation:successPop 0.5s cubic-bezier(0.34,1.56,0.64,1)}
.success-icon{width:48px;height:48px;background:linear-gradient(135deg,#10b981,#06b6d4);display:flex;align-items:center;justify-content:center;margin:0 auto 1rem}
.success-icon svg{width:24px;height:24px;color:white}
.success-url{font-family:'SF Mono','Fira Code',monospace;font-size:14px;font-weight:500;margin-bottom:0.5rem}
.success-redirect{font-family:'SF Mono','Fira Code',monospace;font-size:12px;color:var(--muted)}
.demo-modal .launch-progress{display:block;margin-top:0;padding:0;background:transparent;border:none}
.demo-modal .launch-success{display:block;padding:1rem 0}
@keyframes fadeIn{from{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}
@keyframes pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.3)}}
@keyframes successPop{0%{transform:scale(0.8);opacity:0}100%{transform:scale(1);opacity:1}}
@media(max-width:1024px){.features-grid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:900px){.layout{grid-template-columns:1fr}aside{position:relative;height:auto;border-right:none;border-bottom:1px solid var(--border);flex-direction:row;align-items:center;justify-content:space-between;padding:1rem 1.5rem}.sidebar-tagline{margin-bottom:0}.sidebar-nav,.sidebar-divider,.sidebar-label,.sidebar-footer{display:none}.hero,.section{padding:3rem 2rem}footer{padding:3rem 2rem}.footer-content{flex-direction:column}}
@media(max-width:768px){.features-grid{grid-template-columns:1fr}}
@media(max-width:480px){.hero,.section{padding:2rem 1.5rem}.footer-links{flex-direction:column;gap:2rem}}
< / style >
< / head >
< body >
< div class = "layout" >
< aside >
< div >
< div class = "sidebar-logo" > WriteKit< / div >
< div class = "sidebar-tagline" > Blogging Platform< / div >
2026-01-09 00:16:46 +02:00
< / div >
2026-01-12 02:02:13 +02:00
< nav class = "sidebar-nav" >
< a href = "#why" > Why WriteKit< / a >
< a href = "#features" > Features< / a >
< a href = "/signup" style = "color: var(--primary)" > Create Blog →< / a >
< / nav >
< div class = "sidebar-divider" > < / div >
< div class = "sidebar-label" > Resources< / div >
< nav class = "sidebar-nav" >
< a href = "/docs" > Documentation< / a >
< a href = "/discord" > Discord< / a >
< / nav >
< div class = "sidebar-footer" >
< div class = "env-badge" > ALPHA< / div >
< div class = "version-badge" title = "{{COMMIT}}" > {{VERSION}}< / div >
2026-01-09 00:16:46 +02:00
< / div >
2026-01-12 02:02:13 +02:00
< / aside >
< main >
< section class = "hero" >
< canvas class = "hero-canvas" id = "dither-canvas" > < / canvas >
< p class = "hero-label" > Blog Hosting for Developers / 2025< / p >
< h1 > Your Words,< br / > Your Platform< / h1 >
< p class = "hero-sub" >
Spin up a beautiful, fast blog in seconds.
< code > SQLite< / code > -powered, < code > markdown< / code > -native,
infinitely customizable.
< / p >
< div class = "hero-cta" >
< button class = "demo-btn" id = "try-demo" > Try Demo< / button >
< p class = "demo-note" >
{{DEMO_MINUTES}} minute demo.
< a href = "/signup" style = "color: var(--primary)"
>Create a real blog< /a
>
instead.
< / p >
2026-01-09 00:16:46 +02:00
< / div >
2026-01-12 02:02:13 +02:00
< / section >
< section class = "section" id = "why" >
< p class = "section-label" > Why WriteKit< / p >
< div class = "mission" >
< h2 > Blogging got complicated.< / h2 >
< p >
Ghost is heavy. Hashnode is bloated. Medium doesn't care about
developers. Hugo outputs static sites — great, until you want
comments, logins and analytics without bolting on five
services.< br / > < br / > WriteKit is a blogging platform for
productive developers. Everything works out of the box. Own your
data from day one.
< / p >
< / div >
< / section >
< section class = "section-features" id = "features" >
< div class = "features-grid" >
< div class = "feature" >
< p class = "feature-num" > 01< / p >
< h3 > Comments & Reactions< / h3 >
< p >
Threaded comments and emoji reactions. No Disqus, no third-party
scripts.
< / p >
2026-01-09 00:16:46 +02:00
< / div >
2026-01-12 02:02:13 +02:00
< div class = "feature" >
< p class = "feature-num" > 02< / p >
< h3 > Full-text Search< / h3 >
< p >
SQLite FTS5 powers instant search. Fast, local, no external
service.
< / p >
2026-01-09 00:16:46 +02:00
< / div >
2026-01-12 02:02:13 +02:00
< div class = "feature" >
< p class = "feature-num" > 03< / p >
< h3 > Privacy-first Analytics< / h3 >
< p >
Views, referrers, browsers — no cookies, no tracking pixels.
< / p >
2026-01-09 00:16:46 +02:00
< / div >
2026-01-12 02:02:13 +02:00
< div class = "feature" >
< p class = "feature-num" > 04< / p >
< h3 > REST API< / h3 >
< p >
Full programmatic access. Create posts, manage content, build
integrations.
< / p >
2026-01-09 00:16:46 +02:00
< / div >
2026-01-12 02:02:13 +02:00
< div class = "feature" >
< p class = "feature-num" > 05< / p >
< h3 > Markdown Native< / h3 >
< p >
Write how you already write. YAML frontmatter, syntax
highlighting.
< / p >
< / div >
< div class = "feature" >
< p class = "feature-num" > 06< / p >
< h3 > Custom Domains< / h3 >
< p > Your domain or *.writekit.dev. SSL included automatically.< / p >
< / div >
< div class = "feature" >
< p class = "feature-num" > 07< / p >
< h3 > Own Your Data< / h3 >
< p >
Export anytime. JSON, Markdown, full backup. No lock-in ever.
< / p >
< / div >
< div class = "feature" >
< p class = "feature-num" > 08< / p >
< h3 > One-click Deploy< / h3 >
< p >
No DevOps required. Create your very own blog with one click.
< / p >
2026-01-09 00:16:46 +02:00
< / div >
< / div >
2026-01-12 02:02:13 +02:00
< / section >
< section class = "section section-cta" id = "cta" >
< div class = "cta-content" >
< h2 > Ready to start writing?< / h2 >
< p > Deploy your blog in seconds. No credit card required.< / p >
< div class = "cta-buttons" >
< a href = "/signup" class = "cta-primary" > Create Your Blog< / a >
< button class = "cta-secondary" id = "try-demo-bottom" >
Try Demo First
< / button >
2026-01-09 00:16:46 +02:00
< / div >
< / div >
2026-01-12 02:02:13 +02:00
< / section >
< div class = "demo-modal" id = "demo-modal" >
< div class = "demo-modal-backdrop" > < / div >
< div class = "demo-modal-content" >
< div class = "demo-step active" data-step = "1" >
< div class = "demo-step-header" >
< span class = "demo-step-indicator" > 1 / 2< / span >
< h2 > What's your name?< / h2 >
< p > We'll use this to personalize your blog.< / p >
< / div >
< input
type="text"
id="demo-name"
class="demo-name-input"
placeholder="Your name"
autofocus
autocomplete="off"
/>
< div class = "demo-step-footer" >
< button class = "demo-back" id = "demo-skip" > Skip< / button >
< button class = "demo-next" id = "demo-next-1" disabled >
Next →
< / button >
< / div >
< / div >
< div class = "demo-step" data-step = "2" >
< div class = "demo-step-header" >
< span class = "demo-step-indicator" > 2 / 2< / span >
< h2 > Pick a color< / h2 >
< p > Choose an accent color for your blog.< / p >
< / div >
< div class = "color-picker" >
< button
class="color-swatch selected"
data-color="#10b981"
style="background: #10b981"
title="Emerald"
>< / button >
< button
class="color-swatch"
data-color="#3b82f6"
style="background: #3b82f6"
title="Blue"
>< / button >
< button
class="color-swatch"
data-color="#8b5cf6"
style="background: #8b5cf6"
title="Purple"
>< / button >
< button
class="color-swatch"
data-color="#f97316"
style="background: #f97316"
title="Orange"
>< / button >
< button
class="color-swatch"
data-color="#ef4444"
style="background: #ef4444"
title="Rose"
>< / button >
< button
class="color-swatch"
data-color="#64748b"
style="background: #64748b"
title="Slate"
>< / button >
< / div >
< div class = "demo-step-footer" >
< button class = "demo-back" id = "demo-back-2" > ← Back< / button >
< button class = "demo-launch" id = "demo-launch" >
Launch Demo
< / button >
< / div >
< / div >
< div class = "demo-step" data-step = "3" >
< div class = "demo-step-header" >
< h2 > Launching your demo...< / h2 >
< / div >
< div class = "launch-progress active" >
< div class = "progress-step" data-step = "1" >
< span class = "progress-dot" > < /span
>< span > Creating database...< / span >
< / div >
< div class = "progress-step" data-step = "2" >
< span class = "progress-dot" > < /span
>< span > Configuring settings...< / span >
< / div >
< div class = "progress-step" data-step = "3" >
< span class = "progress-dot" > < /span
>< span > Adding welcome post...< / span >
< / div >
< div class = "progress-step" data-step = "4" >
< span class = "progress-dot" > < / span > < span > Ready!< / span >
< / div >
< / div >
< / div >
< div class = "demo-step" data-step = "4" >
< div class = "launch-success active" >
< div class = "success-icon" >
< svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
< path
stroke-linecap="round"
stroke-linejoin="round"
d="M5 13l4 4L19 7"
/>
< / svg >
< / div >
< div class = "success-url" id = "success-url" > < / div >
< div class = "success-redirect" > Redirecting you now...< / div >
< / div >
2026-01-09 00:16:46 +02:00
< / div >
< / div >
< / div >
2026-01-12 02:02:13 +02:00
< footer >
< div class = "footer-content" >
< div class = "footer-brand" >
< div class = "footer-logo" > WriteKit< / div >
< div class = "footer-tagline" > Your Words, Your Platform< / div >
< div class = "footer-copy" >
© 2025 WriteKit. All rights reserved.
< / div >
2026-01-09 00:16:46 +02:00
< / div >
2026-01-12 02:02:13 +02:00
< div class = "footer-links" >
< div class = "footer-col" >
< h4 > Product< / h4 >
< a href = "#features" > Features< / a >
< a href = "/signup" > Create Blog< / a >
< a href = "/docs" > Documentation< / a >
< / div >
< div class = "footer-col" >
< h4 > Community< / h4 >
< a href = "/discord"
>< svg viewBox = "0 0 24 24" fill = "currentColor" >
< path
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"
/>< /svg
>Discord< /a
>
< / div >
2026-01-09 00:16:46 +02:00
< / div >
< / div >
2026-01-12 02:02:13 +02:00
< / footer >
< / main >
< / div >
< script >
const $ = (s) => document.querySelector(s);
const $$ = (s) => document.querySelectorAll(s);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
const modal = $("#demo-modal");
const backdrop = modal.querySelector(".demo-modal-backdrop");
const nameInput = $("#demo-name");
const nextBtn = $("#demo-next-1");
const backBtn = $("#demo-back-2");
const launchBtn = $("#demo-launch");
const successUrl = $("#success-url");
const colorSwatches = $$(".color-swatch");
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
let demoName = "";
let demoColor = "#10b981";
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
const openDemoModal = () => {
modal.classList.add("active");
setTimeout(() => nameInput.focus(), 100);
};
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
const resetModal = () => {
nameInput.value = "";
demoName = "";
nextBtn.disabled = true;
goToModalStep(1);
colorSwatches.forEach((s) => s.classList.remove("selected"));
colorSwatches[0].classList.add("selected");
demoColor = "#10b981";
};
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
const goToModalStep = (step) => {
modal
.querySelectorAll(".demo-step")
.forEach((el) => el.classList.remove("active"));
modal
.querySelector(`.demo-step[data-step="${step}"]`)
.classList.add("active");
if (step === 1) setTimeout(() => nameInput.focus(), 100);
};
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
const setProgressStep = (n) => {
modal.querySelectorAll(".progress-step").forEach((el) => {
const step = parseInt(el.dataset.step);
el.classList.remove("active", "done");
if (step < n ) el . classList . add ( " done " ) ;
if (step === n) el.classList.add("active");
});
};
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
const launchDemo = async () => {
if (!demoName) return;
launchBtn.disabled = true;
launchBtn.textContent = "Launching...";
goToModalStep(3);
setProgressStep(1);
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
try {
const res = await fetch("/api/demo", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: demoName, color: demoColor }),
});
const data = await res.json();
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
if (res.ok & & data.url) {
setProgressStep(2);
await sleep(150);
setProgressStep(3);
await sleep(150);
setProgressStep(4);
await sleep(150);
goToModalStep(4);
successUrl.textContent = data.url;
await sleep(300);
location.href = data.url;
} else {
goToModalStep(2);
launchBtn.disabled = false;
launchBtn.textContent = "Launch Demo";
alert(data.error || "Failed to create demo");
}
} catch {
goToModalStep(2);
launchBtn.disabled = false;
launchBtn.textContent = "Launch Demo";
alert("Error creating demo");
2026-01-09 00:16:46 +02:00
}
2026-01-12 02:02:13 +02:00
};
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
$("#try-demo").addEventListener("click", openDemoModal);
$("#try-demo-bottom").addEventListener("click", openDemoModal);
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
backdrop.addEventListener("click", () => {
if (
!launchBtn.disabled ||
modal.querySelector('.demo-step[data-step="1"].active') ||
modal.querySelector('.demo-step[data-step="2"].active')
) {
modal.classList.remove("active");
resetModal();
2026-01-09 00:16:46 +02:00
}
2026-01-12 02:02:13 +02:00
});
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" & & modal.classList.contains("active")) {
if (
modal.querySelector('.demo-step[data-step="1"].active') ||
modal.querySelector('.demo-step[data-step="2"].active')
) {
modal.classList.remove("active");
resetModal();
}
}
if (e.key === "Enter" & & modal.classList.contains("active")) {
if (
modal.querySelector('.demo-step[data-step="1"].active') & &
!nextBtn.disabled
)
goToModalStep(2);
else if (modal.querySelector('.demo-step[data-step="2"].active'))
launchDemo();
}
});
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
nameInput.addEventListener("input", () => {
demoName = nameInput.value.trim();
nextBtn.disabled = !demoName.length;
});
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
nextBtn.addEventListener("click", () => goToModalStep(2));
backBtn.addEventListener("click", () => goToModalStep(1));
launchBtn.addEventListener("click", launchDemo);
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
$("#demo-skip").addEventListener("click", () => {
demoName = "Demo User";
launchDemo();
});
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
colorSwatches.forEach((swatch) => {
swatch.addEventListener("click", () => {
colorSwatches.forEach((s) => s.classList.remove("selected"));
swatch.classList.add("selected");
demoColor = swatch.dataset.color;
});
});
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
$$('a[href^="#"]').forEach((anchor) => {
anchor.addEventListener("click", (e) => {
e.preventDefault();
document
.querySelector(anchor.getAttribute("href"))
?.scrollIntoView({ behavior: "smooth" });
});
});
(() => {
const settings = {
pixelSize: 8,
gridSize: 4,
speed: 0.05,
colorShift: 0.6,
colors: {
bright: [0.063, 0.725, 0.506],
mid: [0.047, 0.545, 0.38],
dark: [0.031, 0.363, 0.253],
bg: [0.2, 0.2, 0.2],
},
};
const canvas = $("#dither-canvas");
if (!canvas) return;
const gl = canvas.getContext("webgl2");
if (!gl) return;
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
const vs = `#version 300 es
2026-01-09 00:16:46 +02:00
in vec2 a_position;
out vec2 v_uv;
2026-01-12 02:02:13 +02:00
void main() { v_uv = a_position * 0.5 + 0.5; gl_Position = vec4(a_position, 0.0, 1.0); }`;
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
const fs = `#version 300 es
2026-01-09 00:16:46 +02:00
precision highp float;
in vec2 v_uv;
out vec4 fragColor;
uniform vec2 u_resolution;
uniform float u_time, u_pixelSize, u_gridSize, u_speed, u_colorShift;
uniform vec3 u_color1, u_color2, u_color3, u_bgColor;
float bayer8(vec2 p) { ivec2 P = ivec2(mod(floor(p), 8.0)); int i = P.x + P.y * 8; int b[64] = int[64](0,32,8,40,2,34,10,42,48,16,56,24,50,18,58,26,12,44,4,36,14,46,6,38,60,28,52,20,62,30,54,22,3,35,11,43,1,33,9,41,51,19,59,27,49,17,57,25,15,47,7,39,13,45,5,37,63,31,55,23,61,29,53,21); return float(b[i]) / 64.0; }
float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
float noise(vec2 p) { vec2 i = floor(p), f = fract(p); f = f * f * (3.0 - 2.0 * f); return mix(mix(hash(i), hash(i + vec2(1.0, 0.0)), f.x), mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), f.x), f.y); }
float fbm(vec2 p) { float v = 0.0, a = 0.5; for (int i = 0; i < 5 ; i + + ) { v + = a * noise ( p ) ; p * = 2 . 0 ; a * = 0 . 5 ; } return v ; }
void main() {
vec2 pixelUV = floor(v_uv * u_resolution / u_pixelSize) * u_pixelSize / u_resolution;
vec2 p = pixelUV * 3.0; float t = u_time * u_speed;
float pattern = fbm(p + vec2(t * 0.5, t * 0.3)) * 0.5 + fbm(p * 1.5 - vec2(t * 0.4, -t * 0.2)) * 0.3 + fbm(p * 0.5 + vec2(-t * 0.3, t * 0.5)) * 0.2 + 0.1 * sin(p.x * 2.0 + t) * sin(p.y * 2.0 - t * 0.7);
float luma = clamp(smoothstep(0.1, 0.9, pow(pattern, 0.7)) + u_colorShift * sin(u_time * 0.3) * 0.3, 0.0, 1.0);
float threshold = bayer8(floor(v_uv * u_resolution / u_gridSize));
float level = luma * 3.0; int band = int(floor(level)); float frac = fract(level);
vec3 result = band >= 2 ? (frac > threshold ? u_color1 : u_color2) : band == 1 ? (frac > threshold ? u_color2 : u_color3) : (frac > threshold ? u_color3 : u_bgColor);
fragColor = vec4(result, 1.0);
2026-01-12 02:02:13 +02:00
}`;
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
const createShader = (type, src) => {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
return gl.getShaderParameter(s, gl.COMPILE_STATUS) ? s : null;
};
const vertexShader = createShader(gl.VERTEX_SHADER, vs);
const fragmentShader = createShader(gl.FRAGMENT_SHADER, fs);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) return;
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
gl.STATIC_DRAW
);
const posLoc = gl.getAttribLocation(program, "a_position");
gl.enableVertexAttribArray(posLoc);
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
const u = {
resolution: gl.getUniformLocation(program, "u_resolution"),
time: gl.getUniformLocation(program, "u_time"),
pixelSize: gl.getUniformLocation(program, "u_pixelSize"),
gridSize: gl.getUniformLocation(program, "u_gridSize"),
speed: gl.getUniformLocation(program, "u_speed"),
colorShift: gl.getUniformLocation(program, "u_colorShift"),
color1: gl.getUniformLocation(program, "u_color1"),
color2: gl.getUniformLocation(program, "u_color2"),
color3: gl.getUniformLocation(program, "u_color3"),
bgColor: gl.getUniformLocation(program, "u_bgColor"),
};
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
gl.useProgram(program);
gl.uniform3fv(u.color1, settings.colors.bright);
gl.uniform3fv(u.color2, settings.colors.mid);
gl.uniform3fv(u.color3, settings.colors.dark);
gl.uniform3fv(u.bgColor, settings.colors.bg);
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
const resize = () => {
const dpr = Math.min(devicePixelRatio, 2);
canvas.width = canvas.offsetWidth * dpr;
canvas.height = canvas.offsetHeight * dpr;
};
resize();
addEventListener("resize", resize);
2026-01-09 00:16:46 +02:00
2026-01-12 02:02:13 +02:00
const render = (time) => {
time *= 0.001;
gl.viewport(0, 0, canvas.width, canvas.height);
gl.useProgram(program);
gl.bindVertexArray(vao);
gl.uniform2f(u.resolution, canvas.width, canvas.height);
gl.uniform1f(u.time, time);
gl.uniform1f(u.pixelSize, settings.pixelSize);
gl.uniform1f(u.gridSize, settings.gridSize);
gl.uniform1f(u.speed, settings.speed);
gl.uniform1f(u.colorShift, settings.colorShift);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
requestAnimationFrame(render);
};
requestAnimationFrame(render);
})();
< / script >
< / body >
2026-01-09 00:16:46 +02:00
< / html >