From 1ba9ce735ab7bd7498549b3eeb570fc66f432339 Mon Sep 17 00:00:00 2001 From: trivedi-vatsal Date: Sun, 30 Aug 2026 10:08:58 +0530 Subject: [PATCH 1/3] Add .ci.yml so website Check Runs on ci.openpreflight.xyz. Hosting stays Cloudflare Pages; pipeline uses pnpm to match the lockfile. Co-authored-by: Cursor --- .ci.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .ci.yml diff --git a/.ci.yml b/.ci.yml new file mode 100644 index 0000000..d116068 --- /dev/null +++ b/.ci.yml @@ -0,0 +1,5 @@ +runtime: node:22 +install: corepack enable && corepack prepare pnpm@10 --activate && pnpm install --frozen-lockfile +test: pnpm run check-links +build: pnpm run build +timeout: 15m From f38d84db25002133591d1d0201b5ed428038b177 Mon Sep 17 00:00:00 2001 From: trivedi-vatsal Date: Sun, 30 Aug 2026 10:11:07 +0530 Subject: [PATCH 2/3] Install pnpm via npx in .ci.yml. corepack enable cannot write /usr/local/bin in the node:22 job container. Build before link-check so dist exists. Co-authored-by: Cursor --- .ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.ci.yml b/.ci.yml index d116068..d39c7e6 100644 --- a/.ci.yml +++ b/.ci.yml @@ -1,5 +1,4 @@ runtime: node:22 -install: corepack enable && corepack prepare pnpm@10 --activate && pnpm install --frozen-lockfile -test: pnpm run check-links -build: pnpm run build +install: npx --yes pnpm@10 install --frozen-lockfile +test: npx --yes pnpm@10 run build && npx --yes pnpm@10 run check-links timeout: 15m From 4d669accbf98e00035ec97c367a08157c3e89d8e Mon Sep 17 00:00:00 2001 From: trivedi-vatsal Date: Tue, 1 Sep 2026 10:47:27 +0530 Subject: [PATCH 3/3] Refine text for clarity and consistency across multiple pages - Improved sentence structure and punctuation in the integrations, open-source, pipeline, product, security, self-hosted, use cases, and why pages. - Enhanced descriptions and details for better readability and understanding. - Updated specific phrases to maintain a consistent tone and style throughout the documentation. --- CONTRIBUTING.md | 8 +- README.md | 12 +- astro.config.mjs | 2 +- scripts/gen-banner.mjs | 4 +- src/components/blocks/site-header-01.tsx | 286 ++++++++++++++++--- src/components/marketing/CheckRunPanel.astro | 6 +- src/components/marketing/CodePanel.astro | 2 +- src/components/marketing/Faq.astro | 2 +- src/components/templates/saas-landing-01.tsx | 26 +- src/lib/site.ts | 11 +- src/pages/compare/github-actions.astro | 18 +- src/pages/concepts.astro | 16 +- src/pages/integrations/github-app.astro | 16 +- src/pages/integrations/index.astro | 24 +- src/pages/open-source.astro | 20 +- src/pages/pipeline.astro | 20 +- src/pages/product/index.astro | 22 +- src/pages/security.astro | 20 +- src/pages/self-hosted.astro | 28 +- src/pages/use-cases/index.astro | 18 +- src/pages/use-cases/open-source.astro | 2 +- src/pages/use-cases/private-repos.astro | 12 +- src/pages/use-cases/self-hosted-teams.astro | 12 +- src/pages/why.astro | 15 +- 24 files changed, 412 insertions(+), 190 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bef1804..c8dc100 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,7 @@ # Contributing -This repo is the marketing site at [openpreflight.xyz](https://openpreflight.xyz) -— one Astro page, a Tailwind theme, and a small set of generated brand assets. +This repo is the marketing site at [openpreflight.xyz](https://openpreflight.xyz): +one Astro page, a Tailwind theme, and a small set of generated brand assets. Product docs live in [openpreflight/docs](https://github.com/openpreflight/docs); the tool itself lives in [openpreflight/openpreflight](https://github.com/openpreflight/openpreflight). @@ -17,7 +17,7 @@ pnpm run check-links # required paths + internal hrefs + external allow-list `check-links` runs against `dist/`, so build first. It fails on a missing required asset or an external link outside the allow-list in -`scripts/check-links.mjs` — if you add an outbound domain, add it there too. +`scripts/check-links.mjs`. If you add an outbound domain, add it there too. ## Brand assets @@ -40,7 +40,7 @@ of all three repos, so do not rename or remove those files. ## What we will not take -- New pages or sections without a reason to exist — the site is one page on purpose. +- New pages or sections without a reason to exist. The site is one page on purpose. - Product claims that are not backed by the docs. ## Pull requests diff --git a/README.md b/README.md index 8a96760..198b87b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ /> openpreflight — A small CI provider for private repos. One Go binary, one SQLite file: register a GitHub App, enable your repos, and get one Check Run per commit. @@ -40,10 +40,10 @@ npm run dev - `src/pages/404.astro`: branded not-found - `src/layouts/MarketingPage.astro`: header/footer chrome for inner pages; the pages themselves compose full-bleed sections -- `src/lib/site.ts`: `siteSections` is the canonical site map — the header nav, +- `src/lib/site.ts`: `siteSections` is the canonical site map. The header nav, the footer, and the 404 all render it, so a route cannot exist without being reachable from every page -- `src/components/marketing/`: the subpage system — `PageHero`, `Section`, +- `src/components/marketing/`: the subpage system: `PageHero`, `Section`, `Card`/`Grid`, `Steps`, `MarkList`, `CodePanel`, `CheckRunPanel`, `Callout`, `Faq`, `Prose`, `CtaLink`/`Actions` - `src/components/blocks/`, `src/components/ui/`: Rivelle blocks and primitives @@ -64,7 +64,7 @@ node scripts/gen-icons.mjs # favicons -> website/public/ and ../docs/public/ node scripts/gen-banner.mjs # banner-light.png + banner-dark.png ``` -`gen-banner.mjs` renders the banner through headless Chrome — the fonts are the +`gen-banner.mjs` renders the banner through headless Chrome. The fonts are the same `@fontsource` woff2 files the site loads, which `sharp` cannot rasterise on its own. Set `CHROME_PATH` if Chrome is not at the macOS default location. @@ -72,7 +72,7 @@ The banner is served from `https://openpreflight.xyz/banner-{light,dark}.png` and embedded by the READMEs in [openpreflight](https://github.com/openpreflight/openpreflight), [docs](https://github.com/openpreflight/docs), and this repository. A change to -the tagline or the mark means regenerating it here and redeploying — the other +the tagline or the mark means regenerating it here and redeploying. The other repos need no edit. ## Editing the copy @@ -85,7 +85,7 @@ it into marketing voice. ## Deployment Cloudflare Pages. Root directory is the repository root, build command -`pnpm run build`, output `dist`. This repository uses pnpm — there is a +`pnpm run build`, output `dist`. This repository uses pnpm. There is a `pnpm-lock.yaml` and CI runs `pnpm install --frozen-lockfile`. Sitemap is generated by `@astrojs/sitemap`. diff --git a/astro.config.mjs b/astro.config.mjs index 7878884..20ee16d 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -5,7 +5,7 @@ import sitemap from '@astrojs/sitemap'; import tailwindcss from '@tailwindcss/vite'; // Static marketing site for openpreflight. The reference documentation is a -// separate site (openpreflight/docs) at docs.openpreflight.xyz — keep the copy +// separate site (openpreflight/docs) at docs.openpreflight.xyz. Keep the copy // here to positioning, and link out for anything a reader would need to follow. export default defineConfig({ site: 'https://openpreflight.xyz', diff --git a/scripts/gen-banner.mjs b/scripts/gen-banner.mjs index fa90a9b..ddb9862 100644 --- a/scripts/gen-banner.mjs +++ b/scripts/gen-banner.mjs @@ -5,7 +5,7 @@ import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import sharp from 'sharp'; -// README banner — brand atoms from design.md: forest accent, olive ground, +// README banner. Brand atoms from design.md: forest accent, olive ground, // Inter display, JetBrains Mono for the Check Run panel (the hero artifact). // Rendered through headless Chrome so the webfonts match the sites exactly; // sharp/librsvg cannot load the woff2 files @fontsource ships. @@ -55,7 +55,7 @@ const THEMES = { }; // The mark: rounded square, runway-check, caret at the apex. Always on forest -// green with a white stroke — the mark does not invert between themes. +// green with a white stroke. The mark does not invert between themes. const MARK = `
{ + if (!event.currentTarget.contains(event.relatedTarget as Node)) { + onClose(); + } + }} + onMouseEnter={() => onOpen(group.title)} + onMouseLeave={onClose} + > + +
+
+
+
+

+ {group.title} +

+ {group.description ? ( +

+ {group.description} +

+ ) : null} +
+
+ {group.links.map((link) => { + const Icon = MEGA_ICONS[link.href]; + return ( + + {Icon ? ( + + + + ) : null} + + + {link.label} + + + {link.description ? ( + + {link.description} + + ) : null} + + + ); + })} +
+
+
+
+
+ ); + })} + + ); +} + function SiteHeader01({ brand = "openpreflight", groups = [], @@ -73,6 +258,48 @@ function SiteHeader01({ className, ...props }: SiteHeader01Props) { + const headerRef = React.useRef(null); + const closeTimer = React.useRef>(undefined); + const [openTitle, setOpenTitle] = React.useState(null); + + const iconLinks = links.filter((link) => ICON_LINKS.has(link.label)); + const textLinks = links.filter((link) => !ICON_LINKS.has(link.label)); + const docsHref = iconLinks.find((link) => link.label === "Docs")?.href; + const githubHref = iconLinks.find((link) => link.label === "GitHub")?.href; + + const openGroup = React.useCallback((title: string) => { + clearTimeout(closeTimer.current); + setOpenTitle(title); + }, []); + + const scheduleClose = React.useCallback(() => { + clearTimeout(closeTimer.current); + closeTimer.current = setTimeout(() => setOpenTitle(null), 120); + }, []); + + const closeNow = React.useCallback(() => { + clearTimeout(closeTimer.current); + setOpenTitle(null); + }, []); + + React.useEffect(() => { + if (!openTitle) return; + const onKey = (event: KeyboardEvent) => { + if (event.key === "Escape") closeNow(); + }; + const onPointer = (event: MouseEvent) => { + if (!headerRef.current?.contains(event.target as Node)) closeNow(); + }; + document.addEventListener("keydown", onKey); + document.addEventListener("mousedown", onPointer); + return () => { + document.removeEventListener("keydown", onKey); + document.removeEventListener("mousedown", onPointer); + }; + }, [openTitle, closeNow]); + + React.useEffect(() => () => clearTimeout(closeTimer.current), []); + const initials = (profileName ?? "") .split(" ") .map((part) => part[0]) @@ -87,6 +314,7 @@ function SiteHeader01({ className, )} data-slot="site-header" + ref={headerRef} {...props} >
@@ -98,34 +326,13 @@ function SiteHeader01({ {brand} -
+
+ {docsHref ? ( + + ) : null} + {githubHref ? ( + + ) : null}