From 6569d243308a01919811b0af213c055874e3c15a Mon Sep 17 00:00:00 2001 From: Andre Landgraf Date: Thu, 30 Apr 2026 08:36:11 -0700 Subject: [PATCH] chore: add fallow + clean up dead code and duplication Wire up [fallow](https://github.com/fallow-rs/fallow) as a dev dependency and document a workflow in AGENTS.md for running `npx fallow dead-code` and `npx fallow dupes` after every change, with first-principles guidance on when to remove vs leverage flagged code and when duplication is worth unifying vs leaving alone. Then ran fallow across the repo and acted only on the findings that genuinely made the codebase clearer. Dead code removed: - Delete unused custom components: `pillar-strip.tsx`, `appkit-version-picker.tsx`, `selected-items.tsx`, `recipe-list.tsx`, `copy-button.tsx` (verified zero references across the repo) - Delete dead exports/declarations: `getRecipeMarkdownApiPath`, `COOKBOOK_FILES`/`CookbookFile`, `pillars`/`Pillar`, `SolutionTag` alias, unused `useAllRawRecipeMarkdown`/`useRecipeSections`/`useRawExampleMarkdown` hooks, `buildRobotsTxt` re-export, dead re-exports in `content-markdown.ts` - Demote internal-only types to module-private (no behavior change) - Drop transitive-only deps `date-fns` and `@hookform/resolvers` - Update `author-recipes-and-cookbooks` skill to match the actual hooks Duplication unified (real same-logic-twice cases): - New `useAgentMarkdown` hook consolidates the agent-prompt builder shared by `ai-export-menu` and `copy-prompt-button` - New `useCookbookMarkdown` hook removes ~25 lines of boilerplate from each of the 5 cookbook pages under `src/pages/templates/` - New `BootstrapCopyButton` replaces the duplicated copy-state machines in `hero-section` and `wizard-flow` (also resolves a `CopyPromptButton` name collision) - Extract `buildIncludedTemplateLinks` and `validateContentFolder` helpers for duplicated loops in `build-example-markdown.ts` and `scripts/validate-content.mjs` Duplication left alone (intentional repetition for clarity): - Test files where independent flows happen to share shape - Cross-callsite filter patterns where each consumer reads the catalog differently - Tiny 5-line handler boilerplate where extracting a helper saves nothing Configuration: - `.fallowrc.json` scopes future runs by ignoring `examples/**` and `content/**`, plus documented false positives (mcp-handler peer dep, Docusaurus type wiring, transitive remark deps, the `mcp-handler` HTTP method exports, and the shadcn UI kit catalog) Net: -1,091 lines (-1,396 / +305) across 36 files; duplicated lines drop from 14.9% to 2.2%. `npm run typecheck`, `npm run build`, and 165 smoke tests pass. --- .../author-recipes-and-cookbooks/SKILL.md | 2 +- .fallowrc.json | 28 ++ .gitignore | 1 + AGENTS.md | 30 ++ api/content-markdown.ts | 2 +- package-lock.json | 143 ++++++- package.json | 3 +- plugins/content-entries.ts | 6 +- plugins/cookbooks.ts | 2 +- plugins/robots-txt.ts | 2 - scripts/validate-content.mjs | 99 +++-- src/components/ai-export-menu.tsx | 127 +----- src/components/code/copy-button.tsx | 57 --- src/components/cookbooks/recipe-list.tsx | 72 ---- src/components/copy-prompt-button.tsx | 97 +---- src/components/docs/appkit-version-picker.tsx | 138 ------- src/components/home/bootstrap-copy-button.tsx | 131 +++++++ src/components/home/hero-section.tsx | 113 +----- src/components/home/pillar-strip.tsx | 362 ------------------ src/components/home/wizard-flow.tsx | 79 +--- src/components/templates/selected-items.tsx | 37 -- src/lib/bootstrap-prompt.ts | 4 - src/lib/content-markdown.ts | 17 +- src/lib/content-sections.ts | 2 +- src/lib/cookbook-composition.ts | 2 +- src/lib/examples/build-example-markdown.ts | 43 ++- src/lib/landing-content.ts | 37 -- src/lib/recipes/recipes.ts | 2 +- src/lib/solutions/solutions.ts | 4 +- src/lib/use-agent-markdown.ts | 115 ++++++ src/lib/use-cookbook-markdown.ts | 45 +++ src/lib/use-raw-content-markdown.ts | 24 -- src/pages/templates/ai-chat-app.tsx | 31 +- src/pages/templates/app-with-lakebase.tsx | 31 +- src/pages/templates/genie-analytics-app.tsx | 31 +- src/pages/templates/hello-world-app.tsx | 31 +- src/pages/templates/lakebase-off-platform.tsx | 33 +- .../templates/operational-data-analytics.tsx | 33 +- src/theme/Logo/index.tsx | 2 +- src/theme/TOC/index.tsx | 2 +- 40 files changed, 624 insertions(+), 1396 deletions(-) create mode 100644 .fallowrc.json delete mode 100644 src/components/code/copy-button.tsx delete mode 100644 src/components/cookbooks/recipe-list.tsx delete mode 100644 src/components/docs/appkit-version-picker.tsx create mode 100644 src/components/home/bootstrap-copy-button.tsx delete mode 100644 src/components/home/pillar-strip.tsx delete mode 100644 src/components/templates/selected-items.tsx create mode 100644 src/lib/use-agent-markdown.ts create mode 100644 src/lib/use-cookbook-markdown.ts diff --git a/.agents/skills/author-recipes-and-cookbooks/SKILL.md b/.agents/skills/author-recipes-and-cookbooks/SKILL.md index 12c08a8..d0ba25b 100644 --- a/.agents/skills/author-recipes-and-cookbooks/SKILL.md +++ b/.agents/skills/author-recipes-and-cookbooks/SKILL.md @@ -61,7 +61,7 @@ All three are registered in `src/lib/recipes/recipes.ts`, share a flat `/templat - add an entry to `cookbooks` with `id`, `name`, `description`, `recipeIds` - rely on `createCookbook()` to derive `tags` and `services` 4. Create `src/pages/templates/.tsx` following the existing pattern: - - import `CookbookDetail`, `cookbooks`, `useAllRawRecipeMarkdown` + - import `CookbookDetail`, `cookbooks`, `useAllRecipeSections`, `useCookbookIntro`, and `composeCookbookMarkdown` - import each recipe markdown module from `@site/content/recipes//content.md` - select the cookbook with `cookbooks.find((c) => c.id === "")` - build `rawMarkdown` from `cookbook.recipeIds` joined with `\n\n---\n\n` diff --git a/.fallowrc.json b/.fallowrc.json new file mode 100644 index 0000000..7c1269c --- /dev/null +++ b/.fallowrc.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json", + "ignorePatterns": ["examples/**", "content/**", ".docusaurus/**", "build/**"], + "ignoreDependencies": [ + "@modelcontextprotocol/sdk", + "@docusaurus/module-type-aliases", + "@docusaurus/tsconfig", + "mdast", + "mdast-util-mdx", + "unified", + "unist", + "unist-util-visit", + "prism-react-renderer" + ], + "ignoreExports": [ + { + "file": "api/mcp.ts", + "exports": ["GET", "POST", "DELETE"] + }, + { + "file": "src/components/ui/**", + "exports": ["*"] + } + ], + "rules": { + "unused-dependencies": "warn" + } +} diff --git a/.gitignore b/.gitignore index 906ac04..36d9bec 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ examples/**/.databricks/ # Screenshots /screenshots .vercel +.fallow/ diff --git a/AGENTS.md b/AGENTS.md index 873eab2..8528da8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,7 @@ For every change to DevHub, do the following: - run `npm run fmt` to format the code - run `npm run typecheck` to verify types are correct - run `npm run verify:images` to verify example/resource images (16:9, ≥1600x900) when you've added or changed anything under `static/img/examples/` +- run `npx fallow dead-code` and `npx fallow dupes` to check for dead code and duplication after your changes (see "Dead Code & Duplication" below) - use agent-browser to verify the changes - use the `seo-audit` skill to verify all changes are SEO-friendly - use the `frontend-design` skill to verify all changes adhere to the design principles @@ -59,6 +60,35 @@ This repository uses **npm** exclusively. Do not use bun, yarn, or pnpm. All scr - Use brand colors and avoid raw color values - Always use shadcn/ui components as the foundation for all UI +## Dead Code & Duplication + +After making changes, always run [fallow](https://github.com/fallow-rs/fallow) to keep the codebase clean: + +```bash +npx fallow dead-code +npx fallow dupes +``` + +Then reason from first principles before acting on the report — do not blindly delete or merge: + +### Dead code (`fallow dead-code`) + +For every flagged item, decide between two options: + +- **Remove it** if the code is genuinely unreachable, no longer referenced, or was scaffolding that never got wired up. +- **Wire it up** if the code is something the change you just made should actually be using (e.g. you wrote a parallel implementation and forgot the existing helper). In that case, leverage the existing code instead of duplicating it. + +Always prefer the simpler outcome: a smaller codebase with no orphaned exports. + +### Duplication (`fallow dupes`) + +Duplication reports are a hint, not a verdict. For each cluster: + +- **Ignore it** when the matches are not _real_ duplicates — e.g. similar shapes that happen to look alike, generated code, fixtures, or two functions that share a structure but model genuinely different concepts. Some repetition is good; premature abstraction is worse than a little copy-paste. +- **Unify it** only when the matches are clearly the same type, the same function, or the same logic expressed twice. In that case, extract a shared helper / type / component and replace the call sites. + +When in doubt, leave it alone and write a short note in the PR explaining why the duplication is intentional. + ## Browser Automation Use `agent-browser` for web automation. Run `agent-browser --help` for all commands. diff --git a/api/content-markdown.ts b/api/content-markdown.ts index 3ecd649..efa5a69 100644 --- a/api/content-markdown.ts +++ b/api/content-markdown.ts @@ -5,10 +5,10 @@ import { ABOUT_DEVHUB_SLUG } from "../src/lib/bootstrap-prompt"; import { hasContentSlug, hasSolutionSlug, - joinContentSections, readContentSections, readCookbookIntro, } from "../src/lib/content-markdown"; +import { joinContentSections } from "../src/lib/content-sections"; import { buildCookbookMarkdownDocument } from "../src/lib/cookbook-composition"; import { expandMdxImports } from "../src/lib/expand-mdx"; import { diff --git a/package-lock.json b/package-lock.json index 5d39fee..9b8b23e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "@docusaurus/core": "3.9.2", "@docusaurus/preset-classic": "3.9.2", "@docusaurus/theme-mermaid": "3.9.2", - "@hookform/resolvers": "^5.2.2", "@mdx-js/react": "^3.0.0", "@modelcontextprotocol/sdk": "^1.25.2", "@vercel/analytics": "^2.0.1", @@ -21,7 +20,6 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.0.0", "cmdk": "^1.1.1", - "date-fns": "^4.1.0", "docusaurus-lunr-search": "3.6.0", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", @@ -48,6 +46,7 @@ "@playwright/test": "^1.58.2", "@tailwindcss/postcss": "^4.2.2", "@vercel/node": "^5.6.15", + "fallow": "^2.45.0", "husky": "^9.1.7", "image-size": "^2.0.2", "postcss": "^8.5.8", @@ -4863,6 +4862,104 @@ "node": ">=18" } }, + "node_modules/@fallow-cli/darwin-arm64": { + "version": "2.45.0", + "resolved": "https://npm-proxy.dev.databricks.com/@fallow-cli/darwin-arm64/-/darwin-arm64-2.45.0.tgz", + "integrity": "sha512-Gs+pHUcRMoMpIEJamKQegvs296xoVEFMFzbgaEYwlRrOQEW6Mcgkxbv0sMGU2sFkM82QGj88KqC5ZrtcxMfN3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@fallow-cli/darwin-x64": { + "version": "2.45.0", + "resolved": "https://npm-proxy.dev.databricks.com/@fallow-cli/darwin-x64/-/darwin-x64-2.45.0.tgz", + "integrity": "sha512-aPs2iZvgCfoQdCAmn0Ydsb/j6NsRD+fzlV7i3px2JUgksuN1h9rXDKJh+2gj8jZa5U7uKi0tUlYix0QGifjy4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@fallow-cli/linux-arm64-gnu": { + "version": "2.45.0", + "resolved": "https://npm-proxy.dev.databricks.com/@fallow-cli/linux-arm64-gnu/-/linux-arm64-gnu-2.45.0.tgz", + "integrity": "sha512-26ZJQ8SUCRovrIrI0/nCeOEF4ZdenKYzlUiZ19cEwYGlS3CSq0wKQ+5NbbmICJk2P80yL92F4vINpjubcEcJow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@fallow-cli/linux-arm64-musl": { + "version": "2.45.0", + "resolved": "https://npm-proxy.dev.databricks.com/@fallow-cli/linux-arm64-musl/-/linux-arm64-musl-2.45.0.tgz", + "integrity": "sha512-u7GtjWU4ElU7BnPH2rBkk2lYLYeWRNKYnbz8HbjeH8aPetL+OneTJykPMidPqd3HZNpn1ytEi9wL5m+WwuW2gg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@fallow-cli/linux-x64-gnu": { + "version": "2.45.0", + "resolved": "https://npm-proxy.dev.databricks.com/@fallow-cli/linux-x64-gnu/-/linux-x64-gnu-2.45.0.tgz", + "integrity": "sha512-sl9DAMgKw0L+w7kQ/LXEN/fR+AJ0ERmOAGwPK3oCmPrffkqZKE1YN6NtLO/6/hjMQoGeC3GZKsP1kMpQ3bj26w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@fallow-cli/linux-x64-musl": { + "version": "2.45.0", + "resolved": "https://npm-proxy.dev.databricks.com/@fallow-cli/linux-x64-musl/-/linux-x64-musl-2.45.0.tgz", + "integrity": "sha512-jnuIkY1V+pfU7oY08GXupHQdyTFQ/LqxBVE0hN036st9E2V/hFT/5YR128Umli8uIx05a4rBaLAMAD6rH83IqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@fallow-cli/win32-x64-msvc": { + "version": "2.45.0", + "resolved": "https://npm-proxy.dev.databricks.com/@fallow-cli/win32-x64-msvc/-/win32-x64-msvc-2.45.0.tgz", + "integrity": "sha512-ILherp9BHDILs73gnZn0280scrH3YRZdWzD8caWd5sdIUIszjMV1Y50Nbi0KLIb85Hn2D9h/hlkRtCcI8j6sbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@fastify/busboy": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", @@ -4926,16 +5023,6 @@ "hono": "^4" } }, - "node_modules/@hookform/resolvers": { - "version": "5.2.2", - "license": "MIT", - "dependencies": { - "@standard-schema/utils": "^0.3.0" - }, - "peerDependencies": { - "react-hook-form": "^7.55.0" - } - }, "node_modules/@iconify/types": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", @@ -7933,10 +8020,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@standard-schema/utils": { - "version": "0.3.0", - "license": "MIT" - }, "node_modules/@svgr/babel-plugin-add-jsx-attribute": { "version": "8.0.0", "license": "MIT", @@ -13338,6 +13421,34 @@ "node": ">=0.10.0" } }, + "node_modules/fallow": { + "version": "2.45.0", + "resolved": "https://npm-proxy.dev.databricks.com/fallow/-/fallow-2.45.0.tgz", + "integrity": "sha512-ygFfSeeb2aCQNMRBuntNPXQE+3EV1MN5tkr/wmNa9Ot3CHj8dYGMhhKZZf1w1aaGBOd/8D40d6w0OzuWz5AHjA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "2.1.2" + }, + "bin": { + "fallow": "bin/fallow", + "fallow-lsp": "bin/fallow-lsp", + "fallow-mcp": "bin/fallow-mcp" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@fallow-cli/darwin-arm64": "2.45.0", + "@fallow-cli/darwin-x64": "2.45.0", + "@fallow-cli/linux-arm64-gnu": "2.45.0", + "@fallow-cli/linux-arm64-musl": "2.45.0", + "@fallow-cli/linux-x64-gnu": "2.45.0", + "@fallow-cli/linux-x64-musl": "2.45.0", + "@fallow-cli/win32-x64-msvc": "2.45.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "license": "MIT" diff --git a/package.json b/package.json index b78067d..63979c1 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "@docusaurus/core": "3.9.2", "@docusaurus/preset-classic": "3.9.2", "@docusaurus/theme-mermaid": "3.9.2", - "@hookform/resolvers": "^5.2.2", "@mdx-js/react": "^3.0.0", "@modelcontextprotocol/sdk": "^1.25.2", "@vercel/analytics": "^2.0.1", @@ -40,7 +39,6 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.0.0", "cmdk": "^1.1.1", - "date-fns": "^4.1.0", "docusaurus-lunr-search": "3.6.0", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", @@ -67,6 +65,7 @@ "@playwright/test": "^1.58.2", "@tailwindcss/postcss": "^4.2.2", "@vercel/node": "^5.6.15", + "fallow": "^2.45.0", "husky": "^9.1.7", "image-size": "^2.0.2", "postcss": "^8.5.8", diff --git a/plugins/content-entries.ts b/plugins/content-entries.ts index 5a53077..a0abbbc 100644 --- a/plugins/content-entries.ts +++ b/plugins/content-entries.ts @@ -4,10 +4,12 @@ import type { LoadContext, Plugin } from "@docusaurus/types"; import { getContentSlugs, getSolutionSlugs, - joinContentSections, readContentSections, - type ContentSections, } from "../src/lib/content-markdown"; +import { + joinContentSections, + type ContentSections, +} from "../src/lib/content-sections"; import { recipes, examples, diff --git a/plugins/cookbooks.ts b/plugins/cookbooks.ts index bdaa2bc..239a95e 100644 --- a/plugins/cookbooks.ts +++ b/plugins/cookbooks.ts @@ -5,7 +5,7 @@ import { } from "../src/lib/content-markdown"; import { cookbooks } from "../src/lib/recipes/recipes"; -export type CookbooksGlobalData = { +type CookbooksGlobalData = { /** Raw `content/cookbooks//intro.md` bodies keyed by cookbook id. */ introsBySlug: Record; }; diff --git a/plugins/robots-txt.ts b/plugins/robots-txt.ts index 7923bf1..6f2e25b 100644 --- a/plugins/robots-txt.ts +++ b/plugins/robots-txt.ts @@ -37,5 +37,3 @@ export default function robotsTxtPlugin(context: LoadContext): Plugin { }, }; } - -export { buildRobotsTxt }; diff --git a/scripts/validate-content.mjs b/scripts/validate-content.mjs index 5878fd6..7d17d99 100644 --- a/scripts/validate-content.mjs +++ b/scripts/validate-content.mjs @@ -24,88 +24,85 @@ const COOKBOOK_ALLOWED_FILES = new Set(["intro.md"]); /** @type {string[]} */ const errors = []; -for (const section of RESOURCE_SECTIONS) { - const sectionDir = resolve(ROOT, "content", section); - const entries = readdirSync(sectionDir); - - for (const entry of entries) { +/** + * Validate every entry in a content section folder. Each entry must be a + * directory whose direct children are markdown files from `allowedFiles`, + * with `requiredFile` present when set. + * + * @param {object} opts + * @param {string} opts.sectionPath e.g. "content/recipes" — used in error messages + * @param {string} opts.sectionDir absolute filesystem path to the section + * @param {Set} opts.allowedFiles whitelist of allowed direct-child filenames + * @param {string=} opts.requiredFile filename that must be present (omit for none) + * @param {string} opts.emptyHint trailing instruction appended to the "is empty" error + * @param {string} opts.flatHint trailing instruction appended to the "is not a directory" error + */ +function validateContentFolder({ + sectionPath, + sectionDir, + allowedFiles, + requiredFile, + emptyHint, + flatHint, +}) { + for (const entry of readdirSync(sectionDir)) { const entryPath = resolve(sectionDir, entry); const stats = statSync(entryPath); if (!stats.isDirectory()) { - errors.push( - `content/${section}/${entry} is not a directory. Flat files are not allowed. Move to content/${section}/${entry.replace(/\.md$/, "")}/content.md.`, - ); + errors.push(`${sectionPath}/${entry} is not a directory. ${flatHint}`); continue; } const files = readdirSync(entryPath); if (files.length === 0) { - errors.push(`content/${section}/${entry}/ is empty. Add content.md.`); + errors.push(`${sectionPath}/${entry}/ is empty. ${emptyHint}`); continue; } for (const file of files) { const childPath = resolve(entryPath, file); - const childStats = statSync(childPath); - if (!childStats.isFile()) { + if (!statSync(childPath).isFile()) { errors.push( - `content/${section}/${entry}/${file} is a directory. Only markdown files are allowed.`, + `${sectionPath}/${entry}/${file} is a directory. Only markdown files are allowed.`, ); continue; } - if (!RESOURCE_ALLOWED_FILES.has(file)) { + if (!allowedFiles.has(file)) { errors.push( - `content/${section}/${entry}/${file} is not an allowed filename. Allowed: ${[...RESOURCE_ALLOWED_FILES].sort().join(", ")}.`, + `${sectionPath}/${entry}/${file} is not an allowed filename. Allowed: ${[...allowedFiles].sort().join(", ")}.`, ); } } - if (!files.includes(RESOURCE_REQUIRED_FILE)) { + if (requiredFile && !files.includes(requiredFile)) { errors.push( - `content/${section}/${entry}/ is missing the required ${RESOURCE_REQUIRED_FILE}.`, + `${sectionPath}/${entry}/ is missing the required ${requiredFile}.`, ); } } } +for (const section of RESOURCE_SECTIONS) { + validateContentFolder({ + sectionPath: `content/${section}`, + sectionDir: resolve(ROOT, "content", section), + allowedFiles: RESOURCE_ALLOWED_FILES, + requiredFile: RESOURCE_REQUIRED_FILE, + emptyHint: "Add content.md.", + flatHint: `Flat files are not allowed. Move to content/${section}//content.md.`, + }); +} + const cookbooksDir = resolve(ROOT, "content", "cookbooks"); if (existsSync(cookbooksDir)) { - for (const entry of readdirSync(cookbooksDir)) { - const entryPath = resolve(cookbooksDir, entry); - const stats = statSync(entryPath); - - if (!stats.isDirectory()) { - errors.push( - `content/cookbooks/${entry} is not a directory. Cookbook content lives under content/cookbooks//.`, - ); - continue; - } - - const files = readdirSync(entryPath); - if (files.length === 0) { - errors.push( - `content/cookbooks/${entry}/ is empty. Add at least intro.md or remove the folder.`, - ); - continue; - } - - for (const file of files) { - const childPath = resolve(entryPath, file); - const childStats = statSync(childPath); - if (!childStats.isFile()) { - errors.push( - `content/cookbooks/${entry}/${file} is a directory. Only markdown files are allowed.`, - ); - continue; - } - if (!COOKBOOK_ALLOWED_FILES.has(file)) { - errors.push( - `content/cookbooks/${entry}/${file} is not an allowed filename. Allowed: ${[...COOKBOOK_ALLOWED_FILES].sort().join(", ")}.`, - ); - } - } - } + validateContentFolder({ + sectionPath: "content/cookbooks", + sectionDir: cookbooksDir, + allowedFiles: COOKBOOK_ALLOWED_FILES, + emptyHint: "Add at least intro.md or remove the folder.", + flatHint: "Cookbook content lives under content/cookbooks//.", + }); } if (errors.length > 0) { diff --git a/src/components/ai-export-menu.tsx b/src/components/ai-export-menu.tsx index 5fac2bb..07e4d3b 100644 --- a/src/components/ai-export-menu.tsx +++ b/src/components/ai-export-menu.tsx @@ -1,5 +1,4 @@ -import { useCallback, useEffect, useRef } from "react"; -import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; +import { useCallback } from "react"; import { toast } from "sonner"; import { ClipboardCopyIcon, @@ -23,127 +22,33 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { - buildAboutDevhubForBrowserCopy, - buildMarkdownWithAboutDevhubLeadIn, - useAboutDevhubBody, -} from "@/lib/copy-about-devhub"; + useAgentMarkdown, + type AgentMarkdownInput, +} from "@/lib/use-agent-markdown"; -type AIExportMenuProps = { - rawMarkdown?: string; - rawMarkdownUrl?: string; - /** Extra markdown appended after the main content (e.g. code snippets, links). */ - additionalMarkdown?: string; - /** - * When set, Copy Markdown uses exactly `about-devhub + --- + this string`, - * ignoring frontmatter and raw/additional markdown (e.g. example pages align - * with the Get started "Copy prompt" button). - */ - agentBodyAfterAbout?: string; - /** - * When true, the About DevHub preamble is omitted from copy/send actions - * (used on reference docs pages — agents only need the doc body itself, the - * preamble is already part of resource prompts that link to the doc). - */ - omitAboutDevhubPreamble?: boolean; - title: string; - description: string; - permalink: string; +type AIExportMenuProps = AgentMarkdownInput & { disabled?: boolean; disabledTooltip?: string; }; export function AIExportMenu({ - rawMarkdown, - rawMarkdownUrl, - additionalMarkdown, - agentBodyAfterAbout, - omitAboutDevhubPreamble = false, - title, - description, - permalink, disabled = false, disabledTooltip = "select a template to copy", + ...input }: AIExportMenuProps) { - const { siteConfig } = useDocusaurusContext(); - const buildSiteUrl = siteConfig.url.replace(/\/$/, ""); - const baseUrl = - typeof window !== "undefined" ? window.location.origin : buildSiteUrl; - const fullUrl = baseUrl + permalink; + const { baseUrl, fullUrl, buildAIMarkdown, ensureFetched } = + useAgentMarkdown(input); const mcpUrl = baseUrl + "/api/mcp"; - const aboutDevhubBody = useAboutDevhubBody(); - const fetchedMarkdownRef = useRef(null); - useEffect(() => { - if (rawMarkdown || !rawMarkdownUrl) return; - fetch(rawMarkdownUrl) - .then((res) => (res.ok ? res.text() : null)) - .then((text) => { - fetchedMarkdownRef.current = text; - }) - .catch(() => {}); - }, [rawMarkdown, rawMarkdownUrl]); - - const resolveContent = useCallback((): string => { - if (rawMarkdown) return rawMarkdown; - if (fetchedMarkdownRef.current) return fetchedMarkdownRef.current; - return ""; - }, [rawMarkdown]); - - const buildAIMarkdown = useCallback((): string => { - const originForCopy = baseUrl || buildSiteUrl; - const llmsUrl = `${originForCopy}/llms.txt`; - - if (agentBodyAfterAbout !== undefined) { - return buildMarkdownWithAboutDevhubLeadIn( - aboutDevhubBody, - llmsUrl, - agentBodyAfterAbout, - ); - } - - const rawContent = resolveContent(); - const escapedTitle = title.replace(/"/g, '\\"'); - const escapedDescription = description.replace(/"/g, '\\"'); - - let md = ""; - if (!omitAboutDevhubPreamble) { - md += `${buildAboutDevhubForBrowserCopy(aboutDevhubBody, llmsUrl)}\n\n`; - } - md += `---\ntitle: "${escapedTitle}"\nurl: ${fullUrl}\nsummary: "${escapedDescription}"\n---\n\n`; - if (rawContent) md += `${rawContent}\n\n`; - if (additionalMarkdown) md += `${additionalMarkdown}\n\n`; - return md; - }, [ - agentBodyAfterAbout, - aboutDevhubBody, - omitAboutDevhubPreamble, - resolveContent, - additionalMarkdown, - title, - description, - fullUrl, - baseUrl, - buildSiteUrl, - ]); - - const handleCopyMarkdown = useCallback(() => { - if (rawMarkdownUrl && !rawMarkdown && !fetchedMarkdownRef.current) { - fetch(rawMarkdownUrl) - .then((res) => (res.ok ? res.text() : "")) - .then((text) => { - fetchedMarkdownRef.current = text; - const md = buildAIMarkdown(); - return navigator.clipboard.writeText(md); - }) - .then(() => toast.success("Markdown copied")) - .catch(() => toast.error("Failed to copy markdown")); - return; - } - const md = buildAIMarkdown(); - navigator.clipboard.writeText(md).then(() => { + const handleCopyMarkdown = useCallback(async () => { + try { + await ensureFetched(); + await navigator.clipboard.writeText(buildAIMarkdown()); toast.success("Markdown copied"); - }); - }, [rawMarkdown, rawMarkdownUrl, buildAIMarkdown]); + } catch { + toast.error("Failed to copy markdown"); + } + }, [ensureFetched, buildAIMarkdown]); const handleViewRawMarkdown = useCallback(() => { const mdUrl = fullUrl.replace(/\/$/, "") + ".md"; diff --git a/src/components/code/copy-button.tsx b/src/components/code/copy-button.tsx deleted file mode 100644 index 0faae30..0000000 --- a/src/components/code/copy-button.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { CheckIcon, CopyIcon } from "lucide-react"; -import { useState } from "react"; -import { cn } from "@/lib/utils"; - -type CopyButtonProps = { - text: string; - timeout?: number; - label?: string; - className?: string; - variant?: "default" | "plain" | "segment"; -}; - -export function CopyButton({ - text, - timeout = 2000, - label, - className, - variant = "default", -}: CopyButtonProps) { - const [isCopied, setIsCopied] = useState(false); - - const copyToClipboard = async () => { - if (typeof window === "undefined" || !navigator?.clipboard?.writeText) { - return; - } - - try { - await navigator.clipboard.writeText(text); - setIsCopied(true); - setTimeout(() => setIsCopied(false), timeout); - } catch { - // Silently fail - } - }; - - const Icon = isCopied ? CheckIcon : CopyIcon; - const ariaLabel = isCopied ? "Copied!" : (label ?? "Copy to clipboard"); - const variantClassName = - variant === "plain" - ? "size-6 bg-transparent p-0 shadow-none hover:bg-transparent" - : variant === "segment" - ? "h-full w-11 rounded-none rounded-r-full shadow-none" - : "size-6"; - - return ( - - ); -} diff --git a/src/components/cookbooks/recipe-list.tsx b/src/components/cookbooks/recipe-list.tsx deleted file mode 100644 index 4e6b5a8..0000000 --- a/src/components/cookbooks/recipe-list.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { useState, type ReactNode } from "react"; -import { ChevronRight, BookOpen } from "lucide-react"; -import type { Recipe } from "@/lib/recipes/recipes"; - -type RecipeListProps = { - recipes: Recipe[]; -}; - -export function RecipeList({ recipes }: RecipeListProps): ReactNode { - const [isOpen, setIsOpen] = useState(false); - - if (recipes.length <= 1) return null; - - return ( -
- - - {isOpen && ( -
-
    - {recipes.map((recipe, index) => ( -
  1. - - - {index + 1} - -
    -

    - {recipe.name} -

    -

    - {recipe.description} -

    -
    -
    -
  2. - ))} -
-
- )} -
- ); -} diff --git a/src/components/copy-prompt-button.tsx b/src/components/copy-prompt-button.tsx index 93cb807..4360d5f 100644 --- a/src/components/copy-prompt-button.tsx +++ b/src/components/copy-prompt-button.tsx @@ -1,6 +1,5 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { track } from "@vercel/analytics"; -import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import { toast } from "sonner"; import { Check, Clipboard, LoaderCircle } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -11,107 +10,41 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { - buildAboutDevhubForBrowserCopy, - buildMarkdownWithAboutDevhubLeadIn, - useAboutDevhubBody, -} from "@/lib/copy-about-devhub"; + useAgentMarkdown, + type AgentMarkdownInput, +} from "@/lib/use-agent-markdown"; -type CopyPromptButtonProps = { - rawMarkdown?: string; - rawMarkdownUrl?: string; - additionalMarkdown?: string; - agentBodyAfterAbout?: string; - title: string; - description: string; - permalink: string; +type CopyPromptButtonProps = AgentMarkdownInput & { disabled?: boolean; disabledTooltip?: string; }; export function CopyPromptButton({ - rawMarkdown, - rawMarkdownUrl, - additionalMarkdown, - agentBodyAfterAbout, - title, - description, - permalink, disabled = false, disabledTooltip = "select a template to copy", + ...input }: CopyPromptButtonProps) { - const { siteConfig } = useDocusaurusContext(); - const buildSiteUrl = siteConfig.url.replace(/\/$/, ""); - const baseUrl = - typeof window !== "undefined" ? window.location.origin : buildSiteUrl; - const fullUrl = baseUrl + permalink; - const aboutDevhubBody = useAboutDevhubBody(); - const fetchedMarkdownRef = useRef(null); + const { buildAIMarkdown, ensureFetched } = useAgentMarkdown(input); const [copyState, setCopyState] = useState< "idle" | "copying" | "copied" | "error" >("idle"); const resetTimerRef = useRef>(undefined); - useEffect(() => { - if (rawMarkdown || !rawMarkdownUrl) return; - fetch(rawMarkdownUrl) - .then((res) => (res.ok ? res.text() : null)) - .then((text) => { - fetchedMarkdownRef.current = text; - }) - .catch(() => {}); - }, [rawMarkdown, rawMarkdownUrl]); - - const resolveContent = useCallback((): string => { - if (rawMarkdown) return rawMarkdown; - if (fetchedMarkdownRef.current) return fetchedMarkdownRef.current; - return ""; - }, [rawMarkdown]); - - const buildAIMarkdown = useCallback((): string => { - const originForCopy = baseUrl || buildSiteUrl; - const llmsUrl = `${originForCopy}/llms.txt`; - - if (agentBodyAfterAbout !== undefined) { - return buildMarkdownWithAboutDevhubLeadIn( - aboutDevhubBody, - llmsUrl, - agentBodyAfterAbout, - ); - } - - const rawContent = resolveContent(); - const escapedTitle = title.replace(/"/g, '\\"'); - const escapedDescription = description.replace(/"/g, '\\"'); - - const about = buildAboutDevhubForBrowserCopy(aboutDevhubBody, llmsUrl); - let md = `${about}\n\n`; - md += `---\ntitle: "${escapedTitle}"\nurl: ${fullUrl}\nsummary: "${escapedDescription}"\n---\n\n`; - if (rawContent) md += `${rawContent}\n\n`; - if (additionalMarkdown) md += `${additionalMarkdown}\n\n`; - return md; - }, [ - agentBodyAfterAbout, - aboutDevhubBody, - resolveContent, - additionalMarkdown, - title, - description, - fullUrl, - baseUrl, - buildSiteUrl, - ]); + useEffect( + () => () => { + clearTimeout(resetTimerRef.current); + }, + [], + ); const handleCopy = useCallback(async () => { setCopyState("copying"); try { - if (rawMarkdownUrl && !rawMarkdown && !fetchedMarkdownRef.current) { - const res = await fetch(rawMarkdownUrl); - fetchedMarkdownRef.current = res.ok ? await res.text() : ""; - } + await ensureFetched(); const md = buildAIMarkdown(); await navigator.clipboard.writeText(md); setCopyState("copied"); - track("copy_prompt", { title, permalink }); + track("copy_prompt", { title: input.title, permalink: input.permalink }); toast.success("Prompt copied"); } catch { setCopyState("error"); @@ -120,7 +53,7 @@ export function CopyPromptButton({ clearTimeout(resetTimerRef.current); resetTimerRef.current = setTimeout(() => setCopyState("idle"), 2500); } - }, [rawMarkdown, rawMarkdownUrl, buildAIMarkdown]); + }, [ensureFetched, buildAIMarkdown, input.title, input.permalink]); if (disabled) { return ( diff --git a/src/components/docs/appkit-version-picker.tsx b/src/components/docs/appkit-version-picker.tsx deleted file mode 100644 index 77c72f0..0000000 --- a/src/components/docs/appkit-version-picker.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import Link from "@docusaurus/Link"; -import useIsBrowser from "@docusaurus/useIsBrowser"; -import { type ReactNode, useEffect, useMemo, useState } from "react"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Separator } from "@/components/ui/separator"; - -type AppKitChannel = { - id: string; - label: string; - docsPath: string; - apiPath: string; - note?: string; -}; - -type AppKitVersionPickerProps = { - channels: AppKitChannel[]; -}; - -function normalizePath(value: string): string { - return value.replace(/\/+$/, ""); -} - -function getActiveChannel( - channels: AppKitChannel[], - pathname: string | null, -): AppKitChannel | null { - if (!pathname) { - return channels[0] ?? null; - } - - const normalizedPathname = normalizePath(pathname); - return ( - channels.find((channel) => - normalizedPathname.startsWith(normalizePath(channel.docsPath)), - ) ?? - channels.find((channel) => - normalizedPathname.startsWith(normalizePath(channel.apiPath)), - ) ?? - channels[0] ?? - null - ); -} - -export function AppKitVersionPicker({ - channels, -}: AppKitVersionPickerProps): ReactNode { - const isBrowser = useIsBrowser(); - const activeChannel = useMemo( - () => - getActiveChannel(channels, isBrowser ? window.location.pathname : null), - [channels, isBrowser], - ); - const [selectedChannelId, setSelectedChannelId] = useState( - activeChannel?.id ?? channels[0]?.id ?? "", - ); - - useEffect(() => { - if (activeChannel) { - setSelectedChannelId(activeChannel.id); - } - }, [activeChannel]); - - if (channels.length === 0) { - return null; - } - - const selectedChannel = - channels.find((channel) => channel.id === selectedChannelId) ?? - activeChannel ?? - channels[0]; - - return ( - - -
- AppKit docs versions - Synced -
- - Pick a channel, then jump to the docs entry point or API reference. - -
- -
-

- Version channel -

- - {selectedChannel.note ? ( -

- {selectedChannel.note} -

- ) : null} -
- - - -
- - -
-
-
- ); -} diff --git a/src/components/home/bootstrap-copy-button.tsx b/src/components/home/bootstrap-copy-button.tsx new file mode 100644 index 0000000..8be16f9 --- /dev/null +++ b/src/components/home/bootstrap-copy-button.tsx @@ -0,0 +1,131 @@ +import type { ReactNode } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { Check, Clipboard, LoaderCircle } from "lucide-react"; +import { track } from "@vercel/analytics"; +import { Button } from "@/components/ui/button"; +import { getBootstrapPromptApiPath } from "@/lib/bootstrap-prompt"; + +function fallbackCopyTextToClipboard(text: string): boolean { + if (typeof window === "undefined" || typeof document === "undefined") { + return false; + } + + const textArea = document.createElement("textarea"); + textArea.value = text; + textArea.setAttribute("readonly", ""); + textArea.style.position = "fixed"; + textArea.style.top = "-9999px"; + textArea.style.left = "-9999px"; + document.body.append(textArea); + textArea.select(); + + try { + return document.execCommand("copy"); + } catch { + return false; + } finally { + textArea.remove(); + } +} + +async function copyTextToClipboard(text: string): Promise { + if (typeof window === "undefined") return false; + + if (navigator?.clipboard?.writeText) { + try { + await navigator.clipboard.writeText(text); + return true; + } catch { + // Fall through to legacy copy. + } + } + + return fallbackCopyTextToClipboard(text); +} + +async function fetchBootstrapPrompt(): Promise { + const response = await fetch(getBootstrapPromptApiPath()); + if (!response.ok) { + throw new Error(`Failed to fetch markdown: ${response.status}`); + } + + const bootstrapPrompt = await response.text(); + if (!bootstrapPrompt.trim()) { + throw new Error("Bootstrap prompt markdown is empty"); + } + + return bootstrapPrompt; +} + +type BootstrapCopyButtonProps = { + /** Analytics tag distinguishing call sites (e.g. "hero", "wizard"). */ + source: string; + className?: string; +}; + +/** + * Copy-the-bootstrap-prompt button used on the home page hero and inside the + * wizard step. Shares the same fetch → clipboard → animated state machine; the + * visual differences live entirely in `className`. + */ +export function BootstrapCopyButton({ + source, + className, +}: BootstrapCopyButtonProps): ReactNode { + const [copyState, setCopyState] = useState< + "idle" | "copying" | "copied" | "error" + >("idle"); + const resetTimerRef = useRef>(undefined); + + useEffect( + () => () => { + clearTimeout(resetTimerRef.current); + }, + [], + ); + + const handleCopy = useCallback(async () => { + setCopyState("copying"); + try { + const bootstrapPrompt = await fetchBootstrapPrompt(); + const copied = await copyTextToClipboard(bootstrapPrompt); + if (!copied) throw new Error("Clipboard copy failed"); + + setCopyState("copied"); + track("copy_bootstrap_prompt", { source }); + } catch { + setCopyState("error"); + } finally { + clearTimeout(resetTimerRef.current); + resetTimerRef.current = setTimeout(() => setCopyState("idle"), 2500); + } + }, [source]); + + return ( + + ); +} diff --git a/src/components/home/hero-section.tsx b/src/components/home/hero-section.tsx index 1d59835..037200f 100644 --- a/src/components/home/hero-section.tsx +++ b/src/components/home/hero-section.tsx @@ -1,91 +1,9 @@ import type { ReactNode } from "react"; -import { useCallback, useRef, useState } from "react"; -import { ArrowRight, Check, Clipboard, LoaderCircle } from "lucide-react"; +import { ArrowRight } from "lucide-react"; import Link from "@docusaurus/Link"; -import { track } from "@vercel/analytics"; -import { Button } from "@/components/ui/button"; -import { getBootstrapPromptApiPath } from "@/lib/bootstrap-prompt"; - -function fallbackCopyTextToClipboard(text: string): boolean { - if (typeof window === "undefined" || typeof document === "undefined") { - return false; - } - - const textArea = document.createElement("textarea"); - textArea.value = text; - textArea.setAttribute("readonly", ""); - textArea.style.position = "fixed"; - textArea.style.top = "-9999px"; - textArea.style.left = "-9999px"; - document.body.append(textArea); - textArea.select(); - - try { - return document.execCommand("copy"); - } catch { - return false; - } finally { - textArea.remove(); - } -} - -async function copyTextToClipboard(text: string): Promise { - if (typeof window === "undefined") { - return false; - } - - if (navigator?.clipboard?.writeText) { - try { - await navigator.clipboard.writeText(text); - return true; - } catch { - // Fall through to legacy copy. - } - } - - return fallbackCopyTextToClipboard(text); -} - -async function getBootstrapPrompt(): Promise { - const response = await fetch(getBootstrapPromptApiPath()); - if (!response.ok) { - throw new Error(`Failed to fetch markdown: ${response.status}`); - } - - const bootstrapPrompt = await response.text(); - if (!bootstrapPrompt.trim()) { - throw new Error("Bootstrap prompt markdown is empty"); - } - - return bootstrapPrompt; -} +import { BootstrapCopyButton } from "@/components/home/bootstrap-copy-button"; export function HeroSection(): ReactNode { - const [copyState, setCopyState] = useState< - "idle" | "copying" | "copied" | "error" - >("idle"); - const resetTimerRef = useRef>(undefined); - - const handleCopyBootstrapPrompt = useCallback(async () => { - setCopyState("copying"); - try { - const bootstrapPrompt = await getBootstrapPrompt(); - const copied = await copyTextToClipboard(bootstrapPrompt); - - if (!copied) { - throw new Error("Clipboard copy failed"); - } - - setCopyState("copied"); - track("copy_bootstrap_prompt", { source: "hero" }); - } catch { - setCopyState("error"); - } finally { - clearTimeout(resetTimerRef.current); - resetTimerRef.current = setTimeout(() => setCopyState("idle"), 2500); - } - }, []); - return (
@@ -100,31 +18,10 @@ export function HeroSection(): ReactNode { it will walk you through building a complete app, step by step.

- + />
- ); -} - -function LakebaseConcept(): ReactNode { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} - -function AgentBricksConcept(): ReactNode { - const cx = 200; - const cy = 150; - const hubR = 34; - const tools = [ - { dx: -120, dy: -70, shape: "square" as const }, - { dx: 120, dy: -70, shape: "circle" as const }, - { dx: -140, dy: 0, shape: "triangle" as const }, - { dx: 140, dy: 10, shape: "diamond" as const }, - { dx: -100, dy: 80, shape: "square" as const }, - { dx: 100, dy: 86, shape: "circle" as const }, - ]; - return ( - - - {tools.map((t, i) => ( - - ))} - - - - {tools.map((t, i) => { - const x = cx + t.dx; - const y = cy + t.dy; - const accent = i === 1; - const fill = accent ? "#ff3621" : "currentColor"; - if (t.shape === "circle") { - return ( - - ); - } - if (t.shape === "square") { - return ( - - ); - } - if (t.shape === "triangle") { - return ( - - ); - } - return ( - - ); - })} - - - - - - - - - - - ); -} - -function AppsConcept(): ReactNode { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - ); -} - -function PillarRow({ - pillar, - index, -}: { - pillar: Pillar; - index: number; -}): ReactNode { - const isReversed = index % 2 === 1; - const number = String(index + 1).padStart(2, "0"); - - return ( -
-
-

- {number} - - {pillar.title} - -

-

- {pillar.subtitle} -

-

- {pillar.description} -

- - Learn more - - - - -
-
- -
-
- ); -} - -export function PillarStrip(): ReactNode { - return ( -
-
-
-

- - The building blocks -

-

- The wizard wires these up{" "} - for you. -

-

- Three first-class primitives for operationalizing data on - Databricks. Same workspace, same identity, same governance — - composed by{" "} - - AppKit - - , our open-source TypeScript SDK. -

-
-
- {pillars.map((pillar, index) => ( - - ))} -
-
-
- ); -} diff --git a/src/components/home/wizard-flow.tsx b/src/components/home/wizard-flow.tsx index 7030a17..28e606c 100644 --- a/src/components/home/wizard-flow.tsx +++ b/src/components/home/wizard-flow.tsx @@ -1,9 +1,5 @@ import type { ReactNode } from "react"; -import { useCallback, useRef, useState } from "react"; -import { Check, Clipboard, LoaderCircle } from "lucide-react"; -import { track } from "@vercel/analytics"; -import { Button } from "@/components/ui/button"; -import { getBootstrapPromptApiPath } from "@/lib/bootstrap-prompt"; +import { BootstrapCopyButton } from "@/components/home/bootstrap-copy-button"; type WizardStep = { number: string; @@ -561,72 +557,6 @@ function ShipVisual(): ReactNode { ); } -function CopyPromptButton(): ReactNode { - const [copyState, setCopyState] = useState< - "idle" | "copying" | "copied" | "error" - >("idle"); - const resetTimerRef = useRef>(undefined); - - const handleCopy = useCallback(async () => { - setCopyState("copying"); - try { - const response = await fetch(getBootstrapPromptApiPath()); - if (!response.ok) throw new Error(`Failed to fetch: ${response.status}`); - const text = await response.text(); - if (!text.trim()) throw new Error("Empty prompt"); - - if (navigator?.clipboard?.writeText) { - await navigator.clipboard.writeText(text); - } else { - const textArea = document.createElement("textarea"); - textArea.value = text; - textArea.setAttribute("readonly", ""); - textArea.style.position = "fixed"; - textArea.style.top = "-9999px"; - document.body.append(textArea); - textArea.select(); - document.execCommand("copy"); - textArea.remove(); - } - - setCopyState("copied"); - track("copy_bootstrap_prompt", { source: "wizard" }); - } catch { - setCopyState("error"); - } finally { - clearTimeout(resetTimerRef.current); - resetTimerRef.current = setTimeout(() => setCopyState("idle"), 2500); - } - }, []); - - return ( - - ); -} - const steps: WizardStep[] = [ { number: "01", @@ -635,7 +565,12 @@ const steps: WizardStep[] = [ description: "One click copies everything your coding agent needs to build and deploy on Databricks.", visual: , - action: , + action: ( + + ), }, { number: "02", diff --git a/src/components/templates/selected-items.tsx b/src/components/templates/selected-items.tsx deleted file mode 100644 index 204b016..0000000 --- a/src/components/templates/selected-items.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { XIcon } from "lucide-react"; -import { Badge } from "@/components/ui/badge"; -import type { TemplateItem } from "./template-card"; - -export function SelectedItems({ - items, - onRemove, -}: { - items: TemplateItem[]; - onRemove: (id: string) => void; -}) { - if (items.length === 0) return null; - - return ( -
- - {items.length} selected: - - {items.map((item) => ( - - ))} -
- ); -} diff --git a/src/lib/bootstrap-prompt.ts b/src/lib/bootstrap-prompt.ts index 063d83c..915d5f7 100644 --- a/src/lib/bootstrap-prompt.ts +++ b/src/lib/bootstrap-prompt.ts @@ -5,7 +5,3 @@ export const ABOUT_DEVHUB_SLUG = "about-devhub" as const; export function getBootstrapPromptApiPath(): string { return `/api/bootstrap-prompt`; } - -export function getRecipeMarkdownApiPath(): string { - return `/api/markdown?section=${BOOTSTRAP_PROMPT_SECTION}&slug=${BOOTSTRAP_PROMPT_SLUG}`; -} diff --git a/src/lib/content-markdown.ts b/src/lib/content-markdown.ts index b2677e2..cb12f2f 100644 --- a/src/lib/content-markdown.ts +++ b/src/lib/content-markdown.ts @@ -6,15 +6,8 @@ import { type ContentSections, } from "./content-sections"; -export { - CONTENT_SECTION_FILES, - REQUIRED_CONTENT_SECTION_FILE, - joinContentSections, -} from "./content-sections"; -export type { ContentSectionFile, ContentSections } from "./content-sections"; - -export type ContentMarkdownSection = "recipes" | "solutions" | "examples"; -export type FolderContentSection = "recipes" | "examples"; +type ContentMarkdownSection = "recipes" | "solutions" | "examples"; +type FolderContentSection = "recipes" | "examples"; function markdownDirectory( rootDir: string, @@ -62,7 +55,7 @@ export function hasContentSlug( } /** Read a single section file for a slug; returns undefined when an optional file is absent. */ -export function readContentSection( +function readContentSection( rootDir: string, section: FolderContentSection, slug: string, @@ -77,10 +70,6 @@ export function readContentSection( return readFileSync(filePath, "utf-8"); } -/** Cookbooks only accept an optional intro.md today; folders with other files are rejected by the validator. */ -export const COOKBOOK_FILES = ["intro"] as const; -export type CookbookFile = (typeof COOKBOOK_FILES)[number]; - function cookbookDirectory(rootDir: string): string { return resolve(rootDir, "content", "cookbooks"); } diff --git a/src/lib/content-sections.ts b/src/lib/content-sections.ts index 2414776..0269335 100644 --- a/src/lib/content-sections.ts +++ b/src/lib/content-sections.ts @@ -1,5 +1,5 @@ /** Allowed file names inside each content/// folder. */ -export const CONTENT_SECTION_FILES = [ +const CONTENT_SECTION_FILES = [ "content", "prerequisites", "deployment", diff --git a/src/lib/cookbook-composition.ts b/src/lib/cookbook-composition.ts index 0143d8b..04bcbbd 100644 --- a/src/lib/cookbook-composition.ts +++ b/src/lib/cookbook-composition.ts @@ -6,7 +6,7 @@ export type CookbookRecipeInput = { sections: ContentSections; }; -export type CookbookCompositionInput = { +type CookbookCompositionInput = { cookbookName: string; cookbookDescription: string; intro?: string; diff --git a/src/lib/examples/build-example-markdown.ts b/src/lib/examples/build-example-markdown.ts index 13759b7..4345536 100644 --- a/src/lib/examples/build-example-markdown.ts +++ b/src/lib/examples/build-example-markdown.ts @@ -39,6 +39,19 @@ export function buildIncludedTemplatesPreamble(): string { ].join("\n"); } +function buildIncludedTemplateLinks( + includedCookbooks: ContentRef[], + includedRecipes: ContentRef[], + baseUrl: string, +): string[] { + const renderLink = (item: ContentRef) => + `- [${item.name}](${baseUrl}/templates/${item.id}.md) - ${item.description}`; + return [ + ...includedCookbooks.map(renderLink), + ...includedRecipes.map(renderLink), + ]; +} + /** Get started body for Copy as Markdown exports (includes init or clone command + README pointer). */ export function buildExportGetStartedSection(example: Example): string { if (isInitCommand(example.initCommand)) { @@ -168,16 +181,11 @@ export function buildFullPrompt( lines.push("", `## Source Code`, "", `GitHub: ${githubUrl}`); - const includedTemplateLinks = [ - ...includedCookbooks.map( - (c) => - `- [${c.name}](${baseUrl}/templates/${c.id}.md) - ${c.description}`, - ), - ...includedRecipes.map( - (r) => - `- [${r.name}](${baseUrl}/templates/${r.id}.md) - ${r.description}`, - ), - ]; + const includedTemplateLinks = buildIncludedTemplateLinks( + includedCookbooks, + includedRecipes, + baseUrl, + ); if (includedTemplateLinks.length > 0) { lines.push( "", @@ -200,16 +208,11 @@ export function buildAdditionalMarkdown(opts: ExampleMarkdownOptions): string { sections.push(buildExportGetStartedSection(example)); sections.push(`## Source Code\n\nGitHub: ${githubUrl}`); - const links = [ - ...includedCookbooks.map( - (c) => - `- [${c.name}](${baseUrl}/templates/${c.id}.md) - ${c.description}`, - ), - ...includedRecipes.map( - (r) => - `- [${r.name}](${baseUrl}/templates/${r.id}.md) - ${r.description}`, - ), - ]; + const links = buildIncludedTemplateLinks( + includedCookbooks, + includedRecipes, + baseUrl, + ); if (links.length > 0) { sections.push( "## Included templates", diff --git a/src/lib/landing-content.ts b/src/lib/landing-content.ts index faf1a1d..ff4a807 100644 --- a/src/lib/landing-content.ts +++ b/src/lib/landing-content.ts @@ -1,5 +1,3 @@ -import { Bot, Database, Server } from "lucide-react"; -import type { ComponentType } from "react"; import { examples, filterPublished, @@ -8,14 +6,6 @@ import { type Service, } from "@/lib/recipes/recipes"; -export type Pillar = { - title: string; - subtitle: string; - description: string; - link: string; - icon?: ComponentType<{ className?: string }>; -}; - export type LandingTemplateItem = { id: string; path: string; @@ -28,33 +18,6 @@ export type LandingTemplateItem = { previewImageDarkUrl?: string; }; -export const pillars: Pillar[] = [ - { - title: "Lakebase", - subtitle: "Managed Postgres, colocated with your Lakehouse.", - description: - "Provision with the CLI, connect like any Postgres. Instant branching, scales to zero, and change data feed to Unity Catalog.", - link: "/docs/lakebase/overview", - icon: Database, - }, - { - title: "Agent Bricks", - subtitle: "Managed AI agents and governed LLM endpoints.", - description: - "Connect Knowledge Assistants, Genie spaces, and foundation models to your AppKit app. AI Gateway handles rate limits, cost attribution, and content safety.", - link: "/docs/agents/overview", - icon: Bot, - }, - { - title: "Databricks Apps", - subtitle: "Web apps that run inside your workspace.", - description: - "One CLI command to deploy. Fixed URL, built-in OAuth, and direct access to your workspace data \u2014 no separate hosting service.", - link: "/docs/apps/overview", - icon: Server, - }, -]; - export function buildLandingTemplates( includeDrafts: boolean, includeExamples: boolean, diff --git a/src/lib/recipes/recipes.ts b/src/lib/recipes/recipes.ts index 9fd2ae1..023bd06 100644 --- a/src/lib/recipes/recipes.ts +++ b/src/lib/recipes/recipes.ts @@ -23,7 +23,7 @@ export type Service = (typeof SERVICES)[number]; * - PNG / JPG / WEBP (rasters). SVGs are not valid preview images. * - Provide both light and dark variants (or neither, to fall back). */ -export type PreviewImages = { +type PreviewImages = { previewImageLightUrl?: string; previewImageDarkUrl?: string; }; diff --git a/src/lib/solutions/solutions.ts b/src/lib/solutions/solutions.ts index f54517e..f3b33c5 100644 --- a/src/lib/solutions/solutions.ts +++ b/src/lib/solutions/solutions.ts @@ -1,10 +1,8 @@ -export type SolutionTag = string; - type SolutionBase = { id: string; title: string; description: string; - tags: SolutionTag[]; + tags: string[]; }; /** diff --git a/src/lib/use-agent-markdown.ts b/src/lib/use-agent-markdown.ts new file mode 100644 index 0000000..9d1490f --- /dev/null +++ b/src/lib/use-agent-markdown.ts @@ -0,0 +1,115 @@ +import { useCallback, useEffect, useRef } from "react"; +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; +import { + buildAboutDevhubForBrowserCopy, + buildMarkdownWithAboutDevhubLeadIn, + useAboutDevhubBody, +} from "@/lib/copy-about-devhub"; + +export type AgentMarkdownInput = { + rawMarkdown?: string; + rawMarkdownUrl?: string; + additionalMarkdown?: string; + /** + * When set, the result is `about-devhub + --- + this string`, + * ignoring frontmatter and rawMarkdown/additionalMarkdown. + */ + agentBodyAfterAbout?: string; + /** + * When true, the About DevHub preamble is omitted (used for reference doc + * pages where the preamble is already part of the linking resource). + */ + omitAboutDevhubPreamble?: boolean; + title: string; + description: string; + permalink: string; +}; + +type UseAgentMarkdownResult = { + /** Origin (browser) or build-time site URL (SSR). */ + baseUrl: string; + /** Page URL with origin. */ + fullUrl: string; + /** Build the final agent-ready markdown string. Safe to call after fetch resolves. */ + buildAIMarkdown: () => string; + /** Ensure rawMarkdownUrl is fetched before reading; resolves once content is available. */ + ensureFetched: () => Promise; +}; + +export function useAgentMarkdown( + input: AgentMarkdownInput, +): UseAgentMarkdownResult { + const { + rawMarkdown, + rawMarkdownUrl, + additionalMarkdown, + agentBodyAfterAbout, + omitAboutDevhubPreamble = false, + title, + description, + permalink, + } = input; + + const { siteConfig } = useDocusaurusContext(); + const buildSiteUrl = siteConfig.url.replace(/\/$/, ""); + const baseUrl = + typeof window !== "undefined" ? window.location.origin : buildSiteUrl; + const fullUrl = baseUrl + permalink; + const aboutDevhubBody = useAboutDevhubBody(); + const fetchedMarkdownRef = useRef(null); + + useEffect(() => { + if (rawMarkdown || !rawMarkdownUrl) return; + fetch(rawMarkdownUrl) + .then((res) => (res.ok ? res.text() : null)) + .then((text) => { + fetchedMarkdownRef.current = text; + }) + .catch(() => {}); + }, [rawMarkdown, rawMarkdownUrl]); + + const ensureFetched = useCallback(async (): Promise => { + if (rawMarkdown || !rawMarkdownUrl || fetchedMarkdownRef.current) return; + const res = await fetch(rawMarkdownUrl); + fetchedMarkdownRef.current = res.ok ? await res.text() : ""; + }, [rawMarkdown, rawMarkdownUrl]); + + const buildAIMarkdown = useCallback((): string => { + const originForCopy = baseUrl || buildSiteUrl; + const llmsUrl = `${originForCopy}/llms.txt`; + + if (agentBodyAfterAbout !== undefined) { + return buildMarkdownWithAboutDevhubLeadIn( + aboutDevhubBody, + llmsUrl, + agentBodyAfterAbout, + ); + } + + const rawContent = rawMarkdown ?? fetchedMarkdownRef.current ?? ""; + const escapedTitle = title.replace(/"/g, '\\"'); + const escapedDescription = description.replace(/"/g, '\\"'); + + let md = ""; + if (!omitAboutDevhubPreamble) { + md += `${buildAboutDevhubForBrowserCopy(aboutDevhubBody, llmsUrl)}\n\n`; + } + md += `---\ntitle: "${escapedTitle}"\nurl: ${fullUrl}\nsummary: "${escapedDescription}"\n---\n\n`; + if (rawContent) md += `${rawContent}\n\n`; + if (additionalMarkdown) md += `${additionalMarkdown}\n\n`; + return md; + }, [ + rawMarkdown, + additionalMarkdown, + agentBodyAfterAbout, + aboutDevhubBody, + omitAboutDevhubPreamble, + title, + description, + fullUrl, + baseUrl, + buildSiteUrl, + ]); + + return { baseUrl, fullUrl, buildAIMarkdown, ensureFetched }; +} diff --git a/src/lib/use-cookbook-markdown.ts b/src/lib/use-cookbook-markdown.ts new file mode 100644 index 0000000..b836541 --- /dev/null +++ b/src/lib/use-cookbook-markdown.ts @@ -0,0 +1,45 @@ +import { cookbooks, recipes, type Cookbook } from "@/lib/recipes/recipes"; +import { + useAllRecipeSections, + useCookbookIntro, +} from "@/lib/use-raw-content-markdown"; +import { composeCookbookMarkdown } from "@/lib/cookbook-composition"; + +type UseCookbookMarkdownResult = { + cookbook: Cookbook; + rawMarkdown: string; +}; + +/** + * Resolves a cookbook by id and assembles its agent-ready markdown by joining + * each child recipe's sections via `composeCookbookMarkdown`. Throws on + * missing cookbook, recipe, or recipe sections so config typos surface at + * page render time rather than producing silently empty exports. + */ +export function useCookbookMarkdown( + cookbookId: string, +): UseCookbookMarkdownResult { + const cookbook = cookbooks.find((c) => c.id === cookbookId); + if (!cookbook) throw new Error(`Cookbook ${cookbookId} not found`); + + const sectionsBySlug = useAllRecipeSections(); + const intro = useCookbookIntro(cookbookId); + + const recipeInputs = cookbook.recipeIds.map((id) => { + const recipe = recipes.find((r) => r.id === id); + const sections = sectionsBySlug[id]; + if (!recipe || !sections) { + throw new Error(`Missing recipe or sections for "${id}"`); + } + return { id, name: recipe.name, sections }; + }); + + const rawMarkdown = composeCookbookMarkdown({ + cookbookName: cookbook.name, + cookbookDescription: cookbook.description, + intro, + recipes: recipeInputs, + }); + + return { cookbook, rawMarkdown }; +} diff --git a/src/lib/use-raw-content-markdown.ts b/src/lib/use-raw-content-markdown.ts index 1a14f45..cf72f7d 100644 --- a/src/lib/use-raw-content-markdown.ts +++ b/src/lib/use-raw-content-markdown.ts @@ -17,22 +17,6 @@ export function useRawRecipeMarkdown(slug: string): string | undefined { return data.rawMarkdownBySlug[slug]; } -export function useAllRawRecipeMarkdown(): Record { - const data = usePluginData( - "docusaurus-plugin-content-entries", - "recipes", - ) as ContentEntriesGlobalData; - return data.rawMarkdownBySlug; -} - -export function useRecipeSections(slug: string): ContentSections | undefined { - const data = usePluginData( - "docusaurus-plugin-content-entries", - "recipes", - ) as ContentEntriesGlobalData; - return data.sectionsBySlug[slug]; -} - export function useAllRecipeSections(): Record { const data = usePluginData( "docusaurus-plugin-content-entries", @@ -60,14 +44,6 @@ export function useCookbookIntro(slug: string): string | undefined { return data.introsBySlug[slug]; } -export function useRawExampleMarkdown(slug: string): string { - const data = usePluginData( - "docusaurus-plugin-content-entries", - "examples", - ) as ContentEntriesGlobalData; - return data.rawMarkdownBySlug[slug] ?? ""; -} - export function useExampleSections(slug: string): ContentSections | undefined { const data = usePluginData( "docusaurus-plugin-content-entries", diff --git a/src/pages/templates/ai-chat-app.tsx b/src/pages/templates/ai-chat-app.tsx index 9c7142c..cbbdfc8 100644 --- a/src/pages/templates/ai-chat-app.tsx +++ b/src/pages/templates/ai-chat-app.tsx @@ -1,11 +1,6 @@ import type { ReactNode } from "react"; import { CookbookDetail } from "@/components/cookbooks/cookbook-detail"; -import { cookbooks, recipes } from "@/lib/recipes/recipes"; -import { - useAllRecipeSections, - useCookbookIntro, -} from "@/lib/use-raw-content-markdown"; -import { composeCookbookMarkdown } from "@/lib/cookbook-composition"; +import { useCookbookMarkdown } from "@/lib/use-cookbook-markdown"; import Intro from "@site/content/cookbooks/ai-chat-app/intro.md"; import BootstrapPrereqs from "@site/content/recipes/databricks-local-bootstrap/prerequisites.md"; import BootstrapContent from "@site/content/recipes/databricks-local-bootstrap/content.md"; @@ -20,30 +15,8 @@ import LakebaseDataPersistenceContent from "@site/content/recipes/lakebase-data- import LakebaseChatPersistencePrereqs from "@site/content/recipes/lakebase-chat-persistence/prerequisites.md"; import LakebaseChatPersistenceContent from "@site/content/recipes/lakebase-chat-persistence/content.md"; -const COOKBOOK_ID = "ai-chat-app"; - export default function AiChatAppPage(): ReactNode { - const cookbook = cookbooks.find((t) => t.id === COOKBOOK_ID); - if (!cookbook) throw new Error(`Cookbook ${COOKBOOK_ID} not found`); - - const sectionsBySlug = useAllRecipeSections(); - const intro = useCookbookIntro(COOKBOOK_ID); - - const recipeInputs = cookbook.recipeIds.map((id) => { - const recipe = recipes.find((r) => r.id === id); - const sections = sectionsBySlug[id]; - if (!recipe || !sections) { - throw new Error(`Missing recipe or sections for "${id}"`); - } - return { id, name: recipe.name, sections }; - }); - - const rawMarkdown = composeCookbookMarkdown({ - cookbookName: cookbook.name, - cookbookDescription: cookbook.description, - intro, - recipes: recipeInputs, - }); + const { cookbook, rawMarkdown } = useCookbookMarkdown("ai-chat-app"); return ( diff --git a/src/pages/templates/app-with-lakebase.tsx b/src/pages/templates/app-with-lakebase.tsx index c26fe17..9f702df 100644 --- a/src/pages/templates/app-with-lakebase.tsx +++ b/src/pages/templates/app-with-lakebase.tsx @@ -1,11 +1,6 @@ import type { ReactNode } from "react"; import { CookbookDetail } from "@/components/cookbooks/cookbook-detail"; -import { cookbooks, recipes } from "@/lib/recipes/recipes"; -import { - useAllRecipeSections, - useCookbookIntro, -} from "@/lib/use-raw-content-markdown"; -import { composeCookbookMarkdown } from "@/lib/cookbook-composition"; +import { useCookbookMarkdown } from "@/lib/use-cookbook-markdown"; import BootstrapPrereqs from "@site/content/recipes/databricks-local-bootstrap/prerequisites.md"; import BootstrapContent from "@site/content/recipes/databricks-local-bootstrap/content.md"; import LakebaseCreateInstancePrereqs from "@site/content/recipes/lakebase-create-instance/prerequisites.md"; @@ -13,30 +8,8 @@ import LakebaseCreateInstanceContent from "@site/content/recipes/lakebase-create import LakebaseDataPersistencePrereqs from "@site/content/recipes/lakebase-data-persistence/prerequisites.md"; import LakebaseDataPersistenceContent from "@site/content/recipes/lakebase-data-persistence/content.md"; -const COOKBOOK_ID = "app-with-lakebase"; - export default function AppWithLakebasePage(): ReactNode { - const cookbook = cookbooks.find((t) => t.id === COOKBOOK_ID); - if (!cookbook) throw new Error(`Cookbook ${COOKBOOK_ID} not found`); - - const sectionsBySlug = useAllRecipeSections(); - const intro = useCookbookIntro(COOKBOOK_ID); - - const recipeInputs = cookbook.recipeIds.map((id) => { - const recipe = recipes.find((r) => r.id === id); - const sections = sectionsBySlug[id]; - if (!recipe || !sections) { - throw new Error(`Missing recipe or sections for "${id}"`); - } - return { id, name: recipe.name, sections }; - }); - - const rawMarkdown = composeCookbookMarkdown({ - cookbookName: cookbook.name, - cookbookDescription: cookbook.description, - intro, - recipes: recipeInputs, - }); + const { cookbook, rawMarkdown } = useCookbookMarkdown("app-with-lakebase"); return ( diff --git a/src/pages/templates/genie-analytics-app.tsx b/src/pages/templates/genie-analytics-app.tsx index dfa6753..60ffdf3 100644 --- a/src/pages/templates/genie-analytics-app.tsx +++ b/src/pages/templates/genie-analytics-app.tsx @@ -1,40 +1,13 @@ import type { ReactNode } from "react"; import { CookbookDetail } from "@/components/cookbooks/cookbook-detail"; -import { cookbooks, recipes } from "@/lib/recipes/recipes"; -import { - useAllRecipeSections, - useCookbookIntro, -} from "@/lib/use-raw-content-markdown"; -import { composeCookbookMarkdown } from "@/lib/cookbook-composition"; +import { useCookbookMarkdown } from "@/lib/use-cookbook-markdown"; import BootstrapPrereqs from "@site/content/recipes/databricks-local-bootstrap/prerequisites.md"; import BootstrapContent from "@site/content/recipes/databricks-local-bootstrap/content.md"; import GenieConversationalAnalyticsPrereqs from "@site/content/recipes/genie-conversational-analytics/prerequisites.md"; import GenieConversationalAnalyticsContent from "@site/content/recipes/genie-conversational-analytics/content.md"; -const COOKBOOK_ID = "genie-analytics-app"; - export default function GenieAnalyticsAppPage(): ReactNode { - const cookbook = cookbooks.find((t) => t.id === COOKBOOK_ID); - if (!cookbook) throw new Error(`Cookbook ${COOKBOOK_ID} not found`); - - const sectionsBySlug = useAllRecipeSections(); - const intro = useCookbookIntro(COOKBOOK_ID); - - const recipeInputs = cookbook.recipeIds.map((id) => { - const recipe = recipes.find((r) => r.id === id); - const sections = sectionsBySlug[id]; - if (!recipe || !sections) { - throw new Error(`Missing recipe or sections for "${id}"`); - } - return { id, name: recipe.name, sections }; - }); - - const rawMarkdown = composeCookbookMarkdown({ - cookbookName: cookbook.name, - cookbookDescription: cookbook.description, - intro, - recipes: recipeInputs, - }); + const { cookbook, rawMarkdown } = useCookbookMarkdown("genie-analytics-app"); return ( diff --git a/src/pages/templates/hello-world-app.tsx b/src/pages/templates/hello-world-app.tsx index 6240850..7481444 100644 --- a/src/pages/templates/hello-world-app.tsx +++ b/src/pages/templates/hello-world-app.tsx @@ -1,38 +1,11 @@ import type { ReactNode } from "react"; import { CookbookDetail } from "@/components/cookbooks/cookbook-detail"; -import { cookbooks, recipes } from "@/lib/recipes/recipes"; -import { - useAllRecipeSections, - useCookbookIntro, -} from "@/lib/use-raw-content-markdown"; -import { composeCookbookMarkdown } from "@/lib/cookbook-composition"; +import { useCookbookMarkdown } from "@/lib/use-cookbook-markdown"; import BootstrapPrereqs from "@site/content/recipes/databricks-local-bootstrap/prerequisites.md"; import BootstrapContent from "@site/content/recipes/databricks-local-bootstrap/content.md"; -const COOKBOOK_ID = "hello-world-app"; - export default function HelloWorldAppPage(): ReactNode { - const cookbook = cookbooks.find((t) => t.id === COOKBOOK_ID); - if (!cookbook) throw new Error(`Cookbook ${COOKBOOK_ID} not found`); - - const sectionsBySlug = useAllRecipeSections(); - const intro = useCookbookIntro(COOKBOOK_ID); - - const recipeInputs = cookbook.recipeIds.map((id) => { - const recipe = recipes.find((r) => r.id === id); - const sections = sectionsBySlug[id]; - if (!recipe || !sections) { - throw new Error(`Missing recipe or sections for "${id}"`); - } - return { id, name: recipe.name, sections }; - }); - - const rawMarkdown = composeCookbookMarkdown({ - cookbookName: cookbook.name, - cookbookDescription: cookbook.description, - intro, - recipes: recipeInputs, - }); + const { cookbook, rawMarkdown } = useCookbookMarkdown("hello-world-app"); return ( diff --git a/src/pages/templates/lakebase-off-platform.tsx b/src/pages/templates/lakebase-off-platform.tsx index b78cc2d..f5a4271 100644 --- a/src/pages/templates/lakebase-off-platform.tsx +++ b/src/pages/templates/lakebase-off-platform.tsx @@ -1,11 +1,6 @@ import type { ReactNode } from "react"; import { CookbookDetail } from "@/components/cookbooks/cookbook-detail"; -import { cookbooks, recipes } from "@/lib/recipes/recipes"; -import { - useAllRecipeSections, - useCookbookIntro, -} from "@/lib/use-raw-content-markdown"; -import { composeCookbookMarkdown } from "@/lib/cookbook-composition"; +import { useCookbookMarkdown } from "@/lib/use-cookbook-markdown"; import LakebaseCreateInstancePrereqs from "@site/content/recipes/lakebase-create-instance/prerequisites.md"; import LakebaseCreateInstanceContent from "@site/content/recipes/lakebase-create-instance/content.md"; import LakebaseOffPlatformEnvManagementPrereqs from "@site/content/recipes/lakebase-off-platform-env-management/prerequisites.md"; @@ -15,30 +10,10 @@ import LakebaseTokenManagementContent from "@site/content/recipes/lakebase-token import LakebaseDrizzleOffPlatformPrereqs from "@site/content/recipes/lakebase-drizzle-off-platform/prerequisites.md"; import LakebaseDrizzleOffPlatformContent from "@site/content/recipes/lakebase-drizzle-off-platform/content.md"; -const COOKBOOK_ID = "lakebase-off-platform"; - export default function LakebaseOffPlatformPage(): ReactNode { - const cookbook = cookbooks.find((t) => t.id === COOKBOOK_ID); - if (!cookbook) throw new Error(`Cookbook ${COOKBOOK_ID} not found`); - - const sectionsBySlug = useAllRecipeSections(); - const intro = useCookbookIntro(COOKBOOK_ID); - - const recipeInputs = cookbook.recipeIds.map((id) => { - const recipe = recipes.find((r) => r.id === id); - const sections = sectionsBySlug[id]; - if (!recipe || !sections) { - throw new Error(`Missing recipe or sections for "${id}"`); - } - return { id, name: recipe.name, sections }; - }); - - const rawMarkdown = composeCookbookMarkdown({ - cookbookName: cookbook.name, - cookbookDescription: cookbook.description, - intro, - recipes: recipeInputs, - }); + const { cookbook, rawMarkdown } = useCookbookMarkdown( + "lakebase-off-platform", + ); return ( diff --git a/src/pages/templates/operational-data-analytics.tsx b/src/pages/templates/operational-data-analytics.tsx index 595abed..24de2b7 100644 --- a/src/pages/templates/operational-data-analytics.tsx +++ b/src/pages/templates/operational-data-analytics.tsx @@ -1,11 +1,6 @@ import type { ReactNode } from "react"; import { CookbookDetail } from "@/components/cookbooks/cookbook-detail"; -import { cookbooks, recipes } from "@/lib/recipes/recipes"; -import { - useAllRecipeSections, - useCookbookIntro, -} from "@/lib/use-raw-content-markdown"; -import { composeCookbookMarkdown } from "@/lib/cookbook-composition"; +import { useCookbookMarkdown } from "@/lib/use-cookbook-markdown"; import BootstrapPrereqs from "@site/content/recipes/databricks-local-bootstrap/prerequisites.md"; import BootstrapContent from "@site/content/recipes/databricks-local-bootstrap/content.md"; import UnityCatalogSetupPrereqs from "@site/content/recipes/unity-catalog-setup/prerequisites.md"; @@ -19,30 +14,10 @@ import SyncTablesAutoscalingContent from "@site/content/recipes/sync-tables-auto import MedallionArchitectureFromCdcPrereqs from "@site/content/recipes/medallion-architecture-from-cdc/prerequisites.md"; import MedallionArchitectureFromCdcContent from "@site/content/recipes/medallion-architecture-from-cdc/content.md"; -const COOKBOOK_ID = "operational-data-analytics"; - export default function OperationalDataAnalyticsPage(): ReactNode { - const cookbook = cookbooks.find((t) => t.id === COOKBOOK_ID); - if (!cookbook) throw new Error(`Cookbook ${COOKBOOK_ID} not found`); - - const sectionsBySlug = useAllRecipeSections(); - const intro = useCookbookIntro(COOKBOOK_ID); - - const recipeInputs = cookbook.recipeIds.map((id) => { - const recipe = recipes.find((r) => r.id === id); - const sections = sectionsBySlug[id]; - if (!recipe || !sections) { - throw new Error(`Missing recipe or sections for "${id}"`); - } - return { id, name: recipe.name, sections }; - }); - - const rawMarkdown = composeCookbookMarkdown({ - cookbookName: cookbook.name, - cookbookDescription: cookbook.description, - intro, - recipes: recipeInputs, - }); + const { cookbook, rawMarkdown } = useCookbookMarkdown( + "operational-data-analytics", + ); return ( diff --git a/src/theme/Logo/index.tsx b/src/theme/Logo/index.tsx index c65e6ac..d90041c 100644 --- a/src/theme/Logo/index.tsx +++ b/src/theme/Logo/index.tsx @@ -42,7 +42,7 @@ function LogoThemedImage({ return imageClassName ?
{image}
: image; } -export function Logo(props: Props): ReactNode { +function Logo(props: Props): ReactNode { const { siteConfig: { title }, } = useDocusaurusContext(); diff --git a/src/theme/TOC/index.tsx b/src/theme/TOC/index.tsx index 0cb6f68..5ef2dfa 100644 --- a/src/theme/TOC/index.tsx +++ b/src/theme/TOC/index.tsx @@ -8,7 +8,7 @@ const LINK_CLASS_NAME = const LINK_ACTIVE_CLASS_NAME = "toc-link-active"; -export function TOC({ className, ...props }: Props): ReactNode { +function TOC({ className, ...props }: Props): ReactNode { return (