Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ static/js/homepage.*.js
static/js/marketing.*.js
static/js/consent-manager.*.js
static/js/marketing-homepage.*.js
static/js/header-nav.*.js
data/js_manifest.json
data/openapi-spec.json

Expand Down
41 changes: 15 additions & 26 deletions BUILD-AND-DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ make clean # Remove build artifacts and dependencies
│ 1. Asset Compilation (Webpack, Tailwind CSS) │
│ 2. Documentation Generation (TypeDoc, Sphinx, CLI) │
│ 3. Hugo Build (Markdown → HTML) │
│ 4. CSS Optimization (PurgeCSS, CSSNano)
│ 4. CSS Minification (cssnano)
│ 5. Search Index Generation (Algolia) │
└──────────────────┬───────────────────────────────────────────┘
Expand Down Expand Up @@ -621,10 +621,9 @@ Location: `theme/webpack.config.js`
{
bundle: './src/ts/main.ts', // Main site JavaScript
marketing: './src/ts/marketing.ts', // Marketing pages
'marketing-homepage': './src/ts/marketingHomepage.ts', // Marketing homepage
homepage: './src/ts/homepage.ts', // Homepage-specific JS
algolia: './src/ts/algolia-entry.ts', // Search (Algolia)
'consent-manager': './src/ts/consent-manager/index.ts', // Cookie consent (vanilla TS)
'header-nav': './src/ts/header-nav.ts', // Site header navigation
}
```

Expand All @@ -636,10 +635,9 @@ Async chunks use a similar pattern: `chunk-[contenthash:8].js`.
```
static/js/bundle.<hash>.js
static/js/marketing.<hash>.js
static/js/marketing-homepage.<hash>.js
static/js/homepage.<hash>.js
static/js/algolia.<hash>.js
static/js/consent-manager.<hash>.js
static/js/header-nav.<hash>.js
static/js/chunk-<hash>.js
assets/css/bundle.css
assets/css/marketing.css
Expand Down Expand Up @@ -676,18 +674,15 @@ Multi-stage CSS optimization pipeline:
- Autoprefixer for browser compatibility
- Custom PostCSS plugins

3. **PurgeCSS** (Production Only)
- Scans: `public/**/*.html`, `public/js/bundle.*.js`
- Removes unused CSS classes
- Safelist patterns:
- `hs-*` (HubSpot)
- `highlight`, `code-*` (syntax highlighting)
- `carousel`, `pagination` (UI components)
- `st-*` (ShareThis)
- `icon-*`, `pulumi-*` (custom icons)
- Skips: `azure-native-v1/**/*` (prevents OOM)

4. **CSSNano Minification**
3. **Unused-class purging** is handled by Tailwind v4 itself via `@source`
directives in `theme/src/scss/main.scss` and `_marketing.scss` — Tailwind only
emits utilities it finds in the scanned files. PurgeCSS used to run as a
second pass but was removed: it is incompatible with Tailwind v4's nested-CSS
variant output (`.foo { &:hover { ... } }`) and silently dropped every
`hover:`, `focus:`, `focus-visible:`, `space-y-*`, and `data-[...]`
arbitrary-variant rule.

4. **CSSNano Minification** (Production Only, via `scripts/minify-css.js`)
- Removes whitespace and comments
- Optimizes declarations
- Merges rules
Expand Down Expand Up @@ -3500,7 +3495,7 @@ All Dependabot PRs automatically receive:
**Packages:**

- **Webpack Ecosystem:** `webpack*`, `*-loader`, `*-webpack-plugin*`
- **CSS Processing:** `postcss*`, `sass*`, `cssnano`, `autoprefixer`, `@fullhuman/postcss-purgecss`, `tailwindcss`
- **CSS Processing:** `postcss*`, `sass*`, `cssnano`, `autoprefixer`, `tailwindcss`
- **TypeScript:** `typescript`
- **Pulumi:** `@pulumi/*`
- **AWS SDK:** `@aws-sdk/*` (Lambda@Edge risk)
Expand Down Expand Up @@ -3776,12 +3771,7 @@ ls -lh public/css/

**Optimization Techniques:**

1. **PurgeCSS Configuration**
- Review safelist patterns
- Remove unused components
- Optimize Tailwind config

2. **Scope Tailwind's content scan**
1. **Scope Tailwind's content scan**

Tailwind v4 tree-shakes by default — it only emits CSS for classes it detects
in scanned files. If the bundle is larger than expected, narrow the scan with
Expand All @@ -3796,7 +3786,7 @@ ls -lh public/css/
Avoid broad globs like `../../**` that pull in `node_modules` or generated
files — these inflate the detected class list and slow builds.

3. **Code Splitting**
2. **Code Splitting**
- Separate critical CSS
- Load non-critical CSS async

Expand Down Expand Up @@ -3861,7 +3851,6 @@ find static -type f \( -name "*.png" -o -name "*.jpg" \) -size +500k
/js/consent-manager.*.js: 1 year
/js/algolia.*.js: 1 year
/js/homepage.*.js: 1 year
/js/marketing-homepage.*.js: 1 year
/js/marketing.*.js: 1 year
/fonts/*: 1 year
/fingerprinted/*: 1 year
Expand Down
24 changes: 13 additions & 11 deletions cypress/e2e/site.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,30 @@ describe("www.pulumi.com", () => {
describe("home page", () => {

beforeEach(() => {
// Force a desktop viewport so the new nav's desktop affordances render
// (the nav-desktop breakpoint is 1200px).
cy.viewport(1280, 800);
cy.visit("/");
});

it("loads and applies CSS", () => {
// Checking the computed background-color value validates that the CSS bundle
// was properly loaded and applied.
cy.get(".header-container")
cy.get("header.sticky")
.invoke("css", "background-color")
.should("equal", "rgb(255, 255, 255)");
});

it("loads and applies JavaScript", () => {
// Checking the carousel validates that the JS bundle was loaded and applied
// (excluding Stencil components, which are bundled separately).
cy.get(".header-container")
.should("not.have.class", "is-pinned");

cy.wait(6000)
cy.scrollTo(0, 250);

cy.get(".header-container")
.should("have.class", "is-pinned");
// Clicking a nav trigger button toggles aria-expanded via the header-nav
// bundle's click handler — proves that bundle was loaded and ran.
// force: true bypasses Cypress's pointer-events actionability check, which
// can flake in CI when the overlay's data-attribute Tailwind variant hasn't
// applied yet. The click handler is still exercised end-to-end.
cy.get("[data-nav-trigger-button]").first()
.should("have.attr", "aria-expanded", "false")
.click({ force: true })
.should("have.attr", "aria-expanded", "true");
});
});

Expand Down
155 changes: 155 additions & 0 deletions data/header_nav.yaml
Comment thread
cnunciato marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
dropdowns:
- label: Product
wide: true
sections:
- heading: Core product
columns: 2
items:
- label: Platform overview
href: /product/
description: Everything platform engineering teams need to build, secure, and scale cloud infrastructure
icon: cloud-arrow-up
track: header-product-platform
- label: Infrastructure as code
href: /product/infrastructure-as-code/
description: "IaC for any cloud, in any language — Node.js, Python, Go, .NET, Java, and YAML"
icon: custom/pulumi-iac
track: header-product-iac
- heading: Key capabilities
columns: 2
items:
- label: Agentic infrastructure
href: /product/neo/
description: Meet Neo, our AI-powered infrastructure engineering agent
icon: custom/pulumi-neo
iconWeight: fill
track: header-product-neo
- label: Secrets & configuration
href: /product/secrets-management/
description: Environments, secrets, and configuration management
icon: custom/pulumi-secrets
track: header-product-secrets
- label: Insights & governance
href: /product/insights-governance/
description: Asset management, compliance remediation, and AI insights over the cloud
icon: custom/pulumi-insights
track: header-product-insights
- label: Internal developer platform
href: /product/internal-developer-platforms/
description: The fastest, most secure way to deliver cloud infrastructure
icon: custom/pulumi-idp
track: header-product-idp

- label: For engineers
wide: true
sections:
- columns: 2
items:
- label: Get started
href: /docs/get-started/
description: Follow a step-by-step guide to quickly learn Pulumi
icon: rocket-launch
track: header-engineers-get-started
- label: Documentation
href: /docs/
description: Complete guides and API references
icon: books
track: header-engineers-docs
- label: Registry
href: /registry/
description: Browse 170+ cloud providers and packages
icon: package
track: header-engineers-registry
- label: Templates
href: /templates/
description: Deploy common architectures on any cloud
icon: grid-four
track: header-engineers-templates
- label: Tutorials
href: /learn/
description: Get hands-on with Pulumi concepts
icon: graduation-cap
track: header-engineers-tutorials
- label: Events and workshops
href: /events#upcoming
description: Live sessions and workshops
icon: calendar-blank
track: header-engineers-events
- label: Community
href: /community/
description: Join 10k+ developers on Slack
icon: users-three
track: header-engineers-community
- label: Engineers love Pulumi
href: /testimonials/
description: Hear from engineers why they love us
icon: heart
track: header-engineers-testimonials
- label: Pulumi guides
href: /guides/
description: Practical infrastructure patterns for cloud providers
icon: compass
track: header-engineers-guides

- label: For enterprises
sections:
- items:
- label: Enterprise solutions
href: /enterprise/
description: Security, compliance, and support for teams
icon: buildings
track: header-enterprise-solutions
- label: Case studies
href: /case-studies/
description: How Snowflake, Mercedes-Benz, and others use Pulumi
icon: article
track: header-enterprise-case-studies
- label: Request a demo
href: /request-a-demo/
description: See how Pulumi can help your team
icon: chalkboard-teacher
track: header-enterprise-demo
- label: Professional services
href: /proserv/
description: Get expert help with your implementation
icon: briefcase
track: header-enterprise-proserv
- label: Contact sales
href: /contact/
description: Talk to our team about your needs
icon: chat-circle
track: header-enterprise-contact

- label: Company
sections:
- items:
- label: About us
href: /about/
description: Our purpose and values
icon: quotes
track: header-company-about
- label: Careers
href: /careers/
description: Come work for Pulumi
icon: briefcase
track: header-company-careers
- label: Newsroom
href: /about/newsroom/
description: Pulumi in the news
icon: newspaper
track: header-company-newsroom
- label: Awards
href: /awards/
description: Recognition from press and analysts
icon: trophy
track: header-company-awards

plainLinks:
- { label: Registry, href: /registry/, track: header-registry }
- { label: Blog, href: /blog/, track: header-blog }
- { label: Pricing, href: /pricing/, track: header-pricing }
- { label: Docs, href: /docs/, track: header-docs }

cta:
signIn: { label: Sign in, href: "https://app.pulumi.com/signin", track: header-signin }
getStarted: { label: Get started, href: "https://app.pulumi.com/signup", track: header-get-started }
Copy link
Copy Markdown
Contributor

@cnunciato cnunciato May 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of like Claude's suggestion of having the Dashboard button link to the app at the root (/), rather than /signup. That extra hop seems unnecessary for someone who's already signed in.

Any reason not to do that @alexleventer? Seems like this ought to be fine, but I don't want to break any metrics we might be measuring with this particular link. (This would only affect clicks to "Dashboard", which is only shown to signed-in users.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Talked to @alexleventer and we can link directly to / for the signed-in user (so no need to pass through /signup from the Dashboard button).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

12 changes: 0 additions & 12 deletions infrastructure/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -907,12 +907,6 @@ const distributionArgs: aws.cloudfront.DistributionArgs = {
cachePolicyId: oneYearCachePolicy.id,
responseHeadersPolicyId: ImmutableCachePolicy.id,
},
{
...baseCacheBehavior,
pathPattern: "/css/marketing-homepage.*.css",
cachePolicyId: oneYearCachePolicy.id,
responseHeadersPolicyId: ImmutableCachePolicy.id,
},
{
...baseCacheBehavior,
pathPattern: "/js/bundle.*.js",
Expand Down Expand Up @@ -949,12 +943,6 @@ const distributionArgs: aws.cloudfront.DistributionArgs = {
cachePolicyId: oneYearCachePolicy.id,
responseHeadersPolicyId: ImmutableCachePolicy.id,
},
{
...baseCacheBehavior,
pathPattern: "/js/marketing-homepage.*.js",
cachePolicyId: oneYearCachePolicy.id,
responseHeadersPolicyId: ImmutableCachePolicy.id,
},
{
...baseCacheBehavior,
pathPattern: "/js/marketing.*.js",
Expand Down
12 changes: 4 additions & 8 deletions layouts/partials/assets.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,18 @@
{{ end }}

<!--
CSS bundle selection. Pages can set css_bundle in frontmatter to use a
page-specific bundle (e.g., "homepage"). Pages without css_bundle get the
default full bundle. Marketing pages also get the marketing stylesheet.
CSS bundle selection. Marketing pages additionally load the marketing
stylesheet on top of the default bundle.

In production, beasties extracts critical CSS from each page's stylesheets
and inlines it automatically. Do NOT manually inline CSS here — the full
stylesheets use @layer and unlayered inline styles will override them.
-->
{{ $cssBundle := .Params.css_bundle | default "bundle" }}

{{ $nonMarketingSections := slice "docs" "registry" "blog" "learn" "tutorials" "providers" "collections" "templates" "ai" "authors" "tags" "legal" }}
{{ $isMarketing := not (in $nonMarketingSections .Section) }}

{{ template "css-link" (dict "source" (printf "css/%s.css" $cssBundle) "name" $cssBundle) }}
{{ template "css-link" (dict "source" "css/bundle.css" "name" "bundle") }}

{{ if $isMarketing }}
{{ $marketingBundle := cond (eq $cssBundle "homepage") "marketing-homepage" "marketing" }}
{{ template "css-link" (dict "source" (printf "css/%s.css" $marketingBundle) "name" $marketingBundle) }}
{{ template "css-link" (dict "source" "css/marketing.css" "name" "marketing") }}
{{ end }}
2 changes: 1 addition & 1 deletion layouts/partials/head.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

<!-- Preload critical fonts (skip monospace on the homepage — not used above the fold). -->
{{ if not .IsHome }}
<link rel="preload" href="/fonts/monaspace-neon-var.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/fonts/monaspace-neon-regular.woff2" as="font" type="font/woff2" crossorigin />
{{ end }}
<link rel="preload" href="/fonts/inter-regular.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/fonts/inter-semibold.woff2" as="font" type="font/woff2" crossorigin />
Expand Down
Loading
Loading