A modern website for Plumber (open-source CI/CD compliance CLI), built with Astro, Tailwind CSS, and React.
This is a comprehensive documentation and website for Plumber - a tool for analyzing GitLab CI/CD pipelines for security and compliance issues. Plumber helps you:
- Map out complex CI/CD landscapes
- Detect CI/CD security issues
- Ensure supply chain compliance
- Spot CI/CD technical debt
- Avoid supply chain attacks
The website features:
- π Blog - Release notes, tutorials, and articles about CI/CD security
- π Documentation - Comprehensive guides for installation, usage, and reference
- π¨ Marketing Pages - Product landing pages and feature showcases
- π Search - Full-text search powered by Pagefind
- β¨ CMS - Keystatic CMS for content management
- Node.js 20+
- npm
-
Install dependencies:
npm install
-
Run initial build:
npm run build
-
Copy Pagefind for search:
- Windows:
npm run winsearch - macOS/Linux:
npm run osxsearch
- Windows:
-
Configure i18n:
npm run config-i18n
Follow the prompts to set up your language configuration.
-
Start development server:
npm run dev
Visit
http://localhost:4321
| Command | Action |
|---|---|
npm run dev |
Start local dev server at localhost:4321 |
npm run build |
Build production site to ./dist/ with search indexing |
npm run preview |
Preview production build (not supported with Vercel adapter) |
npm run format |
Format code with ESLint and Prettier |
npm run lint |
Run ESLint |
npm run config-i18n |
Configure internationalization |
npm run remove-keystatic |
Remove Keystatic CMS if not needed |
npm run seo-audit |
Run SEO audit on the site |
npm run generate-favicon-ico |
Generate favicon.ico from favicon.svg |
- Astro 5 - Static site generator with content collections
- Tailwind CSS 4 - Utility-first CSS framework
- React 19 - UI components (via Astro integration)
- TypeScript - Type safety
- MDX - Markdown with JSX support
- Keystatic CMS - Git-based CMS for content editing (accessible at
/keystaticor/admin) - Pagefind - Fast static search
- Astro SEO - SEO optimization
- Astro Icon - Icon system using Tabler icons
- Motion on Scroll - Scroll-triggered animations
- Deployment: Vercel (via
@astrojs/verceladapter) - Image Optimization: Sharp image service
- Compression: HTML/JS compression via Playform Compress
- RSS Feed: Automatic RSS generation for blog posts
.
βββ .github/
β βββ workflows/ # CI/CD (lint, preview, production, security, seo-audit)
βββ public/ # Static assets
β βββ favicons/
β βββ robots.txt
β βββ social-media-card.png
βββ scripts/ # Utility scripts
β βββ config-i18n.js # i18n configuration wizard
β βββ generate-favicon-ico.js
β βββ i18n/ # i18n management utilities
β βββ remove-keystatic.js
β βββ seo-audit.js
βββ src/
β βββ assets/ # Images, videos (optimized by Astro)
β βββ components/ # Reusable Astro/React components
β β βββ Hero/ # Hero sections
β β βββ Feature/ # Feature sections
β β βββ Cta/ # Call-to-action components
β β βββ Nav/ # Navigation
β β βββ Footer/
β β βββ ...
β βββ config/ # Site configuration
β β βββ en/ # Locale-specific (siteData, navData, faqData, etc.)
β β βββ siteSettings.json.ts
β β βββ translationData.json.ts
β βββ data/ # Content collections data
β β βββ blog/ # Blog posts (by language)
β β β βββ en/
β β βββ authors/ # Author profiles
β β βββ otherPages/ # Additional pages (privacy, terms)
β β βββ codeToggles/ # Code example toggles
β βββ docs/ # Documentation system
β β βββ components/ # Docs-specific components
β β β βββ mdx-components/ # MDX components (Aside, Badge, Tabs, etc.)
β β βββ config/ # Docs configuration
β β βββ data/
β β β βββ docs/ # Documentation content (by language)
β β β βββ en/
β β βββ layouts/ # Docs layouts
β β βββ styles/ # Docs-specific styles
β βββ icons/ # SVG icons
β β βββ logos/
β β βββ tabler/ # Tabler icons
β βββ js/ # Utility functions
β β βββ localeUtils.ts
β β βββ translationUtils.ts
β βββ layouts/ # Page layouts
β β βββ BaseLayout.astro
β β βββ BaseHead.astro
β β βββ Blog*.astro # Blog layouts
β βββ pages/ # Route-based pages
β β βββ index.astro # Homepage
β β βββ platform.astro # Platform product page
β β βββ blog/
β β β βββ index.astro # Blog list
β β β βββ [...slug].astro # Blog post
β β βββ docs/
β β β βββ index.astro # Docs home
β β β βββ [docsRoute]/index.astro
β β β βββ [...slug].astro # Docs pages
β β βββ categories/ # Category pages
β β βββ contact.astro
β β βββ [...page].astro # Other pages (privacy-policy, terms-of-use)
β β βββ 404.astro
β β βββ rss.xml.ts # RSS feed
β βββ styles/ # Global styles
β β βββ global.css
β β βββ tailwind-theme.css
β β βββ buttons.css
β β βββ markdown-content.css
β β βββ fonts.css
β β βββ mos.css
β β βββ keystatic.css
β βββ content.config.ts # Content collections schema
βββ astro.config.mjs # Astro configuration
βββ keystatic.config.tsx # Keystatic CMS configuration
βββ package.json
βββ tsconfig.json
βββ vercel.json # Vercel deployment config
Blog articles use MDX format and are stored in src/data/blog/[locale]/.
Create a folder with your article slug:
src/data/blog/en/my-article-name/
βββ index.mdx # Article content
βββ heroImage.png # Hero image (required)
βββ other-images.png # Additional images (optional)
Every blog article must have frontmatter at the top:
---
title: Your Article Title
description: A compelling description for SEO and previews
draft: false
authors:
- main-author
- second-author
pubDate: 2024-01-15
updatedDate: 2024-01-20 # Optional
heroImage: ./heroImage.png
categories:
- ci-cd
- security
mappingKey: my-article # Optional: for i18n matching
---
Your article content starts here...
## Headings
Write in Markdown/MDX format.
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | β | Article title |
description |
string | β | SEO description & preview text |
draft |
boolean | β | Set to true to exclude from build |
authors |
array | β | Array of author IDs (references src/data/authors/) |
pubDate |
date | β | Publication date (YYYY-MM-DD) |
updatedDate |
date | β | Last update date |
heroImage |
image | β | Path to hero image |
categories |
array | β | Array of category strings |
mappingKey |
string | β | For linking translations |
- Hero images: Place in the article folder, reference as
./heroImage.png - Inline images: Place in the article folder, reference with relative paths
- Optimized: All images are automatically optimized by Astro
Auto-imported components (no import needed):
<Admonition>- Info boxes
For other components, import them:
---
title: My Article
---
import CustomComponent from "@components/CustomComponent.astro";
<CustomComponent prop="value" />To announce a new product release (e.g. 1.0.0 Plumber), add a post under the releases/ directory. These posts appear on the main blog page and in the RSS feed.
Where to add it: src/data/blog/[locale]/releases/[version]/
Example for release "1.0.0 Plumber":
src/data/blog/en/releases/1.0.0/
βββ index.mdx # Release notes content
βββ heroImage.png # Hero image (required)
βββ screenshot.png # Optional screenshots
Use the same frontmatter as any blog article. Include a Releases category so itβs easy to filter:
---
title: 1.0.0 Plumber Release
description: Short description for SEO and previews
draft: false
authors:
- plumber
pubDate: 2025-02-04
heroImage: ./heroImage.png
categories:
- "Releases"
---Note: Posts in archive/ are archived: they only appear on /blog/archive, not on the main blog list. Use releases/ for new release announcements and keep archive/ for old/historical release notes (e.g. legacy R2Devops releases).
See existing blog posts in src/data/blog/en/ for reference:
archive/2.17/index.mdx- Archived release notes (R2Devops)releases/- Add new Plumber release posts heretj-actions-compromised/index.mdx- Article exampletop-5-cybersecurity-incidents-in-cicd/index.mdx- Long-form article
Authors are defined in src/data/authors/[author-id]/.
Structure:
src/data/authors/john-doe/
βββ index.mdx
βββ avatar.jpg
Content (index.mdx):
---
name: John Doe
avatar: ./avatar.jpg
about: DevOps engineer passionate about CI/CD security
email: john@example.com
authorLink: https://johndoe.com
---Documentation articles use MDX and are stored in src/docs/data/docs/[locale]/.
Option A: Single file
src/docs/data/docs/en/getting-started.mdx
Option B: Folder with index (recommended for images)
src/docs/data/docs/en/installation/
βββ index.mdx
βββ img/
βββ screenshot.png
Option C: Nested sections
src/docs/data/docs/en/installation/
βββ index.mdx
βββ docker-compose.mdx
βββ kubernetes.mdx
βββ img/
βββ diagrams.svg
---
title: Getting Started with Plumber
description: Learn how to install and use Plumber CLI
sidebar:
label: Quick Start # Label in sidebar (defaults to title)
order: 1 # Order in sidebar
badge:
text: New
variant: tip # note, tip, caution, danger, info
tableOfContents:
minHeadingLevel: 2
maxHeadingLevel: 3
pagefind: true # Include in search (default: true)
draft: false # Exclude from build if true
mappingKey: getting-started # For i18n
---
## Your documentation content
Write your docs here in Markdown/MDX.| Field | Type | Required | Description |
|---|---|---|---|
title |
string | β | Page title |
description |
string | β | SEO description |
sidebar.label |
string | β | Sidebar text (defaults to title) |
sidebar.order |
number | β | Sidebar position |
sidebar.badge |
object | β | Badge with text & variant |
tableOfContents |
object | β | TOC configuration |
pagefind |
boolean | β | Include in search (default: true) |
draft |
boolean | β | Exclude from build |
mappingKey |
string | β | For i18n matching |
The following components are auto-imported in all documentation files:
<Aside variant="note">This is a note callout.</Aside>
<Aside variant="tip">This is a helpful tip!</Aside>
<Aside variant="caution">Be careful with this.</Aside>
<Aside variant="danger">This is dangerous!</Aside>Variants: note, tip, caution, danger, info
<Badge variant="tip">New</Badge>
<Badge variant="caution">Deprecated</Badge><Button href="/docs/installation">Get Started</Button>
<Button variant="secondary">Learn More</Button><Steps>
1. First, install the dependencies
```bash
npm install- Then configure your settings
- Finally, run the build
##### Tabs
```mdx
<Tabs defaultValue="npm">
<TabsList>
<TabsTrigger value="npm">npm</TabsTrigger>
<TabsTrigger value="pnpm">pnpm</TabsTrigger>
</TabsList>
<TabsContent value="npm">
```bash
npm install plumber-cli
```
</TabsContent>
<TabsContent value="pnpm">
```bash
pnpm add plumber-cli
```
</TabsContent>
</Tabs>
The sidebar is auto-generated from:
- The folder structure in
src/docs/data/docs/[locale]/ - The
sidebar.ordervalues in frontmatter - The
sidebar.labelvalues (or falls back totitle)
Tips:
- Use
sidebar.orderto control positioning - Folders become collapsible sections
index.mdxfiles become the section's main page
See existing docs for reference:
src/docs/data/docs/en/getting-started/index.mdx- Overviewsrc/docs/data/docs/en/installation/- Multiple pages (docker-compose, kubernetes, podman, etc.)src/docs/data/docs/en/components/- MDX components (Aside, Badge, Button, Steps, Tabs)src/docs/data/docs/en/use-plumber/- Controls, issues, register-templates, roles-permissions
Create a new .astro file in src/pages/:
---
// src/pages/about.astro
import BaseLayout from "@layouts/BaseLayout.astro";
import Hero1 from "@components/Hero/Hero1.astro";
---
<BaseLayout title="About Us" description="Learn about our mission">
<Hero1 />
<section class="container mx-auto px-4 py-16">
<h2>Our Story</h2>
<p>Content here...</p>
</section>
</BaseLayout>This creates a route at /about.
Use [param].astro for dynamic routes:
---
// src/pages/product/[id].astro
export async function getStaticPaths() {
return [{ params: { id: "plumber" } }, { params: { id: "platform" } }];
}
const { id } = Astro.params;
---
<BaseLayout title={`Product: ${id}`}>
<!-- Content -->
</BaseLayout>The project includes many pre-built components:
---
import Hero1 from "@components/Hero/Hero1.astro";
import Feature1 from "@components/Feature/Feature1.astro";
import Cta1 from "@components/Cta/Cta1.astro";
import Pricing1 from "@components/Pricing/Pricing1.astro";
---
<Hero1 />
<Feature1 />
<Pricing1 />
<Cta1 />- Hero - Hero sections for landing pages
- Feature - Feature showcase sections
- Cta - Call-to-action sections
- Pricing - Pricing tables
- Testimonials - Customer testimonials
- LogoCloud - Client/partner logos
- PostCard - Blog post cards
- SiteLogo - Site logo (nav, footer, mobile)
- Nav / Footer - Navigation and footer
---
// src/components/MyComponent/MyComponent.astro
interface Props {
title: string;
description?: string;
}
const { title, description } = Astro.props;
---
<div class="my-component">
<h2>{title}</h2>
{description && <p>{description}</p>}
<slot />
</div>
<style>
.my-component {
/* Component-scoped styles */
}
</style>Usage:
---
import MyComponent from "@components/MyComponent/MyComponent.astro";
---
<MyComponent title="Hello" description="World">
<p>Slotted content</p>
</MyComponent>Tailwind 4 is configured via CSS variables in src/styles/tailwind-theme.css:
@theme {
--color-primary-*: ...;
--color-secondary-*: ...;
}Located in src/styles/:
global.css- Base styles, CSS variablesbuttons.css- Button variantsmarkdown-content.css- Markdown rendering stylesfonts.css- Font imports
Dark mode is implemented via CSS classes. Toggle component: ThemeToggle.astro
Search is powered by Pagefind, built during the production build.
Configuration:
- Automatically indexes all pages
- Control indexing with
data-pagefind-ignoreattribute - Control in docs with
pagefind: falsein frontmatter
Building search index:
npm run build # Runs pagefind and copies index into Vercel output for deployed search- Default language: English (
en) - Locale prefix: Disabled for default locale
-
Run the i18n configuration wizard:
npm run config-i18n
-
Follow the prompts to add a new locale
-
The script will:
- Update
astro.config.mjs - Update
keystatic.config.tsx - Update site settings
- Create content folders for the new locale
- Update
After adding a locale, translate content by:
- Copying content from
src/data/blog/en/tosrc/data/blog/[locale]/ - Copying docs from
src/docs/data/docs/en/tosrc/docs/data/docs/[locale]/ - Updating
src/config/translationData.json.ts
- Local development:
http://localhost:4321/keystatic - Production:
https://yoursite.com/keystatic
Keystatic is configured in keystatic.config.tsx. Current collections:
blogEN- Blog posts (English)authors- Author profilesotherPagesEN- Additional pages
- Local: No authentication required
- Production: Uses Keystatic Cloud (requires account)
If you don't need a CMS:
npm run remove-keystaticThe project is configured for Vercel deployment:
- Connect your repository to Vercel
- Vercel auto-detects Astro
- Build command:
npm run build - Output directory:
dist
Configuration is in vercel.json and uses the @astrojs/vercel adapter.
To deploy on other platforms:
- Update the adapter in
astro.config.mjs - Follow Astro's deployment guides
For Keystatic Cloud in production:
- Set up a project at keystatic.cloud
- Configure OAuth if needed
Astro provides fast HMR for:
- Astro components
- MDX files
- Styles
- React components
TypeScript is fully configured. Run type checking:
npx astro checknpm run lint # Check for issues
npm run format # Auto-fix issuesInstall the CodeTour extension to view guided tours in .tours/.
---
import { getCollection } from "astro:content";
// Get all blog posts
const allPosts = await getCollection("blog");
// Filter published posts
const publishedPosts = await getCollection("blog", ({ data }) => {
return !data.draft;
});
// Get by category
const securityPosts = allPosts.filter((post) => post.data.categories?.includes("security"));
------
import { getCollection } from "astro:content";
const docs = await getCollection("docs");
------
import { getEntry } from "astro:content";
const post = await getEntry("blog", "my-article-name");
const { Content } = await post.render();
---
<article>
<h1>{post.data.title}</h1>
<Content />
</article>Main Astro configuration:
- Site URL, image service
- Integrations (MDX, React, Sitemap, Keystatic, Compress, etc.)
- Markdown settings (Shiki syntax highlighting)
- i18n configuration
- Auto-imports for MDX
Content collections schema:
- Defines validation for frontmatter (blog, authors, docs, otherPages)
- Configures content loaders
- Sets up relationships (e.g. blog posts β authors)
Dependencies and scripts. Key packages:
- Astro 5.x
- Tailwind CSS 4.x
- React 19.x
- Keystatic CMS
- Pagefind (search)
- Node 20+
TypeScript configuration with path aliases:
@components/*βsrc/components/*@layouts/*βsrc/layouts/*@js/*βsrc/js/*- etc.
npm run buildNote: With the Vercel adapter, npm run preview is not supported. To test the built site locally, use a static server (e.g. npx serve dist/client -p 4321) or deploy to a Vercel preview.
Search only works after running a production build:
npm run build
npm run winsearch # or osxsearch
npm run dev- Plumber CLI: Open-source GitLab CI/CD compliance tool
- Plumber Platform: Full-featured compliance and security platform
- Discord Community: Join for support and discussions
- Support Email: hello@getplumber.io
- Astro Documentation
- Astro Discord
- Tailwind CSS Documentation
- MDX Documentation
- Keystatic Documentation
When contributing content:
- Follow the established patterns in existing content
- Use the frontmatter schemas defined in
content.config.ts - Test locally before committing
- Ensure all images are optimized
- Run linting:
npm run format
See license details at the main Plumber project repository.
- Technical Issues: Open an issue in the repository
- Content Questions: Contact via Discord or support email
- Theme/Styling: Refer to Cosmic Themes documentation (original theme base)
Built with β€οΈ by the Plumber team
Securing CI/CD pipelines, one commit at a time.