feat: improve build profile DevEx — a la carte collection picker, copy buttons, contributing guide link#7588
feat: improve build profile DevEx — a la carte collection picker, copy buttons, contributing guide link#7588
Conversation
- Add `make site-content` target for the content profile (blog/news/events/resources
included; members/integrations skipped) — previously only discoverable via an echo hint
- Add `make site-custom` target backed by a new `none` profile that starts with zero
preset exclusions, so BUILD_COLLECTIONS_EXCLUDE fully controls what is skipped
- Add `make profiles` target that lists all profiles with their excluded collections
and documents the BUILD_COLLECTIONS_EXCLUDE escape hatch
- Add `make cache-clean` to clear Gatsby cache without triggering a full production
rebuild (the existing `make clean` does both, which surprised contributors)
- Add `develop:content` and `develop:custom` npm scripts to support the new targets
- Fix `site-analyze` missing ## help comment and .PHONY entry
- Improve `site-fast` description from "Alternate method" to something accurate
- Update lite-placeholder.js with three contributor-facing improvements:
* Copy-to-clipboard button on every suggested make command
* A la carte collection picker — checkboxes generate the exact make command
needed; the current route's collection is pre-checked for convenience
* Link to the contributing guide's Environment Variables section
- Pass `collection` name through gatsby-node.js litePlaceholderPages context so
the template can pre-select the relevant checkbox
- Add `none` profile to build-collections.js as the foundation for a la carte builds
- Fix lint-staged silently failing on files in ESLint-ignored directories (add
--no-warn-ignored to suppress the spurious warning that tripped --max-warnings=0)
Signed-off-by: Lee Calcote <lee.calcote@layer5.io>
There was a problem hiding this comment.
Pull request overview
This PR improves developer experience around build profiles/targets and enhances the lite-mode placeholder UX to help contributors quickly re-enable disabled collections.
Changes:
- Adds new build profiles and ergonomics:
site-content,site-custom,profiles, andcache-clean; improves Makefile help text and fixessite-analyzevisibility. - Adds npm scripts for
contentandcustomlite development modes. - Upgrades the lite placeholder page with copy-to-clipboard buttons, an à la carte collection picker, and a link to contributing docs.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/build-collections.js | Adds a none lite profile to enable fully à la carte exclusion via BUILD_COLLECTIONS_EXCLUDE. |
| src/templates/lite-placeholder.js | Adds copy buttons, collection picker, and contributing guide link to the lite-mode placeholder page. |
| package.json | Adds develop:content, develop:custom, and start:content scripts for new profiles. |
| gatsby-node.js | Passes enabledBy and collection into placeholder context to drive UI defaults. |
| Makefile | Introduces new targets (site-content, site-custom, profiles, cache-clean) and improves help output. |
| .lintstagedrc.js | Prevents --max-warnings=0 failures caused by ESLint “ignored file” warnings during lint-staged runs. |
| if (typeof navigator === "undefined") return; | ||
| navigator.clipboard.writeText(text).then(() => { | ||
| setCopied(true); | ||
| setTimeout(() => setCopied(false), 1500); | ||
| }); |
There was a problem hiding this comment.
navigator.clipboardis not guaranteed to exist (and can fail outside secure contexts or due to permissions). As written, this can throw (clipboard undefined) or create an unhandled promise rejection (writeText rejected). Guard with a check fornavigator.clipboard?.writeText(and optionallywindow.isSecureContext) and add a .catch(...)` path (or fallback copy approach) so the UI fails gracefully instead of erroring.
| if (typeof navigator === "undefined") return; | |
| navigator.clipboard.writeText(text).then(() => { | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 1500); | |
| }); | |
| if ( | |
| typeof navigator === "undefined" || | |
| typeof window === "undefined" || | |
| !window.isSecureContext || | |
| !navigator.clipboard?.writeText | |
| ) { | |
| return; | |
| } | |
| navigator.clipboard | |
| .writeText(text) | |
| .then(() => { | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 1500); | |
| }) | |
| .catch(() => {}); |
| const [copied, setCopied] = useState(false); | ||
|
|
||
| const handleCopy = useCallback(() => { | ||
| if (typeof navigator === "undefined") return; | ||
| navigator.clipboard.writeText(text).then(() => { | ||
| setCopied(true); | ||
| setTimeout(() => setCopied(false), 1500); | ||
| }); | ||
| }, [text]); |
There was a problem hiding this comment.
The setTimeout is not cleaned up, so this can attempt to update state after unmount (React warns about state updates on unmounted components). Store the timeout id (e.g., via useRef) and clear it in an effect cleanup; also consider clearing any existing timer before setting a new one on repeated clicks.
| <button | ||
| onClick={handleCopy} | ||
| aria-label={copied ? "Copied!" : `Copy command: ${text}`} | ||
| style={{ | ||
| flexShrink: 0, | ||
| padding: "0.2rem 0.6rem", | ||
| background: copied ? "#00b39f" : "transparent", | ||
| border: "1px solid #00b39f", | ||
| borderRadius: "4px", | ||
| color: copied ? "#fff" : "#00b39f", | ||
| cursor: "pointer", | ||
| fontSize: "0.75rem", | ||
| lineHeight: 1.4, | ||
| transition: "background 0.15s, color 0.15s", | ||
| whiteSpace: "nowrap", | ||
| }} | ||
| > | ||
| {copied ? "Copied!" : "Copy"} | ||
| </button> |
There was a problem hiding this comment.
The “Copied!” state is purely visual; many screen readers won’t announce the change reliably. Consider adding an aria-live="polite" status element (or role="status") that announces “Copied” on success. Also consider adding type="button" to avoid unintended form submission behavior if this component is ever rendered inside a form.
| <button | |
| onClick={handleCopy} | |
| aria-label={copied ? "Copied!" : `Copy command: ${text}`} | |
| style={{ | |
| flexShrink: 0, | |
| padding: "0.2rem 0.6rem", | |
| background: copied ? "#00b39f" : "transparent", | |
| border: "1px solid #00b39f", | |
| borderRadius: "4px", | |
| color: copied ? "#fff" : "#00b39f", | |
| cursor: "pointer", | |
| fontSize: "0.75rem", | |
| lineHeight: 1.4, | |
| transition: "background 0.15s, color 0.15s", | |
| whiteSpace: "nowrap", | |
| }} | |
| > | |
| {copied ? "Copied!" : "Copy"} | |
| </button> | |
| <> | |
| <button | |
| type="button" | |
| onClick={handleCopy} | |
| aria-label={copied ? "Copied!" : `Copy command: ${text}`} | |
| style={{ | |
| flexShrink: 0, | |
| padding: "0.2rem 0.6rem", | |
| background: copied ? "#00b39f" : "transparent", | |
| border: "1px solid #00b39f", | |
| borderRadius: "4px", | |
| color: copied ? "#fff" : "#00b39f", | |
| cursor: "pointer", | |
| fontSize: "0.75rem", | |
| lineHeight: 1.4, | |
| transition: "background 0.15s, color 0.15s", | |
| whiteSpace: "nowrap", | |
| }} | |
| > | |
| {copied ? "Copied!" : "Copy"} | |
| </button> | |
| <span | |
| aria-live="polite" | |
| role="status" | |
| style={{ | |
| position: "absolute", | |
| width: "1px", | |
| height: "1px", | |
| padding: 0, | |
| margin: "-1px", | |
| overflow: "hidden", | |
| clip: "rect(0, 0, 0, 0)", | |
| whiteSpace: "nowrap", | |
| border: 0, | |
| }} | |
| > | |
| {copied ? "Copied!" : ""} | |
| </span> | |
| </> |
| <SEO | ||
| title={heading} | ||
| description={`${description} ${restoreCommands[0].cmd}`} | ||
| /> |
There was a problem hiding this comment.
SEO is rendered both in the component body and in the Gatsby Head export, which can produce duplicate meta tags and inconsistent head state. Prefer a single approach (typically the Head export in Gatsby 5). Removing the in-body <SEO ... /> will avoid duplication and keep head management in one place.
| @@ -41,15 +302,16 @@ const LitePlaceholder = ({ pageContext, location }) => { | |||
| export default LitePlaceholder; | |||
|
|
|||
| export const Head = ({ pageContext }) => { | |||
There was a problem hiding this comment.
SEO is rendered both in the component body and in the Gatsby Head export, which can produce duplicate meta tags and inconsistent head state. Prefer a single approach (typically the Head export in Gatsby 5). Removing the in-body <SEO ... /> will avoid duplication and keep head management in one place.
| return ( | ||
| <SEO | ||
| title={heading} | ||
| description={`${description} ${instructions}`.trim()} | ||
| description={`${description} ${commands[0].cmd}`.trim()} | ||
| /> |
There was a problem hiding this comment.
SEO is rendered both in the component body and in the Gatsby Head export, which can produce duplicate meta tags and inconsistent head state. Prefer a single approach (typically the Head export in Gatsby 5). Removing the in-body <SEO ... /> will avoid duplication and keep head management in one place.
| function generateCommand(included) { | ||
| const excluded = ALL_COLLECTIONS.filter((c) => !included.has(c)).sort(); | ||
|
|
||
| if (excluded.length === 0) return "make site-full"; | ||
| if (excluded.length === ALL_COLLECTIONS.length) return "make site"; | ||
|
|
||
| const isContentProfile = | ||
| excluded.length === 2 && | ||
| excluded[0] === "integrations" && | ||
| excluded[1] === "members"; | ||
| if (isContentProfile) return "make site-content"; | ||
|
|
||
| return `BUILD_COLLECTIONS_EXCLUDE=${excluded.join(",")} make site-custom`; | ||
| } |
There was a problem hiding this comment.
The isContentProfile detection relies on excluded.sort() producing a specific positional order, which is fragile if collection names change or new collections are introduced. Prefer checking membership (e.g., “excluded has exactly members+integrations”) instead of index-based comparisons to make the logic resilient.
Summary
This PR improves the developer experience around site build profiles, make targets, and the lite-mode placeholder page that contributors see when visiting a disabled route.
Make targets
make site-content— new target for thecontentprofile (blog/news/events/resources included; members/integrations skipped). Previously the content profile existed in code but had no make target — it was only discoverable via an echo hint inmake site.make site-custom— new target for fully à la carte builds. Uses a newnonebase profile (zero preset exclusions) soBUILD_COLLECTIONS_EXCLUDEis the only thing that controls what is skipped. Example:BUILD_COLLECTIONS_EXCLUDE=members,events make site-custommake profiles— new target that lists all profiles, their excluded collections, and theBUILD_COLLECTIONS_EXCLUDEescape hatch.make cache-clean— new target that clears the Gatsby cache without triggering a full production rebuild. The existingmake cleandoes both, which surprised contributors who just wanted to reset cache.make site-analyze— fixed missing##help comment and.PHONYentry (was invisible tomakehelp output).site-fast— description updated from the useless "Alternate method" to a precise description of what it actually skips.npm scripts
develop:content— mirrorsdevelop:litewithLITE_BUILD_PROFILE=content.develop:custom— usesLITE_BUILD_PROFILE=nonefor the à la carte target.start:content— alias consistent with the existingstart/start:fullpattern.build-collections.js
noneprofile — excludes nothing by default, makingBUILD_COLLECTIONS_EXCLUDEthe sole driver of exclusions formake site-custom.Lite-mode placeholder page
The placeholder shown when a contributor visits a disabled route (e.g.
/blogin core mode) now has three improvements:makecommand.CONTRIBUTING.md#environment-variables) for full profile documentation.Lint-staged fix
Added
--no-warn-ignoredto the ESLint call in.lintstagedrc.js.src/utils/is in ESLint's ignore list, but lint-staged passes files explicitly, causing a warning that tripped--max-warnings=0on every commit touching asrc/utils/file.Test plan
make(no args) — help output listssite-content,site-custom,profiles,cache-clean,site-analyzemake profiles— prints all four profiles with accurate exclusion listsmake site— echo banner mentionssite-contentandsite-fullmake site-content— dev server starts with blog/news/events/resources enabledmake cache-clean— clears.cachewithout starting a buildBUILD_COLLECTIONS_EXCLUDE=members,events make site-custom— only listed collections excluded/blogin core mode — placeholder showsmake site-contentfirst with Copy button;blogpre-checked in picker/community/members— placeholder shows onlymake site-fullmake site-full; none checked →make siteCONTRIBUTING.md#environment-variables