Note: For comprehensive details on the build system, deployment infrastructure, and CI/CD workflows, see
BUILD-AND-DEPLOY.md. This file is large, so read only specific sections as needed to conserve tokens.
Never push directly to
master. Always create a branch and open a PR. Direct pushes tomasterbypass review and CI checks. (Server-side branch protection is planned but not yet in place.)
Agents must use these exact commands:
- Install deps:
make ensure - Build site:
make build - Serve locally on port 1313 (accessible with curl):
- Normal:
make serve - With asset rebuilds:
make serve-all
- Normal:
- Lint:
make lint(must pass before commit/merge) - Lint prose:
make lint-prose(Vale; nags, never blocks. Also surfaces in pinned PR reviews.) - Format:
make format - Run all tests:
make test - Run specific program test:
ONLY_TEST="program-name" ./scripts/programs/test.sh - Fix trailing spaces:
sed -i '' 's/[[:space:]]*$//' file1.md file2.md ...
Do not substitute other tools or commands, or change package.json to use pnpm (Yarn/npm only).
For all content files, follow STYLE-GUIDE.md. If a rule is not covered there, fall back to the Google Developer Documentation Style Guide. Do not invent new style conventions; ask for clarification if something is ambiguous.
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.):
- Markdown: Must always end with a newline.
- Headings:
- H1 = Title Case
- H2+ = Sentence case
- TypeScript/JavaScript: Must follow
tsconfig.jsonsettings. No comments unless explicitly requested. - TypeScript program files (
static/programs/): Use hand-written constructor style — resource name and opening{on the same line,}, {inline when an opts argument follows:Do NOT use Prettier's multi-arg style where name, props, and opts are each on separate indented lines.const r = new SomeResource("name", { prop: value, }, { provider: p, });
- File Placement:
- Docs go under
content/docs/... - Blog posts go under
content/blog/... - Other content goes into appropriate
content/...subdirectory - Code examples go under
/static/programswith a language suffix in the filename. - Mirror the structure of existing content; do not invent new layouts.
- Docs go under
- Includes: Use Hugo shortcodes for shared content, never raw Markdown copy-paste.
- Naming: Use lowercase for non-proper nouns (e.g. “stack,” not “Stack”).
- 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```mermaidfenced code blocks. See Mermaid docs 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/*) underassets/fingerprinted/, mirroring the path you'd use understatic/. The template partials route every<img>throughlayouts/partials/fingerprinted-img.html, which content-hashes filenames, converts rasters to WebP, and generates responsivesrcsets. 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_imageand assets used by non-template layouts can stay instatic/. - Meta images:
meta_imageis optional fordocs,tutorials,case-studies,what-is,migrate,partner,topics,events, andblogpages. Leave it blank andscripts/generate-meta-images.mjsproduces an on-brand social card at build time (resolved bylayouts/partials/meta-image-url.html). A page-levelmeta_imagealways 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 PRneeds-designfor a designer-made one); a post's off-brand legacy meta image, if any, was renamed tometa-legacy.pngand shows in a collapsed "Archived feature image" panel. - Spelling/Grammar: Always correct errors. Use American English spelling.
Use the /move-doc skill for Hugo content files — it handles git mv, alias injection, link updates, and verification. For non-Hugo files (generated content, static assets), add S3 redirects in /scripts/redirects/ (format: source-path|destination-url, place entries in topic-appropriate files). Manual move procedure and anchor-link caveats: see .claude/commands/move-doc/SKILL.md.
When moving documentation, aliases handle redirects automatically. Update internal links strategically:
- DO update links in
/content/docs/,/content/product/, and/content/tutorials/. /content/blog/is historical — swap a broken link only for an equivalent replacement (stamplastmod); otherwise route around it with an alias/redirect.- 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.
For find/sed implementation patterns, see .claude/commands/move-doc/SKILL.md.
The left nav is data-driven from data/docs_menu_sections.yml, which is consumed by layouts/partials/docs/menu.html (the rendered nav), layouts/index.llms.txt (the curated /llms.txt index), and layouts/partials/llm-sitemap-walk.json (the /docs/llm-sitemap.json machine-readable sitemap). When you add, remove, or reorder top-level nav sections, all three flow through automatically. Per-section descriptions in /llms.txt come from each landing page's meta_desc front-matter — edit the page if you need to change how it reads in the index.
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, and the Pulumi 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.
The reference pages under content/docs/iac/concepts/resources/options/ show a classification callout (custom resource / component resource / both, plus per-SDK enforcement) rendered by the resource-option-scope shortcode. The classification data — and the summary table on that section's _index.md — is generated from data/resource_options.yaml, which is the single source of truth. When you add a new resource option, you must add an entry to data/resource_options.yaml and place the {{< resource-option-scope "<name>" >}} shortcode on the new page. That file's header comment is the authoritative step-by-step checklist; the build fails if a page references an option missing from the data file.
Blog posts carry two taxonomy axes (plus optional series):
category— the kind of post. This is a closed set defined indata/blog_categories.yaml(the single source of truth read byscripts/lint/lint-markdown.js). Category is required and singular: every post declares exactly onecategory:scalar value. Use the best-fitting specific kind, orgeneral(the default) for posts that don't fit cleanly (e.g. SEO comparisons or "what is X" explainers — those rely on tags instead).make lintfails on a missing value, a list value, or a value outside the set. Do not invent categories — pick an id from the data file. To add/rename one, editdata/blog_categories.yamlin a PR and raise it in #blogs. The blog docs-review additionally flags posts that landed in a specific kind but really belong ingeneral(and vice versa).tags— the topical axis (clouds, languages, products, scenarios). Curated-but-open, not build-enforced. Reuse a tag from the canonical vocabulary indata/blog_tags.yamland avoid near-duplicates (kubernetesnotk8s,infrastructure-as-codenotiac,pulumi-cloudnotpulumi-service,dotnetnotc#/.net). Tags are lowercase and hyphen-delimited.
See BLOGGING.md for the author-facing version of these rules.
The /docs section supports a light/dark/system theme toggle. Dark is light-first: light is the baseline (unchanged from before) and dark is a pure override. The whole system lives in theme/src/scss/docs/_docs-theme.scss (read its header comment first) and is driven by semantic --docs-* tokens defined on body.section-docs and re-pointed under html[data-theme="dark"]. It is scoped entirely to docs pages; nothing here can affect a non-docs page.
You must test both modes whenever you add or restyle a visible element on a docs page — new partials, shortcodes, cards, callouts, buttons, icons, or any markup that introduces its own colors, backgrounds, borders, or images. Toggle dark mode (theme switcher at the bottom of the docs sidebar) and confirm the element is legible and on-brand in both. Pure content changes (prose, code samples, frontmatter, links) are safe and don't need a dark-mode pass.
When something needs dark-mode work, prefer the existing levers over hand-written one-off colors:
- Use Tailwind
dark:variants. Thedark:variant is wired to the docsdata-themeattribute (@custom-variant darkintheme/src/scss/main.scss), sodark:bg-gray-900,dark:text-white, etc. work directly in templates and are automatically scoped to/docs. This is the most direct way to dark-style a new element. - Use the semantic tokens. Paint with
var(--docs-fg),--docs-fg-muted,--docs-bg,--docs-bg-alt,--docs-surface,--docs-border,--docs-card,--docs-link,--docs-ringrather than raw--color-*scales — they flip automatically. For selectors shared with non-docs pages, use thevar(--docs-TOKEN, ORIGINAL)fallback form so light source files stay untouched. - Lean on the automatic flips. Brand violet (
--color-violet-primary/text-violet-primary) and the literal Tailwind gray/white/violet utility classes (text-gray-950,bg-white,border-gray-200,bg-gray-50, etc.) are already remapped in the dark block, so markup authored with those gets dark mode for free. Surfaces styled via Tailwind@apply(e.g. content.btn-*variants) don't inherit a literal class and need their own dark override in_docs-theme.scss. - Theme-aware images: use the
layouts/partials/docs-logo.htmlpartial (light asset + optional-on-dark.svg), not a bare<img>, for any logo/mark whose colors don't read on a dark background. Masked icons in_icons.scsstint automatically;background:url()colored marks do not.
Before starting any documentation task, check .claude/commands/ for a relevant skill — there are well-structured skills covering common tasks like creating docs, reviewing PRs (see .claude/commands/docs-review/SKILL.md), moving files, and more. To see a full inventory, run .claude/commands/docs-tools/scripts/scrape-metadata.py.
Non-Claude agents: If the user runs a slash command or issues a short command that could be a skill name (e.g., fix-issue, new-doc), look for a matching file in .claude/commands/ to guide your actions.
Open as draft, mark ready when done. Each ready-transition fires one full review; thrashing draft → ready → draft burns budget. Leave AI authoring trailers in commits (Co-Authored-By: Claude ...) — stripping them is bad form and changes nothing about which review runs. Don't delete <!-- CLAUDE_REVIEW N/M --> comments — the re-entrant pipeline edits them in place. To refresh a stale review, mention @claude #update-review (fix-response / dispute / re-verify) or transition through draft and back to ready. Bare @claude (no hashtag) is for ad-hoc help,
For the full mechanics — refresh-pattern details, short-circuit thresholds, classifier internals — see CONTRIBUTING.md §AI-assisted contributions.