Refine home page: traffic-grid hero, editorial feature rail, real platform marks

- Replace hero shield with HeroVisual: 11-track flowing SVG of request glyphs
  with ~7% Apple-red blocked highlights, slow CSS-only drift, edge fade,
  monospace meta header and pass/blocked footer (data-driven feel).
- Drop the six default feature cards. New HomeFeatureRail renders a 3x2
  hairline-bordered editorial grid: numbered eyebrow + bold title + body,
  zero icon chrome.
- Redraw platform icons as recognizable brand marks (Nginx hexagon-N, Apache
  feather, Traefik "Mr. Traefik" head, HAProxy load-balanced H). Showcase
  cards drop card chrome in favor of column dividers; hover adopts the
  platform's brand color via per-card --accent CSS var.
- Stats strip becomes a hairline-bordered four-column rail with tabular-num
  values, mono sub-labels, and a Google-data-display feel.
- Hero name no longer uses gradient text; pure neutral.
- Code-block bg corrected for light mode.
- Respects prefers-reduced-motion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Fabrizio Salmi
2026-05-01 09:39:20 +02:00
parent 8cd150af87
commit 075f92d2a6
11 changed files with 933 additions and 537 deletions

View File

@@ -0,0 +1,264 @@
<script setup lang="ts">
// Deterministic pseudo-random sequence so SSR and client agree.
function lcg(seed: number) {
let n = seed
return () => {
n = (n * 1103515245 + 12345) % 2147483648
return n / 2147483648
}
}
type Seg = { x: number; w: number; blocked: boolean }
function buildTrack(seed: number): Seg[] {
const r = lcg(seed)
const segs: Seg[] = []
let x = 0
const widths = [4, 6, 8, 10, 14, 18, 22]
const gaps = [4, 5, 6, 8, 10]
while (x < 480) {
const w = widths[Math.floor(r() * widths.length)]
const gap = gaps[Math.floor(r() * gaps.length)]
const blocked = r() < 0.07 // ~7%
segs.push({ x, w, blocked })
x += w + gap
}
return segs
}
const trackCount = 11
const tracks = Array.from({ length: trackCount }, (_, i) => ({
y: 18 + i * 26,
delay: -(i * 2.7),
duration: 36 + (i % 3) * 6, // slight variance per track
segs: buildTrack(i + 7)
}))
</script>
<template>
<div class="hero-visual" aria-hidden="true">
<div class="hero-visual-frame">
<div class="hero-visual-meta">
<span class="meta-label">// inbound · request inspection</span>
<span class="meta-live">
<span class="live-dot" />
live
</span>
</div>
<svg
class="hero-visual-svg"
viewBox="0 0 480 320"
preserveAspectRatio="xMidYMid slice"
>
<defs>
<linearGradient id="edge-fade" x1="0" x2="1" y1="0" y2="0">
<stop offset="0" stop-color="#fff" stop-opacity="0" />
<stop offset="0.08" stop-color="#fff" stop-opacity="1" />
<stop offset="0.92" stop-color="#fff" stop-opacity="1" />
<stop offset="1" stop-color="#fff" stop-opacity="0" />
</linearGradient>
<mask id="edge-mask">
<rect x="0" y="0" width="480" height="320" fill="url(#edge-fade)" />
</mask>
</defs>
<g mask="url(#edge-mask)">
<g
v-for="(t, i) in tracks"
:key="i"
:transform="`translate(0, ${t.y})`"
>
<g
class="drift"
:style="{
'--drift-delay': `${t.delay}s`,
'--drift-duration': `${t.duration}s`
}"
>
<!-- two copies side-by-side give a seamless loop -->
<g v-for="copy in 2" :key="copy" :transform="`translate(${(copy - 1) * 480}, 0)`">
<template v-for="(s, j) in t.segs" :key="j">
<rect
:x="s.x"
y="-2"
:width="s.w"
height="4"
rx="2"
:class="s.blocked ? 'seg-blocked' : 'seg-normal'"
/>
</template>
</g>
</g>
</g>
</g>
</svg>
<div class="hero-visual-foot">
<span class="foot-stat">
<span class="stat-num">99.93<span class="stat-unit">%</span></span>
<span class="stat-key">pass</span>
</span>
<span class="foot-sep" />
<span class="foot-stat foot-stat--blocked">
<span class="stat-num">0.07<span class="stat-unit">%</span></span>
<span class="stat-key">blocked</span>
</span>
</div>
</div>
</div>
</template>
<style scoped>
.hero-visual {
position: relative;
width: 100%;
max-width: 540px;
margin-left: auto;
}
.hero-visual-frame {
position: relative;
border-radius: 18px;
border: 1px solid var(--vp-c-divider);
background:
linear-gradient(180deg, var(--vp-c-bg-elv), var(--vp-c-bg-alt));
padding: 18px 18px 14px;
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.04) inset,
0 24px 60px -28px rgba(0, 0, 0, 0.18);
overflow: hidden;
}
.dark .hero-visual-frame {
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.04) inset,
0 24px 60px -20px rgba(0, 0, 0, 0.7);
}
.hero-visual-meta {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
font-family: var(--vp-font-family-mono);
font-size: 11px;
letter-spacing: 0.04em;
color: var(--vp-c-text-3);
}
.meta-label {
text-transform: lowercase;
}
.meta-live {
display: inline-flex;
align-items: center;
gap: 6px;
text-transform: uppercase;
font-size: 10px;
letter-spacing: 0.12em;
color: var(--vp-c-text-2);
}
.live-dot {
width: 6px;
height: 6px;
border-radius: 999px;
background: #34c759; /* Apple system green */
box-shadow: 0 0 0 0 rgba(52, 199, 89, 0.45);
animation: live-pulse 2.4s ease-out infinite;
}
@keyframes live-pulse {
0% { box-shadow: 0 0 0 0 rgba(52, 199, 89, 0.45); }
70% { box-shadow: 0 0 0 6px rgba(52, 199, 89, 0); }
100% { box-shadow: 0 0 0 0 rgba(52, 199, 89, 0); }
}
.hero-visual-svg {
width: 100%;
height: 280px;
display: block;
}
.seg-normal {
fill: var(--vp-c-text-3);
fill-opacity: 0.32;
}
.dark .seg-normal {
fill-opacity: 0.42;
}
.seg-blocked {
fill: #ff453a; /* Apple system red */
fill-opacity: 0.95;
}
.drift {
animation: drift var(--drift-duration, 36s) linear infinite;
animation-delay: var(--drift-delay, 0s);
will-change: transform;
}
@keyframes drift {
from { transform: translateX(0); }
to { transform: translateX(-480px); }
}
.hero-visual-foot {
display: flex;
align-items: center;
gap: 16px;
padding-top: 12px;
margin-top: 4px;
border-top: 1px solid var(--vp-c-divider);
font-family: var(--vp-font-family-mono);
font-variant-numeric: tabular-nums;
}
.foot-stat {
display: inline-flex;
align-items: baseline;
gap: 8px;
}
.stat-num {
font-size: 15px;
font-weight: 600;
letter-spacing: -0.01em;
color: var(--vp-c-text-1);
}
.stat-unit {
font-size: 11px;
color: var(--vp-c-text-3);
margin-left: 1px;
}
.stat-key {
font-size: 10px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--vp-c-text-3);
}
.foot-stat--blocked .stat-num {
color: #ff453a;
}
.foot-sep {
flex: 1;
height: 1px;
background: var(--vp-c-divider);
}
@media (prefers-reduced-motion: reduce) {
.drift {
animation: none;
}
.live-dot {
animation: none;
}
}
</style>

View File

@@ -0,0 +1,157 @@
<script setup lang="ts">
const features = [
{
eyebrow: '01',
title: 'OWASP CRS coverage',
body: 'Rules for SQL injection, XSS, RCE, LFI, and RFI, derived from the same Core Rule Set behind ModSecurity.'
},
{
eyebrow: '02',
title: 'Native multi-server output',
body: 'One source, four idiomatic backends — Nginx maps, Apache SecRule, Traefik middleware, HAProxy ACL files.'
},
{
eyebrow: '03',
title: 'Bad-bot blocking',
body: 'Curated User-Agent lists from public sources — scrapers, AI crawlers, scanners, with allow-lists for legitimate engines.'
},
{
eyebrow: '04',
title: 'Daily automated rebuild',
body: 'A scheduled GitHub Actions workflow re-fetches the latest CRS release and republishes every archive — no maintenance.'
},
{
eyebrow: '05',
title: 'Pre-built archives',
body: 'Drop-in zips published on every run: nginx_waf.zip, apache_waf.zip, traefik_waf.zip, haproxy_waf.zip.'
},
{
eyebrow: '06',
title: 'Composable pipeline',
body: 'Each backend is a small Python converter on a single JSON intermediate. Adding a platform is a few hundred lines.'
}
]
</script>
<template>
<section class="home-rail">
<header class="home-rail-head">
<span class="home-eyebrow">Capabilities</span>
<h2 class="home-title">Engineered for production</h2>
<p class="home-lede">
Six guarantees that turn a daily scrape of upstream rules into something your traffic can actually
live behind &mdash; quietly, predictably, and without operator toil.
</p>
</header>
<ul class="rail-grid">
<li v-for="f in features" :key="f.title" class="rail-item">
<span class="rail-eyebrow">{{ f.eyebrow }}</span>
<h3 class="rail-title">{{ f.title }}</h3>
<p class="rail-body">{{ f.body }}</p>
</li>
</ul>
</section>
</template>
<style scoped>
.home-rail {
max-width: 1120px;
margin: 0 auto;
padding: 24px 24px 80px;
}
.home-rail-head {
margin-bottom: 56px;
max-width: 720px;
}
.rail-grid {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0;
border-top: 1px solid var(--vp-c-divider);
}
@media (max-width: 900px) {
.rail-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 600px) {
.rail-grid {
grid-template-columns: 1fr;
}
}
.rail-item {
position: relative;
padding: 28px 24px 32px 0;
border-bottom: 1px solid var(--vp-c-divider);
}
.rail-item:not(:nth-child(3n)) {
border-right: 1px solid var(--vp-c-divider);
padding-right: 24px;
}
.rail-item:not(:nth-child(3n)) {
padding-left: 24px;
}
.rail-item:nth-child(3n+1) {
padding-left: 0;
}
@media (max-width: 900px) {
.rail-item:not(:nth-child(3n)) {
border-right: none;
}
.rail-item:nth-child(odd) {
border-right: 1px solid var(--vp-c-divider);
padding-right: 24px;
padding-left: 0;
}
.rail-item:nth-child(even) {
padding-left: 24px;
padding-right: 0;
}
}
@media (max-width: 600px) {
.rail-item,
.rail-item:nth-child(odd),
.rail-item:nth-child(even) {
border-right: none !important;
padding: 24px 0 24px 0;
}
}
.rail-eyebrow {
display: inline-block;
font-family: var(--vp-font-family-mono);
font-size: 11px;
font-weight: 500;
letter-spacing: 0.08em;
color: var(--vp-c-text-3);
margin-bottom: 14px;
}
.rail-title {
font-size: 1.05rem;
font-weight: 600;
letter-spacing: -0.015em;
color: var(--vp-c-text-1);
margin: 0 0 8px;
line-height: 1.3;
}
.rail-body {
font-size: 0.92rem;
line-height: 1.55;
color: var(--vp-c-text-2);
margin: 0;
}
</style>

View File

@@ -1,63 +1,62 @@
<script setup>
<script setup lang="ts">
import { withBase } from 'vitepress'
import IconNginx from './icons/IconNginx.vue'
import IconApache from './icons/IconApache.vue'
import IconTraefik from './icons/IconTraefik.vue'
import IconHaproxy from './icons/IconHaproxy.vue'
import IconArrow from './icons/IconArrow.vue'
import { withBase } from 'vitepress'
const platforms = [
{ name: 'Nginx', meta: 'map directives + if rules', href: '/nginx', icon: IconNginx },
{ name: 'Apache', meta: 'ModSecurity SecRule directives', href: '/apache', icon: IconApache },
{ name: 'Traefik', meta: 'Middleware TOML configuration', href: '/traefik', icon: IconTraefik },
{ name: 'HAProxy', meta: 'ACL pattern files', href: '/haproxy', icon: IconHaproxy }
{ name: 'Nginx', meta: 'map directives + if rules', href: '/nginx', icon: IconNginx, accent: '#009639' },
{ name: 'Apache', meta: 'ModSecurity SecRule directives', href: '/apache', icon: IconApache, accent: '#d22128' },
{ name: 'Traefik', meta: 'Middleware TOML configuration', href: '/traefik', icon: IconTraefik, accent: '#24a1c1' },
{ name: 'HAProxy', meta: 'ACL pattern files', href: '/haproxy', icon: IconHaproxy, accent: '#7cc242' }
]
const stats = [
{ value: '600+', label: 'OWASP CRS patterns', sub: 'Extracted from upstream daily' },
{ value: 'Daily', label: 'Refresh cadence', sub: 'Scheduled GitHub Actions' },
{ value: '4', label: 'Server backends', sub: 'Native, idiomatic output' },
{ value: 'MIT', label: 'License', sub: 'Open-source, no vendor lock' }
]
</script>
<template>
<section class="home-section">
<div class="home-eyebrow">Integrations</div>
<h2 class="home-title">Four web servers, one source of truth</h2>
<p class="home-lede">
The same OWASP CRS rule set is converted into the native syntax of each platform &mdash;
so you get equivalent protection whether you run Nginx, Apache, Traefik, or HAProxy.
</p>
<section class="home-section">
<header class="home-rail-head">
<span class="home-eyebrow">Integrations</span>
<h2 class="home-title">Four web servers, one source of truth</h2>
<p class="home-lede">
The same OWASP CRS rule set is converted into the native syntax of each platform &mdash;
so you get equivalent protection regardless of the proxy in front of your stack.
</p>
</header>
<div class="platform-grid">
<a
v-for="p in platforms"
:key="p.name"
:href="withBase(p.href)"
class="platform-card"
>
<span class="platform-icon">
<component :is="p.icon" />
</span>
<div class="platform-name">{{ p.name }}</div>
<div class="platform-meta">{{ p.meta }}</div>
<span class="platform-arrow"><IconArrow /></span>
</a>
</div>
</section>
<div class="platform-grid">
<a
v-for="p in platforms"
:key="p.name"
:href="withBase(p.href)"
class="platform-card"
:style="{ '--accent': p.accent }"
>
<span class="platform-icon">
<component :is="p.icon" />
</span>
<div class="platform-name">{{ p.name }}</div>
<div class="platform-meta">{{ p.meta }}</div>
<span class="platform-arrow"><IconArrow /></span>
</a>
</div>
</section>
<section class="home-section">
<div class="stats-strip">
<div class="stat-item">
<div class="stat-value">600+</div>
<div class="stat-label">OWASP CRS patterns extracted</div>
</div>
<div class="stat-item">
<div class="stat-value">Daily</div>
<div class="stat-label">Automated rule refresh</div>
</div>
<div class="stat-item">
<div class="stat-value">4</div>
<div class="stat-label">Web server backends</div>
</div>
<div class="stat-item">
<div class="stat-value">MIT</div>
<div class="stat-label">Open-source license</div>
</div>
</div>
</section>
<section class="home-section home-section--stats">
<div class="stats-rail">
<div v-for="s in stats" :key="s.label" class="stat-cell">
<div class="stat-value">{{ s.value }}</div>
<div class="stat-key">{{ s.label }}</div>
<div class="stat-sub">{{ s.sub }}</div>
</div>
</div>
</section>
</template>

View File

@@ -1,8 +1,12 @@
<template>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M5.5 20c1-4 3-9 6.5-13.5 1.5 1.5 2.5 3.5 3 5.5" />
<path d="M9 14c2-3 5-5 9-5" />
<path d="M11 17c2-2 5-3 8-3" />
<path d="M5.5 20h2" />
</svg>
<!-- Apache-inspired feather: curved spine with tapered barbs -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M20 4c-9 1-15 8-16 17" />
<path d="M17.5 4.7c-3 .2-5.6 1.7-7.6 4.1" />
<path d="M14.5 6c-2.2.5-4.2 1.9-5.6 3.9" />
<path d="M11.6 8.2c-1.6.7-3.1 2-4.1 3.6" />
<path d="M9 11.2c-1.4.8-2.6 2-3.4 3.4" />
<path d="M6.5 14.6c-1 .9-1.8 2-2.4 3.3" />
<path d="M4 21h2.5" />
</svg>
</template>

View File

@@ -1,9 +1,11 @@
<template>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="5" cy="12" r="2" />
<circle cx="19" cy="6" r="2" />
<circle cx="19" cy="18" r="2" />
<path d="M7 11 17 7" />
<path d="M7 13l10 4" />
</svg>
<!-- HAProxy-inspired stylized H with load-balanced node accents -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M5 4v16" />
<path d="M19 4v16" />
<path d="M5 12h14" />
<circle cx="5" cy="12" r="1.4" fill="currentColor" stroke="none" />
<circle cx="19" cy="12" r="1.4" fill="currentColor" stroke="none" />
<path d="M5 6h2M17 6h2M5 18h2M17 18h2" stroke-opacity="0.55" />
</svg>
</template>

View File

@@ -1,6 +1,7 @@
<template>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 2.5 3.5 7v10L12 21.5 20.5 17V7L12 2.5Z" />
<path d="M9 16V8l6 8V8" />
</svg>
<!-- Nginx-inspired hexagon mark with angular N -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 2.5 4 7v10l8 4.5L20 17V7L12 2.5Z" />
<path d="M9.5 16.5V8.2l5 7.7V8.2" />
</svg>
</template>

View File

@@ -1,5 +1,10 @@
<template>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M3 12h4l2-3 3 6 3-9 2 6h4" />
</svg>
<!-- Traefik-inspired "Mr. Traefik" mark: rounded body with horns and eyes -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M5 11.5v6.5a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6.5" />
<path d="m5 11.5 2-7 2 4 1.5-3 1.5 3 1.5-3 1.5 3 2-4 2 7" />
<circle cx="9.5" cy="14.6" r="0.85" fill="currentColor" stroke="none" />
<circle cx="14.5" cy="14.6" r="0.85" fill="currentColor" stroke="none" />
<path d="M10.5 17.6h3" />
</svg>
</template>

View File

@@ -1,6 +1,8 @@
import { h } from 'vue'
import { h, Fragment } from 'vue'
import type { Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import HeroVisual from './components/HeroVisual.vue'
import HomeFeatureRail from './components/HomeFeatureRail.vue'
import HomeShowcase from './components/HomeShowcase.vue'
import './style.css'
@@ -8,7 +10,9 @@ export default {
extends: DefaultTheme,
Layout: () => {
return h(DefaultTheme.Layout, null, {
'home-features-after': () => h(HomeShowcase)
'home-hero-image': () => h(HeroVisual),
'home-features-after': () =>
h(Fragment, null, [h(HomeFeatureRail), h(HomeShowcase)])
})
}
} satisfies Theme

View File

File diff suppressed because it is too large Load Diff

View File

@@ -5,9 +5,6 @@ hero:
name: Patterns
text: Production-grade WAF rules, on autopilot.
tagline: Automated OWASP Core Rule Set and bad-bot patterns, converted into native configurations for Nginx, Apache, Traefik, and HAProxy &mdash; refreshed every day.
image:
src: /hero-shield.svg
alt: Patterns
actions:
- theme: brand
text: Get Started
@@ -15,53 +12,17 @@ hero:
- theme: alt
text: View on GitHub
link: https://github.com/fabriziosalmi/patterns
features:
- icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3 4.5 5.8v6.1c0 4.7 3.3 9.1 7.5 10.4 4.2-1.3 7.5-5.7 7.5-10.4V5.8L12 3Z"/><path d="m8.5 12 2.5 2.5L15.5 9.5"/></svg>'
title: OWASP CRS Protection
details: Defends against SQL injection, XSS, RCE, LFI, and RFI by deriving rules from the OWASP Core Rule Set &mdash; the same engine that powers ModSecurity worldwide.
- icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="7.5" width="16" height="11" rx="3"/><path d="M12 4v3.5"/><circle cx="12" cy="3.5" r="0.9" fill="currentColor" stroke="none"/><circle cx="9" cy="13" r="1.1" fill="currentColor" stroke="none"/><circle cx="15" cy="13" r="1.1" fill="currentColor" stroke="none"/><path d="M2 13.5v2M22 13.5v2"/></svg>'
title: Bad Bot Blocking
details: Curated User-Agent lists from public sources block scrapers, AI crawlers, vulnerability scanners, and SEO spam &mdash; with configurable allow-lists for legitimate bots.
- icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4" width="17" height="6" rx="1.5"/><rect x="3.5" y="14" width="17" height="6" rx="1.5"/><path d="M7 7h.01M7 17h.01"/><path d="M11 7h6M11 17h6"/></svg>'
title: Native Multi-Server Output
details: One source rule set, four idiomatic outputs &mdash; Nginx <code>map</code>+<code>if</code>, Apache <code>SecRule</code>, Traefik middleware TOML, and HAProxy ACL files.
- icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 12a8.5 8.5 0 0 1 14.5-6L20 8"/><path d="M20 3v5h-5"/><path d="M20.5 12a8.5 8.5 0 0 1-14.5 6L4 16"/><path d="M4 21v-5h5"/></svg>'
title: Daily Automated Updates
details: A GitHub Actions workflow re-fetches the latest CRS release, rebuilds every backend, and publishes a fresh release archive &mdash; without manual intervention.
- icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8 12 3 3 8v8l9 5 9-5V8Z"/><path d="m3.3 8 8.7 5 8.7-5"/><path d="M12 13v8"/><path d="m7.5 5.5 9 5"/></svg>'
title: Pre-Built Releases
details: Drop-in archives are published on every run. Skip the toolchain &mdash; download <code>nginx_waf.zip</code>, <code>apache_waf.zip</code>, <code>traefik_waf.zip</code>, or <code>haproxy_waf.zip</code>.
- icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4.5a2 2 0 1 0-4 0V6H6a1.5 1.5 0 0 0-1.5 1.5V11h1.5a2 2 0 1 1 0 4H4.5v3.5A1.5 1.5 0 0 0 6 20h3.5v-1.5a2 2 0 1 1 4 0V20H17a1.5 1.5 0 0 0 1.5-1.5V15H20a2 2 0 1 0 0-4h-1.5V7.5A1.5 1.5 0 0 0 17 6h-3V4.5Z"/></svg>'
title: Composable & Extensible
details: Each backend is a small Python converter that consumes a single JSON intermediate. Adding a new platform is a few hundred lines &mdash; not a fork.
---
<div class="home-section">
## Quick start
Pull the latest release archive and include it in your existing server configuration &mdash; no toolchain required.
```bash
# Pick the archive that matches your stack
curl -LO https://github.com/fabriziosalmi/patterns/releases/latest/download/nginx_waf.zip
unzip nginx_waf.zip -d /etc/nginx/waf_patterns
```
Or build from source to customize before deploying:
```bash
git clone https://github.com/fabriziosalmi/patterns.git
cd patterns
pip install -r requirements.txt
python owasp2json.py # Fetch the latest OWASP CRS
python json2nginx.py # Or json2apache.py / json2traefik.py / json2haproxy.py
python badbots.py # Generate bad-bot blocklists
```
::: tip Using Caddy?
See the dedicated [caddy-waf](https://github.com/fabriziosalmi/caddy-waf) project for Caddy-specific WAF support.
:::
Or build from source &mdash; full toolchain instructions in [Getting Started](/getting-started).
</div>

View File

@@ -1,21 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 320" fill="none">
<defs>
<linearGradient id="shieldGrad" x1="40" y1="20" x2="280" y2="300" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#5ac8fa"/>
<stop offset="0.5" stop-color="#0a84ff"/>
<stop offset="1" stop-color="#0040dd"/>
</linearGradient>
<linearGradient id="shieldGlow" x1="160" y1="40" x2="160" y2="280" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#fff" stop-opacity="0.18"/>
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="12" stdDeviation="18" flood-color="#0a84ff" flood-opacity="0.25"/>
</filter>
</defs>
<g filter="url(#shadow)">
<path d="M160 24 56 60v82c0 64.5 45 124 104 142 59-18 104-77.5 104-142V60L160 24Z" fill="url(#shieldGrad)"/>
<path d="M160 24 56 60v82c0 64.5 45 124 104 142 59-18 104-77.5 104-142V60L160 24Z" fill="url(#shieldGlow)"/>
</g>
<path d="M108 162 145 199 218 124" stroke="#fff" stroke-width="18" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB