Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .claude/commands/docs-review/references/blog.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ Vale catches Pulumi product-name capitalization, the Pulumi Policies singular-ve

When a blog post announces a new feature, provider, or significant capability:

- **Check that `/content/docs/` covers it.** Search for the feature name across `content/docs/`, `content/tutorials/`. If the only mention of the feature is the blog post itself, that's a finding.
- **Check that `/content/docs/` covers it.** Search for the feature name across `content/docs/`. If the only mention of the feature is the blog post itself, that's a finding.
- **Note specific gaps.** Don't just say "docs are missing" — name the page that should exist (e.g., "no `content/docs/esc/integrations/<feature>/` page found").
- **Suggest a doc type.** Reference / tutorial / concept guide / how-to — pick the one that matches the feature's nature.

Expand Down
2 changes: 1 addition & 1 deletion .claude/commands/docs-review/references/docs.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
user-invocable: false
description: Review criteria for technical documentation under content/docs, content/tutorials, content/what-is.
description: Review criteria for technical documentation under content/docs and content/what-is.
---

# Review — Docs
Expand Down
2 changes: 1 addition & 1 deletion .claude/commands/docs-review/references/domain-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Each changed file routes to **exactly one** domain by path. Apply the rules in o
|---|---|---|
| 1 | `docs-review:references:programs` | `static/programs/**` (includes every nested file in a program directory: `Pulumi.yaml`, `package.json`, `requirements.txt`, source files) |
| 2 | `docs-review:references:blog` | `content/blog/**`, `content/case-studies/**` |
| 3 | `docs-review:references:docs` | `content/docs/**`, `content/tutorials/**`, `content/what-is/**` |
| 3 | `docs-review:references:docs` | `content/docs/**`, `content/what-is/**` |
| 4 | `docs-review:references:website` | Any other `content/**.md` (pricing, legal, `vs/`, `why-pulumi/`, `about/`, `careers/`, etc.) |
| 5 | `docs-review:references:infra` | `.github/workflows/**`, `scripts/**` except `scripts/programs/**`, `infrastructure/**`, `Makefile` (repo root), `package.json` (repo root only), `webpack.config.js`, `webpack.*.js` |
| 6 | `docs-review:references:shared-criteria` only | Anything else (`layouts/`, `assets/`, `data/`, etc.) |
Expand Down
2 changes: 1 addition & 1 deletion .claude/commands/docs-review/scripts/triage-classify.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def classify_path(path: str) -> str | None:
return "domain:programs"
if path.startswith("content/blog/") or path.startswith("content/case-studies/"):
return "domain:blog"
for prefix in ("content/docs/", "content/tutorials/", "content/what-is/"):
for prefix in ("content/docs/", "content/what-is/"):
if path.startswith(prefix):
return "domain:docs"
if path.startswith(".github/workflows/"):
Expand Down
2 changes: 1 addition & 1 deletion .claude/commands/fix-broken-links/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ Apply the first row that matches.
|---|---|---|
| Internal link points at a moved/renamed path, but the destination page still exists at a new path | **Hugo alias** | Add the old path to `aliases:` on the destination page's frontmatter |
| Internal link to a page that was deleted/restructured, or a non-Hugo path (registry, generated docs) | **S3 redirect** | Add a redirect line to the topic-appropriate file in `scripts/redirects/` |
| Broken link living in **editable** content (`content/docs`, `content/product`, `content/tutorials`) | **Edit at source** | Fix the link in place; use the full root-relative path (`/docs/...`), never `../` |
| Broken link living in **editable** content (`content/docs`, `content/product`) | **Edit at source** | Fix the link in place; use the full root-relative path (`/docs/...`), never `../` |
| Broken link in a **blog** post **and** an equivalent replacement exists (same content, or close enough that the post's meaning is unchanged) | **Edit at source + stamp `lastmod`** | Swap the link; add/update `lastmod:` in that post's frontmatter (leave `date:` alone) |
| Broken link in a **blog** post with **no** equivalent replacement | **Route around the prose** | Add an alias/redirect so it resolves; if it's a dead external link, add it to the exclusion list. **Do not reword blog prose** |
| Dead / transient / bot-protected **external** link | **Exclusion list** | Add the URL to `getDefaultExcludedKeywords()` in `check-links.js` with an inline `//` comment naming the reason + blog post/issue. If the page mattered, prefer swapping to a Wayback snapshot |
Expand Down
3 changes: 1 addition & 2 deletions .claude/commands/move-doc/references/link-updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ From `AGENTS.md` (lines 83-98):
**DO NOT update links in**:

- `content/blog/` - Historical blog posts
- `content/tutorials/` - Historical tutorials

**Why**: Blog posts and tutorials represent a point in time. Aliases handle redirects automatically, preserving historical accuracy.
**Why**: Blog posts represent a point in time. Aliases handle redirects automatically, preserving historical accuracy.

## Search Strategy

Expand Down
3 changes: 0 additions & 3 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
# content/docs/esc/cli/commands/
# content/docs/iac/download-install/versions.md
#
# content/tutorials/ @pulumi/docs
# content/templates/ @pulumi/docs
#
# Require approval from docs team for edits to robots.txt (see issue #9885)
# layouts/robots.txt @pulumi/docs
#
2 changes: 1 addition & 1 deletion .github/labels-pr-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Informational signal labels — surfaced for human filterability. Routing in CI

| Label | Color | Description |
|---|---|---|
| `domain:docs` | `0e8a16` | PR touches technical docs (`content/docs/`, `content/tutorials/`, `content/what-is/`). |
| `domain:docs` | `0e8a16` | PR touches technical docs (`content/docs/`, `content/what-is/`). |
| `domain:blog` | `a2eeef` | PR touches blog posts or customer stories (`content/blog/`, `content/case-studies/`). |
| `domain:infra` | `d4c5f9` | PR touches workflows, scripts, infrastructure code, Makefile, or build/bundling config. |
| `domain:programs` | `fbca04` | PR touches example programs under `static/programs/`. |
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ jobs:
PR: ${{ steps.pr-context.outputs.pr_number }}
run: |
CHANGED=$(gh pr diff "$PR" --name-only \
| grep -E '^content/(docs|blog|what-is|tutorials)/.*\.md$' || true)
| grep -E '^content/(docs|blog|what-is)/.*\.md$' || true)
if [ -z "$CHANGED" ]; then
echo '{}' > .vale-raw.json
echo '[]' > .vale-findings.json
Expand Down Expand Up @@ -653,7 +653,7 @@ jobs:
PR: ${{ steps.pr-context.outputs.pr_number }}
run: |
CHANGED=$(gh pr diff "$PR" --name-only \
| grep -E '^content/(docs|blog|what-is|tutorials)/.*\.md$' || true)
| grep -E '^content/(docs|blog|what-is)/.*\.md$' || true)
if [ -z "$CHANGED" ]; then
echo '[]' > .fetched-urls.json
echo "extract-urls: no in-scope prose files changed; skipping"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/claude-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ jobs:
VALE_BLOCKERS=""
if [[ "$PROSE_CHECK_NEEDED" == "true" ]]; then
VALE_FILES=$(gh pr diff "$PR" --repo "$REPO" --name-only \
| grep -E '^content/(docs|blog|what-is|tutorials)/.*\.md$' || true)
| grep -E '^content/(docs|blog|what-is)/.*\.md$' || true)
if [[ -n "$VALE_FILES" ]]; then
vale --no-exit --output=JSON $VALE_FILES > .vale-raw.json 2>/dev/null \
|| echo '{}' > .vale-raw.json
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/claude-update.yml
Original file line number Diff line number Diff line change
Expand Up @@ -340,8 +340,8 @@ jobs:
printf '%s' "$BODY" > .claude-mention-body.txt

# Run Vale on PR-changed prose files (content/docs, content/blog,
# content/what-is, content/tutorials) so the
# refreshed review reflects style nits in the current commit.
# content/what-is) so the refreshed review reflects style nits in
# the current commit.
# Skipped on issue mentions and when no in-scope prose files were
# touched. The `||` fallbacks ensure both files exist even when
# vale is missing or the filter crashes (mirrors claude-triage.yml).
Expand All @@ -356,7 +356,7 @@ jobs:
PR: ${{ steps.pr-context.outputs.pr_number }}
run: |
CHANGED=$(gh pr diff "$PR" --name-only \
| grep -E '^content/(docs|blog|what-is|tutorials)/.*\.md$' || true)
| grep -E '^content/(docs|blog|what-is)/.*\.md$' || true)
if [ -z "$CHANGED" ]; then
echo '{}' > .vale-raw.json
echo '[]' > .vale-findings.json
Expand Down
23 changes: 18 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ For all content files, follow `STYLE-GUIDE.md`. If a rule is not covered there,

Meta files like this one, `BUILD-AND-DEPLOY.md`, and agent instruction/skill files (e.g., `.claude/commands/*.md`) are exempt from formatting rules (heading case, trailing newlines, etc.).

For all content files (docs, blogs, tutorials, etc.):
For all content files (docs, blogs, changelog entries, etc.):

- **Markdown**: Must always end with a newline.
- **Headings**:
Expand All @@ -62,7 +62,7 @@ For all content files (docs, blogs, tutorials, etc.):
- **Ordered Lists**: Every item begins with `1.` to minimize diff noise.
- **Diagrams**: Prefer Mermaid diagrams over ASCII art. The site renders Mermaid natively via a Hugo code block hook (`layouts/_default/_markup/render-codeblock-mermaid.html`). Use ` ```mermaid ` fenced code blocks. See [Mermaid docs](https://mermaid.js.org/) for syntax.
- **Images on template-driven pages**: Place new images for template-driven pages (homepage, product pages, event pages, case studies — anything rendered through `layouts/partials/template-partials/*`) under `assets/fingerprinted/`, mirroring the path you'd use under `static/`. The template partials route every `<img>` through `layouts/partials/fingerprinted-img.html`, which content-hashes filenames, converts rasters to WebP, and generates responsive `srcset`s. Frontmatter paths still look like `/images/foo.svg`; the partial resolves them. Missing assets cause a build panic, so there is no silent fallback. `meta_image` and assets used by non-template layouts can stay in `static/`.
- **Meta images**: `meta_image` is optional for `docs`, `tutorials`, `case-studies`, `what-is`, `migrate`, `partner`, `topics`, `events`, and `blog` pages. Leave it blank and `scripts/generate-meta-images.mjs` produces an on-brand social card at build time (resolved by `layouts/partials/meta-image-url.html`). A page-level `meta_image` always wins, but custom overrides are discouraged — the generated card covers virtually every case and stays on-brand automatically. For blog posts the card is built from the post title + `feature_image` (generate the feature image with `/blog-feature-image`, or label the PR `needs-design` for a designer-made one); a post's off-brand legacy meta image, if any, was renamed to `meta-legacy.png` and shows in a collapsed "Archived feature image" panel.
- **Meta images**: `meta_image` is optional for `docs`, `case-studies`, `what-is`, `migrate`, `partner`, `topics`, `events`, and `blog` pages. Leave it blank and `scripts/generate-meta-images.mjs` produces an on-brand social card at build time (resolved by `layouts/partials/meta-image-url.html`). A page-level `meta_image` always wins, but custom overrides are discouraged — the generated card covers virtually every case and stays on-brand automatically. For blog posts the card is built from the post title + `feature_image` (generate the feature image with `/blog-feature-image`, or label the PR `needs-design` for a designer-made one); a post's off-brand legacy meta image, if any, was renamed to `meta-legacy.png` and shows in a collapsed "Archived feature image" panel.
- **Spelling/Grammar**: Always correct errors. Use American English spelling.

---
Expand All @@ -79,7 +79,7 @@ Use the `/move-doc` skill for Hugo content files — it handles `git mv`, alias

When moving documentation, aliases handle redirects automatically. Update internal links strategically:

- **DO update** links in `/content/docs/`, `/content/product/`, and `/content/tutorials/`.
- **DO update** links in `/content/docs/` and `/content/product/`.
- **`/content/blog/`** is historical — swap a broken link only for an equivalent replacement, and when the change is worth surfacing to readers stamp `updated: YYYY-MM-DD` (not `lastmod`); otherwise route around it with an alias/redirect. See "Dates: `updated` vs `lastmod`" below.
- **Link style**: links within `/docs/` must use the full canonical path (e.g. `/docs/iac/concepts/stacks/`). Never use parent-directory references (`../stacks/`) — they break when files move.

Expand All @@ -93,11 +93,24 @@ The left nav is data-driven from `data/docs_menu_sections.yml`, which is consume

---

## Learn lives in another repo

`/learn` — tutorials, official templates, community examples, and the glossary — is **not** in this repo. It ships from [pulumi/marketing-web](https://github.com/pulumi/marketing-web) (`apps/www`, an Astro build with its own S3 + CloudFront), and `infrastructure/index.ts` proxies `/learn*` to that distribution the same way it proxies `/registry` and `/guides`. Hugo's `content/tutorials/` and `content/templates/` trees were deleted when it launched; `scripts/redirects/learn-redirects.txt` 301s their URLs into `/learn`.

What that means when you work here:

- **Don't add tutorial or template content to this repo.** A new tutorial, a new official-template page, or a glossary term goes to pulumi/marketing-web. (The `glossary` shortcode and `data/glossary.toml` are a *different*, docs-only glossary rendered at `/docs/glossary/` — that one stays.)
- **Link to `/learn/tutorials/<slug>/`, `/learn/official-templates/<group>/[<cloud>/]`, and `/learn/glossary/<term>/`.** Never `/tutorials/` or `/templates/`; those only redirect.
- **`data/footer.yml` and `data/header_nav.yaml` are synced downstream.** marketing-web's `scripts/sync-content.mjs` reads both, so a nav or footer edit here also changes Learn's chrome.
- **Search does not cover Learn.** Docs search indexes this repo and the Registry only; `/learn` has its own search at `/learn/browse`. `scripts/search/update-search-index.js` deliberately doesn't fetch `/learn/search-index.json`, and there is no Learn facet in the docs search UI.

---

## AI and agent positioning

Pulumi supports the full spectrum of AI agents, and content must never present Neo as the only way to use AI with Pulumi or frame Neo as an either-or choice against other coding agents.

- **Docs** (`content/docs/`, `content/what-is/`, `content/tutorials/`): community-centric and balanced. Third-party coding agents (Claude Code, Codex, Cursor, GitHub Copilot, etc.) working with Pulumi — through IaC, [Agent Skills](/docs/ai/skills/), and the [Pulumi MCP server](/docs/ai/mcp-server/) — are first-class. Neo is Pulumi's purpose-built infrastructure agent: the deepest integration and the fastest path to a great infrastructure agent out of the box, but one option on a spectrum, and most teams benefit from using both.
- **Docs** (`content/docs/`, `content/what-is/`): community-centric and balanced. Third-party coding agents (Claude Code, Codex, Cursor, GitHub Copilot, etc.) working with Pulumi — through IaC, [Agent Skills](/docs/ai/skills/), and the [Pulumi MCP server](/docs/ai/mcp-server/) — are first-class. Neo is Pulumi's purpose-built infrastructure agent: the deepest integration and the fastest path to a great infrastructure agent out of the box, but one option on a spectrum, and most teams benefit from using both.
- **Product/marketing pages** (`content/product/`, homepage): may lead with Neo and sell it hard, but should still acknowledge that Pulumi's code-first approach works with the agent a reader already uses. Avoid copy that disparages other agents (e.g. "unlike generic AI tools").
- **When listing agent options** (e.g. in migration guides), follow the pattern in `content/docs/iac/guides/migration/migrating-to-pulumi/from-terraform.md`: list Neo alongside Claude Code, Cursor, and Codex as equally legitimate choices, with at most a light note on Neo's built-in advantage.

Expand All @@ -113,7 +126,7 @@ The reference pages under `content/docs/iac/concepts/resources/options/` show a

Docs pages state which Pulumi Cloud edition a feature needs through a generated violet callout, not hand-written prose. A marker names a **feature**, never an edition — the edition the callout states is derived from that feature's availability in `data/pulumi_pricing.yaml` (see "Pricing data" below), so a feature that moves editions is a one-line edit that updates `/pricing/` and every marked page at once.

- **Whole page**: add `pulumi_cloud_feature: <feature-id>` to the front matter (for example `pulumi_cloud_feature: rbac`). `layouts/docs/{single,list}.{html,md}` renders the callout above the content. We only mark what a reader has to buy, so an unknown id, an *edition* id, `true`, `false`, and a feature that's available on the Individual edition are all hard lint failures (`checkPulumiCloudFeature` in `scripts/lint/lint-markdown.js`). An ungated page carries no key. The key names the feature because the value does; it isn't `cloud_feature` because `content/templates/` already uses `cloud:` for the cloud *provider* a template targets.
- **Whole page**: add `pulumi_cloud_feature: <feature-id>` to the front matter (for example `pulumi_cloud_feature: rbac`). `layouts/docs/{single,list}.{html,md}` renders the callout above the content. We only mark what a reader has to buy, so an unknown id, an *edition* id, `true`, `false`, and a feature that's available on the Individual edition are all hard lint failures (`checkPulumiCloudFeature` in `scripts/lint/lint-markdown.js`). An ungated page carries no key. The key names the feature because the value does, and to leave `cloud_feature` free for the cloud *provider* sense the word carries elsewhere.
- **One section**: put `{{< pulumi-cloud "<feature-id>" />}}` on the line **directly after** the heading it applies to. `scripts/search/page.js` relies on that adjacency to skip the callout when it builds a heading's search snippet. The no-argument form `{{< pulumi-cloud />}}` means "Pulumi Cloud, all editions" and is only for mixed pages where the reader can't otherwise tell a section needs Cloud at all; a block form with inner content renders orientation prose in the same box. `checkPulumiCloudShortcode` validates the argument against the same vocabulary as the front matter key.
- **If the feature isn't in the data file yet, add it there first.** Give it `hidden: true` when it isn't a marketed line item on `/pricing/` (gated deployments, ESC change requests, organization templates): hidden features stay out of the comparison table but remain resolvable by id.
- **Don't say it twice.** When you add a marker, delete the hand-written "only available in the Enterprise and Business Critical editions" sentence or note it replaces. The callout already links to `/pricing/`. Keep only prose that says something the callout doesn't (for example, a per-edition limit like "Enterprise allows up to 25 custom roles").
Expand Down
5 changes: 3 additions & 2 deletions BUILD-AND-DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -835,10 +835,10 @@ Hugo processes 46+ content directories:

- `/content/docs/` → Documentation
- `/content/blog/` → Blog posts
- `/content/templates/` → Templates
- `/content/product/` → Product pages
- `/content/case-studies/` → Customer stories

> **Note:** content/registry.md is a single landing page file, not a content directory. The full registry application is served from the separate pulumi/registry repository via CloudFront origin routing.
> **Note:** content/registry.md is a single landing page file, not a content directory. The full registry application is served from the separate pulumi/registry repository via CloudFront origin routing. `/learn` (tutorials, official templates, community examples, glossary) is served the same way, from pulumi/marketing-web.

Templates are in `/layouts/` with various shortcodes for:

Expand Down Expand Up @@ -1637,6 +1637,7 @@ Delivery: CloudWatch Logs infrastructure v2
| /js/*.js | S3 Main | 1 year | Versioned assets |
| /registry/* | Registry | 30 minutes | Dynamic content, origin-proxied |
| /guides/* | Guides | 30 minutes | Dynamic content, origin-proxied |
| /learn* | Learn (pulumi/marketing-web) | 30 minutes | Origin-proxied; cache key includes Accept for the origin's markdown negotiation |
| /docs/* | S3 Main | 10 min | Content negotiation for Accept: text/markdown |
| /docs/reference/pkg/dotnet/* | S3 Main | 10 min | CloudFront Function lowercases URI (viewer-request); Lambda@Edge handles redirects (origin-request) |
| /ai | S3 Main | 1 week | 301 redirect to /product/neo/ (Lambda@Edge) |
Expand Down
Loading
Loading