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. |
src/templates/lite-placeholder.js
Outdated
| 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(() => {}); |
There was a problem hiding this comment.
Fixed — added isSecureContext and optional-chaining clipboard guard; failures are silently caught.
| 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.
There was a problem hiding this comment.
Fixed — timeout ID stored in a useRef, cleared in a useEffect cleanup and before each new timer.
src/templates/lite-placeholder.js
Outdated
| <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> | |
| </> |
There was a problem hiding this comment.
Fixed — added type="button" and an aria-live="polite" status span that announces copy success to screen readers.
src/templates/lite-placeholder.js
Outdated
| <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.
There was a problem hiding this comment.
Fixed — removed the SEO component from the component body; head management is now exclusively in the Head export.
| @@ -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.
There was a problem hiding this comment.
Fixed — same as above.
| 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.
There was a problem hiding this comment.
Fixed — same as above.
| 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.
There was a problem hiding this comment.
Fixed — replaced index-based check with Set membership (CONTENT_EXCLUSIONS) so the logic is resilient to collection renames or insertions.
- Guard navigator.clipboard with isSecureContext + optional chaining before writing; eliminates throws in non-secure or unsupported contexts - Store setTimeout id in useRef and clear in useEffect cleanup to avoid state-update-on-unmounted-component React warning on rapid unmount - Add type="button" to prevent unintended form submission - Add aria-live polite status span so screen readers announce copy success - Remove duplicate <SEO> from component body; keep only the Head export (Gatsby 5 manages head state via the exported Head — dual rendering produced duplicate meta tags) - Replace index-based isContentProfile check with Set membership test to guard against future collection renames or insertions Signed-off-by: Lee Calcote <lee.calcote@layer5.io>
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