From 3a7e40185040c0608597cfa507c02a7af7d8cbb4 Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Wed, 7 Oct 2026 23:40:06 +0100 Subject: [PATCH 1/2] feat: Astro 7 SSR Cloudflare migration, Sanity semantic search, SearchModal & syndication (#685) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: stronger black bg enforcement in Gemini image prompts V5b showed 2/4 infographic frames with gray/off-white backgrounds despite the color rule saying 'Use ONLY black'. Restructured as numbered rules with explicit rejection language. Background rule is now #1 priority. * temp * refactor: consolidate monorepo with Next.js in apps/web and Sanity in apps/sanity Replace the Astro apps/web on dev with main's Next.js app, strip the embedded Studio route, wire typegen to apps/sanity's schema extract, and trim CI to Sanity Studio deploys only (Vercel handles apps/web). Co-authored-by: Cursor * fix: update route type imports and adjust draft mode client configuration - Changed the import path for route types in next-env.d.ts to point to the development directory. - Modified the client configuration in route.tsx to disable CDN usage for draft mode, ensuring accurate content retrieval during editing. * feat: update Sanity dependencies and enhance draft mode functionality - Upgraded Sanity-related packages to their latest versions for improved performance and features. - Implemented dynamic fetching options in various components to support draft mode, enhancing content visibility during editing. - Adjusted the sitemap and page components to utilize the new fetching methods, ensuring accurate data retrieval based on the selected perspective. - Introduced caching mechanisms for improved efficiency in data fetching. * refactor: replace deprecated sanity-typegen.json with inline typegen configuration - Removed the obsolete sanity-typegen.json file. - Integrated typegen configuration directly into the Sanity CLI setup, ensuring schema extraction and type generation are streamlined within the cliConfig. * chore: update dependencies for React and Next.js - Upgraded React and React DOM to version 19.2.6 for improved performance and compatibility. - Updated Next.js to version 16.2.6 to leverage the latest features and enhancements. * chore: update package dependencies and enhance sitemap functionality - Upgraded pnpm to version 10.34.1 for improved package management. - Added new skills to skills-lock.json for better content modeling and SEO practices. - Introduced schema-dts version 2.0.0 to enhance TypeScript support. - Updated sitemap generation to utilize SITE_URL for dynamic URL handling across various components. - Enhanced metadata generation for authors, guests, podcasts, posts, and sponsors to include structured data for better SEO. * fix: resolve Cache Components prerender errors blocking production build Dashboard pages are auth-gated and render live data per request, but were being prerendered into the static shell, tripping Cache Components rules (Date.now in lib/config, uncached Sanity fetches outside Suspense). - Refactor dashboard pages to the three-layer pattern: static shell plus a Suspense-wrapped child that calls connection() before fetching (config, content, pipeline, review, review/[id], sponsors, videos) - Wrap the live time-reading components on the dashboard index in Suspense - Wrap NavHeaderSlot and Footer in the (main) layout in Suspense so the one dynamic route (/podcast/preview/[token]) can produce a static shell - Make /api/youtube/rss.xml dynamic via connection() (external fetch + Date) Co-authored-by: Cursor * refactor: update RSS feed generation methods for podcasts and blogs - Replaced the deprecated `buildFeed` and `buildPodcastFeed` functions with `getFeedJson` and `getPodcastFeedXml` for improved performance and clarity. - Streamlined the fetching of feed data to reduce payload size and enhance caching efficiency. - Updated related route handlers to utilize the new feed generation methods, ensuring consistent data retrieval across JSON and XML feeds. * refactor: update cache usage in live data fetching functions - Changed cache directive from "use cache" to "use cache: remote" in the `sanityFetchStaticParams` and `sanityFetchMetadata` functions to clarify the caching strategy. - This update enhances the understanding of data fetching behavior in relation to remote caching. * refactor: enhance data fetching and caching strategies in dashboard components - Updated dashboard components to improve data fetching efficiency by implementing a more structured caching approach. - Refactored components to better utilize Suspense for dynamic data rendering, ensuring a smoother user experience. - Adjusted the integration of live data fetching methods to align with recent caching updates, enhancing overall performance. * feat: scaffold Astro 7 site on Cloudflare Workers Adds apps/site (@codingcatdev/site) as a new workspace package alongside the existing Next app, which keeps building until the dashboard is ported. Added rather than replacing apps/web because the pre-Next Astro app (branch `dev`) was also named @codingcatdev/web — replacing in place would make every `pnpm --filter` and the git history ambiguous. - output: "server" + @astrojs/cloudflare 14 (SSR on Workers) - wrangler.jsonc with dev/production envs; D1 binding deliberately omitted until the codingcat.dev Cloudflare account is available - rawFonts() Vite plugin harvested verbatim from dev:apps/web/astro.config.mjs (Workers has no filesystem; needed for OG image generation) - Dropped that config's Vite alias/dedupe/optimizeDeps block — it existed to fight @sanity/visual-editing under Astro 6 and is obsoleted by @sanity/visual-editing-standalone - Node pinned to >=22.12 (.nvmrc + engines), required by sanity 6 / @sanity/ui 4 - baseline/ holds pre-migration production sitemap + RSS snapshots for post-cutover parity diffing (472 URLs, 50 items per feed) Verified: pnpm build succeeds and wrangler dev serves SSR HTML via workerd. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HZ4S1uTJwFdbv3A6PHWfw2 * chore: upgrade Sanity Studio to v6.9.2 sanity 5.17.1 -> 6.9.2, @sanity/ui 3 -> 4, @sanity/vision 6.9.2, @sanity/icons 3 -> 5, @sanity/assist 6.1.19, @sanity/code-input 7.3.5, sanity-plugin-media 4 -> 6, @portabletext/react 6 -> 7 (matching sanity@6.9.2's own dep so toPlainText resolves a single copy). Four blockers surfaced during the upgrade: 1. @sanity/icons v5 removed every icon from the root entry (they remain as `declare const X: never` with a deprecation naming the fix). All eight imports moved to subpaths, e.g. @sanity/icons/User. 2. The workspace override "@portabletext/sanity-bridge": "^1.2.14" starved sanity@6.9.2, which needs ^3.2.5 for getSanitySubSchema. Removed; pnpm now resolves 1.2.14 for apps/web's sanity@5 and 3.2.5 for the Studio. 3. @codingcatdev/sanity-plugin-podcast-rss could not simply be dropped: schemas/documents/podcast.ts declares a field of type "podcastRssEpisode", which only that plugin registers, so removing it broke schema extraction. Vendored into plugins/podcast-rss/ (see its README) migrated to @sanity/ui v4 — Menu/MenuButton/MenuItem from the /menu subpath, `space` -> `gap` — and off xml2js onto the native DOMParser, since xml2js only resolved via a Vite polyfill that Vite 8 no longer provides. Field shapes are kept byte-identical so existing podcast documents are unaffected. 4. @sanity/ui v4: Stack `space` -> `gap` (2 call sites) and Avatar `alt` -> `title`. Also fixed pre-existing issues the upgrade exposed: react-inlinesvg was imported by the Studio but declared only in apps/web; SharePreviewActionButton imported five symbols from "@/sanity/lib/api" despite the Studio tsconfig having no such path alias (all unused); CodeSandboxPreview imported an unused RenderPreviewCallbackProps. Removed the stray unused podcast-rss dependency from apps/web. tsconfig target ES2020 -> ES2022 for Array.prototype.at. Verified: builds, typechecks clean, dev server boots on Vite 8.2.1, and schema extract succeeds with zero types removed (two added by sanity-plugin-media v6). Still needs a human click-test of markdown paste in the Portable Text editor. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HZ4S1uTJwFdbv3A6PHWfw2 * ci: add build/lint/typecheck gate and repo agent docs The repo had no CI at all — no lint, typecheck, build, or test ran on any push. Two latent breakages had accumulated unnoticed as a result: - apps/web/biome.json is invalid for Biome 2 (`jsxRuntime: "automatic"` is not an accepted value), so `pnpm lint` there has been failing on a config error rather than linting anything. Left as-is since apps/web is being retired; apps/site gets a corrected Biome 2 config (`assist.actions.source`, `files.includes` with negations, `jsxRuntime: "transparent"`). - apps/sanity/extract.json can silently drift from the schema, which is how three divergent copies accumulated. CI now re-runs the extract and fails if the result differs from what is committed. ci.yml runs lint + typecheck + build for the site, typecheck + build for the Studio, and the extract drift check. Uses node-version-file: .nvmrc rather than a bare "22", which is what actually enforces the >=22.12 floor that sanity 6 and @sanity/ui 4 require; deploy.yml switched to the same. deploy.yml also gains a paths filter so site-only changes stop redeploying the Studio on every push. Adds a root CLAUDE.md (the repo had no root docs) recording the package layout, the in-flight migration, where the vendored Sanity skills live, and the conventions that have already caused real bugs here — no getStaticPaths under output: "server", GROQ must live in .ts files for TypeGen, never read secrets from import.meta.env, bindings are not inherited into env.production, and stegaClean anything bound to an attribute or feed. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HZ4S1uTJwFdbv3A6PHWfw2 * feat: Astro Sanity data layer and consolidated typegen Adds apps/site/src/lib/sanity/* and converts all 26 GROQ queries from `groq` tagged templates to `defineQuery`, so generated types flow to call sites and the `as` casts the Next app needed are gone. Two assumptions from the migration plan turned out to be wrong, and were verified empirically before committing to a layout: - TypeGen DOES resolve const interpolation inside defineQuery, including across module boundaries. The shared projection partials (baseFieldsNoContent, contentFields, ...) are therefore kept in queries/shared.ts rather than being inlined into every query. - @astrojs/cloudflare v14 reduced `Runtime` to `{ cfContext }` and marks `locals.runtime` a deprecated getter. Bindings and vars are read via `import { env } from "cloudflare:workers"` instead — which is also what the pre-Next Astro app did. Three bugs in the harvested fetch layer (dev:apps/web/src/utils/sanity.ts) are fixed rather than carried over: - `useCdn: false` on the base client. Under Next this was masked by ISR; under pure SSR it would put every anonymous page view on the uncached live API. Published reads now use the CDN, which Sanity purges on publish. - stega configured on the base client and suppressed per-request. Inverted: off by default, on only in previewClient, so RSS/sitemap/OG/JSON-LD cannot leak zero-width characters into hrefs and XML. - read token sourced from `import.meta.env`, which Vite inlines into the server bundle — that would bake the token into the deployed Worker script. Now read per-request from the runtime env, and preview fails closed when absent. Config is resolved per-request because wrangler `vars` reach the Worker through the runtime env, not `import.meta.env`. apiVersion is pinned to 2025-09-30 to match apps/sanity/sanity.config.ts; the old Astro app used 2026-03-17, a silent divergence between what authors preview in Vision and what the site renders. Middleware builds the request context onto `locals.sanity` and sets `no-store, private` + `noindex` on preview responses, so a page containing unpublished drafts cannot be written to the edge cache. Queries that took a $type parameter (docCount, rssQuery) are split per type — a parameterised _type widens the generated result to a union of every document type. urlForImage is consolidated to one implementation keeping the `.auto("format")` behaviour; resolveHref now covers the whole public route map instead of only "post". Typegen moves entirely into apps/sanity: apps/web/sanity.cli.ts is deleted along with its typegen/predev/prebuild scripts and its `sanity` devDependency, and the two stale schema copies (apps/web/sanity/extract.json, apps/web/schema.json) are removed. CI regenerates types before typechecking and fails if the committed extract.json has drifted. Verified end to end: `wrangler dev` serves the home page with real Sanity content from the dev dataset, typechecking clean with no casts. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HZ4S1uTJwFdbv3A6PHWfw2 * feat: Astro layout, components, and listing routes Ports the shared layout and every listing route from the Next app, with production parity as the target rather than a redesign. Design tokens come from apps/web/app/globals.css — the live production stylesheet — not the pre-Next Astro app on `dev`, which carried a different, never-shipped dark-first token set that would have been a visual regression at cutover. Dashboard-only tokens are dropped. Routes: /, /blog + /blog/page/[num], /podcasts + /podcasts/page/[num], /authors|guests|sponsors/page/[num], and 404. Pagination is a runtime bounds check on Astro.params — there is no getStaticPaths — rejecting anything that is not a bare positive integer so one page cannot be served under unlimited URLs, and 302-ing out-of-range requests to the last page. Redirects mirror production exactly: /authors, /guests and /sponsors redirect to /page/1, and the bare /{base}/page stubs go to /page/1 (not to the section index, which is what the scaffold assumed). Components ship no framework JS. next-themes becomes an inline script, the Radix Sheet becomes a details-free drawer, react-icons paths are inlined, and usePathname-based active-nav detection is resolved at render time. A blog listing page now carries 1.4 KB of inline script and no external bundle. Two fixes over the Next behaviour: - Canonical and og:url were derived from build-time `Astro.site`, but SITE_URL is a per-environment wrangler var and CI builds once for both Workers — the dev Worker would have claimed production URLs. Resolved per request in middleware as `Astro.locals.siteUrl`. - JSON-LD advertised an Organization logo at /icon.svg, which 404s in production because Next serves that file at a hashed metadata URL. The asset is now in public/ and the URL resolves. The home page's two-column grids drop the `slice(0, length / 2)` index arithmetic, which silently dropped the last item on an odd count. Verified against a real Worker under `wrangler dev`: all routes 200, /blog/page/abc 404s, /blog/page/99999 302s to the last page, no stega leakage into attributes, and .assetsignore keeps _worker.js unserved. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HZ4S1uTJwFdbv3A6PHWfw2 * feat: portable text renderers and content detail routes Twelve Astro renderers for the custom block types, plus the detail routes that consume them: /post/[slug], /podcast/[slug], /author/[slug], /guest/[slug], /sponsor/[slug], and the /[slug] catch-all for page documents. The pre-Next Astro app rendered Portable Text with no component map at all, so every custom block rendered as nothing. This is the piece that was genuinely missing rather than portable. astro-portabletext uses SINGULAR `type` / `block` / `mark` keys where @portabletext/react uses `types` / `marks`, and mark components receive the mark definition on `node.markDef` rather than on the node. Both fail silently — the components simply never run — so both are called out at the definition sites. Dependencies dropped, not ported: - react-syntax-highlighter and the hand-maintained Prism language list become Astro's bundled Shiki, highlighting at render time. Language ids are normalised through an allowlist first: `@sanity/code-input` does not constrain the field, and one live document carries `language: "Zanzibar"`, which Shiki throws on. The allowlist is typed as a literal union so a typo in it is a compile error rather than a runtime 500. - react-markdown per table cell. No table block exists in production — every cell in the dataset is null — so it was never exercised. Cells render as text. - embla-carousel for YouTube shorts becomes a scroll-snap row. - react-twitter-embed becomes the official blockquote markup, with widgets.js loaded only when a page actually contains a post. It now degrades to a readable link when the script is blocked. - react-icons for the 20 social platforms: paths inlined. Fixes carried in: - CodePen and CodeSandbox URL parsing was positional destructuring and an unguarded double `.at(-1)`; a malformed URL threw at render. Both validate now. - Tables get a header row. The Next version emitted every row as `` in a bare ``, leaving no column headers for screen readers. - Sanity's file-asset URL for cover video read NEXT_PUBLIC_* env at module scope, which does not exist on Workers; it uses the per-request config. - Socials rendered an empty clickable link for any key without an icon (`email`, `polywork`); unknown keys are skipped and both now have one. Podcast episodes play through a native audio element for now — the persistent floating player is a React island and lands with the rest of the islands. Verified against a real Worker: code, quote, image, htmlBlock and twitter blocks all render from live dev-dataset documents, Shiki emits dual-theme output with the dark variant activated, and every detail route 200s while unknown slugs 404. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HZ4S1uTJwFdbv3A6PHWfw2 * feat: feeds, sitemap, and robots Ports /blog/rss.{xml,json}, /podcasts/rss.{xml,json}, /sitemap.xml and /robots.txt, all serialized by hand — the `feed` package reaches for Node built-ins that are awkward on workerd, and these are fixed shapes. Verified against the pre-migration production snapshots in apps/site/baseline by running the Worker with the production config: - sitemap.xml: byte-identical, all 472 URLs - podcasts/rss.xml: byte-identical apart from timestamps - blog/rss.xml: identical apart from , which no longer names a library we do not use - both JSON feeds: same key order, same 50 ids in the same order - robots.txt: identical GUID stability was the thing to get right and it holds: all 100 feed items keep their Sanity _id, in order. A changed podcast GUID would re-publish the entire back catalogue to Apple and Spotify. Two live bugs fixed rather than reproduced: - The production blog feed is not well-formed XML. The `feed` library emits unescaped `&` in enclosure URLs, so `ET.parse` rejects blog_rss.xml at line 25. Ours escapes them. - Feed bodies were serialized by `toHTML` with no component map, so internal links rendered as `` and code, images, tables and embeds were dropped or stubbed. There is now a map; links resolve to absolute URLs so they work inside a reader. Deliberately reproduced, including the odd parts: `image/jpg` rather than `image/jpeg` for enclosures, one `` element per author, and `` omitted entirely when the excerpt is empty. Churning those would change what existing subscribers see for no benefit. robots.txt now serves `Disallow: /` outside production, so the dev Worker and preview aliases cannot be indexed alongside the real site. Also fixes a deployment trap found while verifying this. The Cloudflare adapter flattens wrangler.jsonc at BUILD time and writes .wrangler/deploy/config.json, which redirects wrangler away from the source config — `env.production` is dropped, keeping only its name. So `wrangler deploy --env production` on a default build would have shipped the dev dataset under the dev worker name. The environment is selected by `CLOUDFLARE_ENV` at build time; added a `build:production` script and documented it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HZ4S1uTJwFdbv3A6PHWfw2 * feat: OG image generation on Workers Four Satori-backed endpoints — /api/og/{default,blog,podcast,person}.png — recovered from the pre-Next Astro app along with the two Inter TTFs and the rawFonts Vite plugin that inlines them as Uint8Array (Workers has no filesystem for Satori to read from). The load-bearing part of that recovery is its workaround, kept verbatim with its explanation: workers-og parses HTML with HTMLRewriter, which corrupts the HTML-to-VDOM conversion on Workers, so these build React element objects and hand them to ImageResponse directly. Wired in as a FALLBACK, not a replacement. Where a document has a cover image the card is still that image, byte-for-byte what the Next app served. Documents without one previously emitted no og:image at all; those now get a generated card. The home page and section indexes get one too. Verified on real workerd under `wrangler dev`, not `astro dev` — all four endpoints return valid 1200x630 PNGs, and the rendered images were inspected rather than just status-checked. That caught the one visual defect: the recovered logo used a cat emoji, and Satori renders only glyphs present in the fonts it is given, so it came out as a tofu box on every card. Replaced with a monogram tile in the brand gradient. Brand tokens are literal hex rather than the CSS custom properties the rest of the site uses: Satori resolves neither var() nor oklch(). Font imports are now typed via a `*.ttf` module declaration, since Astro's own asset typings declare them as URL strings — the opposite of what the rawFonts plugin actually returns. That removes the `@ts-ignore` pair and the `as any` in loadFonts. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HZ4S1uTJwFdbv3A6PHWfw2 * feat: Astro SSR parity, Sanity semantic search, SearchModal, and syndication * ci: add cf-typegen step and automatic PR preview comment with Cloudflare preview URL * fix(ci): fix yaml indentation and use echo for pr comment markdown * fix(ci): fix hardcoded font paths in test mocks and enhance pr preview url reporting * fix(ci): fix yaml indentation and clean up secrets syntax in workflows * feat(sanity): dedicated studio dataset workspaces and deploy workflow environment passing --------- Co-authored-by: Miriad Co-authored-by: Cursor Co-authored-by: Claude Opus 5 (1M context) --- .../content-modeling-best-practices/SKILL.md | 32 + .../references/content-reuse.md | 134 + .../references/reference-vs-embedding.md | 89 + .../references/separation-of-concerns.md | 60 + .../references/taxonomy-classification.md | 136 + .../skills/portable-text-conversion/SKILL.md | 65 + .../rules/html-to-pt.md | 242 + .../rules/manual-construction.md | 210 + .../rules/markdown-to-pt.md | 204 + .../portable-text-serialization/SKILL.md | 111 + .../rules/astro.md | 124 + .../portable-text-serialization/rules/html.md | 115 + .../rules/markdown.md | 123 + .../rules/plain-text.md | 66 + .../rules/react.md | 142 + .../rules/svelte.md | 134 + .../portable-text-serialization/rules/vue.md | 125 + .agents/skills/sanity-best-practices/SKILL.md | 74 + .../references/angular.md | 565 + .../references/app-sdk.md | 462 + .../sanity-best-practices/references/astro.md | 107 + .../references/blueprints.md | 88 + .../references/functions.md | 634 + .../references/get-started.md | 315 + .../sanity-best-practices/references/groq.md | 350 + .../references/hydrogen.md | 261 + .../sanity-best-practices/references/image.md | 124 + .../references/localization.md | 432 + .../references/migration-html-import.md | 138 + .../references/migration.md | 44 + .../references/nextjs.md | 544 + .../sanity-best-practices/references/nuxt.md | 84 + .../references/page-builder.md | 307 + .../references/portable-text.md | 365 + .../references/project-structure.md | 116 + .../sanity-best-practices/references/remix.md | 134 + .../references/schema.md | 378 + .../sanity-best-practices/references/seo.md | 331 + .../references/studio-structure.md | 136 + .../references/svelte.md | 155 + .../references/typegen.md | 215 + .../references/visual-editing.md | 263 + .../sanity-live-cache-components/SKILL.md | 168 + .../reference/dynamic-segments.md | 114 + .../reference/layouts.md | 163 + .../reference/live-helpers.md | 220 + .../reference/three-layer-pattern.md | 146 + .../skills/seo-aeo-best-practices/SKILL.md | 37 + .../references/aeo-considerations.md | 159 + .../references/eeat-principles.md | 127 + .../references/structured-data.md | 183 + .../references/technical-seo.md | 188 + .github/.branch-cleanup | 1 + .github/workflows/ci.yml | 153 + .github/workflows/deploy-workers.yml | 83 + .github/workflows/deploy.yml | 50 +- .gitignore | 4 + .nvmrc | 1 + CLAUDE.md | 88 + apps/sanity/components/CodeSandboxPreview.tsx | 1 - apps/sanity/components/YouTubePreview.tsx | 2 +- .../SharePreviewActionButton.tsx | 6 +- .../documentActions/sharePreviewAction.tsx | 2 +- apps/sanity/extract.json | 15097 +++++++++ .../functions/syndicate-content/index.ts | 238 + apps/sanity/package.json | 31 +- apps/sanity/plugins/podcast-rss/README.md | 21 + .../podcast-rss/components/EpisodePicker.tsx | 116 + .../plugins/podcast-rss/components/Popup.tsx | 23 + .../podcast-rss/components/SearchBar.tsx | 21 + .../podcast-rss/components/SearchResults.tsx | 71 + apps/sanity/plugins/podcast-rss/constants.ts | 3 + .../podcast-rss/hooks/useDebouncedCallback.ts | 22 + .../plugins/podcast-rss/hooks/useQuery.ts | 51 + apps/sanity/plugins/podcast-rss/index.ts | 25 + .../podcast-rss/schema/podcastRssEpisode.ts | 80 + apps/sanity/plugins/podcast-rss/types.ts | 26 + apps/sanity/plugins/podcast-rss/utils/rss.ts | 83 + apps/sanity/sanity.blueprint.ts | 14 + apps/sanity/sanity.cli.ts | 8 + apps/sanity/sanity.config.ts | 70 +- apps/sanity/schemas/documents/category.ts | 2 +- apps/sanity/schemas/documents/guest.ts | 2 +- apps/sanity/schemas/documents/page.ts | 2 +- apps/sanity/schemas/documents/short.ts | 2 +- apps/sanity/schemas/documents/syndication.ts | 82 + apps/sanity/schemas/partials/base.ts | 2 +- .../sanity/schemas/singletons/engineConfig.ts | 10 + apps/sanity/schemas/singletons/settings.tsx | 2 +- apps/sanity/tsconfig.json | 9 +- apps/site/.dev.vars.example | 2 + apps/site/.gitignore | 6 + apps/site/astro.config.mjs | 82 + apps/site/baseline/blog_rss.json | 663 + apps/site/baseline/blog_rss.xml | 518 + apps/site/baseline/podcasts_rss.json | 664 + apps/site/baseline/podcasts_rss.xml | 1418 + apps/site/baseline/robots.txt | 7 + apps/site/baseline/sitemap.xml | 2835 ++ apps/site/biome.json | 67 + apps/site/package.json | 45 + apps/{web => site}/public/.assetsignore | 0 apps/site/public/ads.txt | 1 + apps/site/public/apple-touch-icon.png | Bin 0 -> 18156 bytes apps/site/public/favicon-16x16.png | Bin 0 -> 768 bytes apps/site/public/favicon-32x32.png | Bin 0 -> 1928 bytes apps/site/public/favicon.ico | Bin 0 -> 10979 bytes apps/site/public/icon.png | Bin 0 -> 62066 bytes apps/site/public/icon.svg | 35 + .../src/assets/fonts/Inter-Bold.ttf | Bin .../src/assets/fonts/Inter-Regular.ttf | Bin apps/site/src/components/AnimatedHero.astro | 48 + apps/site/src/components/Avatar.astro | 65 + apps/site/src/components/Breadcrumbs.astro | 33 + apps/site/src/components/Container.astro | 11 + apps/site/src/components/ContentCard.astro | 73 + apps/site/src/components/ContentGrid.astro | 22 + apps/site/src/components/CoverImage.astro | 54 + apps/site/src/components/CoverMedia.astro | 61 + .../site/src/components/DraftModeToggle.astro | 22 + apps/site/src/components/Footer.astro | 106 + apps/site/src/components/FormattedDate.astro | 38 + apps/site/src/components/Header.astro | 139 + apps/site/src/components/HomeSection.astro | 63 + apps/site/src/components/JsonLd.astro | 23 + apps/site/src/components/NavLink.astro | 49 + apps/site/src/components/Pagination.astro | 76 + apps/site/src/components/PodcastListen.astro | 80 + apps/site/src/components/PodcastPicks.astro | 86 + apps/site/src/components/PodmatchBadge.astro | 59 + apps/site/src/components/SearchModal.astro | 421 + apps/site/src/components/SectionHeader.astro | 30 + apps/site/src/components/SocialMeta.astro | 90 + apps/site/src/components/SponsorCard.astro | 52 + apps/site/src/components/ThemeScript.astro | 28 + apps/site/src/components/ThemeToggle.astro | 54 + apps/site/src/components/UserSocials.astro | 78 + .../src/components/icons/AjHeadphones.astro | 257 + .../src/components/icons/AjPrimaryAlt.astro | 158 + .../portable-text/PortableTextContent.astro | 119 + .../src/components/portable-text/PtCode.astro | 68 + .../components/portable-text/PtCodePen.astro | 51 + .../portable-text/PtCodeSandbox.astro | 49 + .../components/portable-text/PtHeading5.astro | 7 + .../components/portable-text/PtHeading6.astro | 7 + .../src/components/portable-text/PtHtml.astro | 22 + .../components/portable-text/PtImage.astro | 46 + .../portable-text/PtInternalLink.astro | 19 + .../src/components/portable-text/PtLink.astro | 26 + .../components/portable-text/PtQuote.astro | 42 + .../components/portable-text/PtTable.astro | 55 + .../components/portable-text/PtTweet.astro | 30 + .../components/portable-text/PtYouTube.astro | 53 + .../portable-text/PtYouTubeShorts.astro | 62 + apps/site/src/env.d.ts | 31 + apps/site/src/layouts/BaseLayout.astro | 127 + apps/site/src/lib/code-languages.ts | 102 + apps/site/src/lib/content.ts | 45 + apps/site/src/lib/feeds.ts | 311 + apps/site/src/lib/hero-icons.ts | 100 + apps/site/src/lib/og-utils.ts | 459 + apps/site/src/lib/og.ts | 53 + apps/site/src/lib/pagination.ts | 63 + apps/site/src/lib/portable-text-html.ts | 123 + apps/site/src/lib/sanity/client.ts | 70 + apps/site/src/lib/sanity/config.ts | 50 + apps/site/src/lib/sanity/context.ts | 67 + apps/site/src/lib/sanity/image.ts | 56 + apps/site/src/lib/sanity/load-query.ts | 39 + apps/site/src/lib/sanity/preview.ts | 63 + apps/site/src/lib/sanity/queries/feeds.ts | 29 + apps/site/src/lib/sanity/queries/index.ts | 7 + apps/site/src/lib/sanity/queries/pages.ts | 33 + apps/site/src/lib/sanity/queries/people.ts | 59 + apps/site/src/lib/sanity/queries/podcasts.ts | 43 + apps/site/src/lib/sanity/queries/posts.ts | 39 + apps/site/src/lib/sanity/queries/search.ts | 48 + apps/site/src/lib/sanity/queries/shared.ts | 125 + apps/site/src/lib/sanity/queries/sponsors.ts | 31 + apps/site/src/lib/sanity/resolve-href.ts | 52 + apps/site/src/lib/site.ts | 43 + apps/site/src/lib/social-icons.ts | 158 + apps/site/src/lib/structured-data.ts | 161 + apps/site/src/lib/xml.ts | 43 + apps/site/src/lib/youtube.ts | 16 + apps/site/src/middleware.ts | 38 + apps/site/src/pages/404.astro | 37 + apps/site/src/pages/[slug].astro | 43 + apps/site/src/pages/api/draft-mode/disable.ts | 61 + apps/site/src/pages/api/draft-mode/enable.ts | 46 + apps/site/src/pages/api/og/blog.png.ts | 44 + apps/site/src/pages/api/og/default.png.ts | 47 + apps/site/src/pages/api/og/person.png.ts | 43 + apps/site/src/pages/api/og/podcast.png.ts | 49 + apps/site/src/pages/api/search.ts | 138 + apps/site/src/pages/api/sponsorship.ts | 121 + apps/site/src/pages/author/[slug].astro | 95 + apps/site/src/pages/authors/index.astro | 3 + apps/site/src/pages/authors/page/[num].astro | 44 + apps/site/src/pages/authors/page/index.astro | 3 + apps/site/src/pages/blog/index.astro | 90 + apps/site/src/pages/blog/page/[num].astro | 48 + apps/site/src/pages/blog/page/index.astro | 3 + apps/site/src/pages/blog/rss.json.ts | 28 + apps/site/src/pages/blog/rss.xml.ts | 28 + apps/site/src/pages/guest/[slug].astro | 95 + apps/site/src/pages/guests/index.astro | 3 + apps/site/src/pages/guests/page/[num].astro | 44 + apps/site/src/pages/guests/page/index.astro | 3 + apps/site/src/pages/index.astro | 49 + apps/site/src/pages/podcast/[slug].astro | 160 + apps/site/src/pages/podcasts/index.astro | 92 + apps/site/src/pages/podcasts/page/[num].astro | 44 + apps/site/src/pages/podcasts/page/index.astro | 3 + apps/site/src/pages/podcasts/rss.json.ts | 28 + apps/site/src/pages/podcasts/rss.xml.ts | 28 + apps/site/src/pages/post/[slug].astro | 114 + apps/site/src/pages/robots.txt.ts | 34 + apps/site/src/pages/search.astro | 422 + apps/site/src/pages/sitemap.xml.ts | 60 + apps/site/src/pages/sponsor/[slug].astro | 92 + apps/site/src/pages/sponsors/index.astro | 3 + apps/site/src/pages/sponsors/page/[num].astro | 44 + apps/site/src/pages/sponsors/page/index.astro | 3 + apps/site/src/pages/sponsorships.astro | 276 + apps/site/src/styles/global.css | 213 + apps/site/tests/README.md | 92 + apps/site/tests/cf-mock.mjs | 1 + apps/site/tests/font-bold-mock.mjs | 10 + apps/site/tests/font-reg-mock.mjs | 13 + apps/site/tests/helpers.mjs | 167 + apps/site/tests/run-e2e.mjs | 169 + apps/site/tests/setup.mjs | 1 + apps/site/tests/tier1-features.test.mjs | 1008 + apps/site/tests/tier2-boundaries.test.mjs | 608 + apps/site/tests/tier3-combinations.test.mjs | 371 + .../tests/tier4-baseline-contract.test.mjs | 367 + apps/site/tsconfig.json | 7 + apps/site/wrangler.jsonc | 54 + apps/web/.dev.vars.example | 15 - apps/web/.env.example | 76 +- apps/web/.eslintignore | 2 + apps/web/.prettierignore | 3 + apps/web/README.md | 43 - apps/web/app/(dashboard)/dashboard/actions.ts | 10 + .../dashboard/auth/callback/route.ts | 20 + .../dashboard/config/config-form.tsx | 597 + .../app/(dashboard)/dashboard/config/page.tsx | 63 + .../(dashboard)/dashboard/content/actions.ts | 16 + .../dashboard/content/content-ideas-table.tsx | 176 + .../(dashboard)/dashboard/content/page.tsx | 68 + .../app/(dashboard)/dashboard/login/page.tsx | 92 + apps/web/app/(dashboard)/dashboard/page.tsx | 44 + .../(dashboard)/dashboard/pipeline/page.tsx | 218 + .../dashboard/review/[id]/page.tsx | 61 + .../review/[id]/review-detail-client.tsx | 305 + .../(dashboard)/dashboard/review/actions.ts | 102 + .../app/(dashboard)/dashboard/review/page.tsx | 125 + .../(dashboard)/dashboard/settings/page.tsx | 115 + .../dashboard/settings/settings-form.tsx | 370 + .../(dashboard)/dashboard/sponsors/actions.ts | 10 + .../(dashboard)/dashboard/sponsors/page.tsx | 123 + .../sponsors/sponsor-leads-table.tsx | 198 + .../dashboard/sponsors/sponsor-pool-table.tsx | 131 + .../(dashboard)/dashboard/videos/actions.ts | 26 + .../app/(dashboard)/dashboard/videos/page.tsx | 75 + .../dashboard/videos/videos-table.tsx | 190 + apps/web/app/(dashboard)/layout.tsx | 103 + .../(main)/(author)/author/[slug]/page.tsx | 161 + apps/web/app/(main)/(author)/authors/page.tsx | 5 + .../(author)/authors/page/[num]/page.tsx | 86 + .../app/(main)/(author)/authors/page/page.tsx | 5 + .../app/(main)/(guest)/guest/[slug]/page.tsx | 159 + apps/web/app/(main)/(guest)/guests/page.tsx | 5 + .../(main)/(guest)/guests/page/[num]/page.tsx | 81 + .../app/(main)/(guest)/guests/page/page.tsx | 5 + .../app/(main)/(podcast)/podcast/Podcast.tsx | 151 + .../(main)/(podcast)/podcast/[slug]/page.tsx | 119 + .../app/(main)/(podcast)/podcast/picks.tsx | 88 + .../podcast/preview/[token]/page.tsx | 49 + .../app/(main)/(podcast)/podcasts/page.tsx | 141 + .../(podcast)/podcasts/page/[num]/page.tsx | 86 + .../(main)/(podcast)/podcasts/page/page.tsx | 5 + .../(podcast)/podcasts/rss.json/route.ts | 14 + .../(podcast)/podcasts/rss.xml/route.ts | 13 + apps/web/app/(main)/(post)/blog/page.tsx | 127 + .../(main)/(post)/blog/page/[num]/page.tsx | 81 + apps/web/app/(main)/(post)/blog/page/page.tsx | 5 + .../app/(main)/(post)/blog/rss.json/route.ts | 14 + .../app/(main)/(post)/blog/rss.xml/route.ts | 14 + .../app/(main)/(post)/post/[slug]/page.tsx | 183 + .../(main)/(sponsor)/sponsor/[slug]/page.tsx | 145 + .../app/(main)/(sponsor)/sponsors/page.tsx | 5 + .../(sponsor)/sponsors/page/[num]/page.tsx | 99 + .../(main)/(sponsor)/sponsors/page/page.tsx | 5 + .../(main)/(top-level-pages)/[slug]/page.tsx | 113 + .../app/(main)/(top-level-pages)/pro/page.tsx | 100 + .../(main)/(top-level-pages)/search/page.tsx | 10 + .../(top-level-pages)/sponsorships/page.tsx | 108 + .../sponsorships/sponsorship-form.tsx | 241 + apps/web/app/(main)/icon.svg | 35 + apps/web/app/(main)/layout.tsx | 236 + apps/web/app/(main)/meta-pixel.tsx | 31 + apps/web/app/(main)/page.tsx | 218 + apps/web/app/(sponsor-portal)/layout.tsx | 19 + apps/web/app/(sponsor-portal)/portal/page.tsx | 17 + apps/web/app/api/algolia/route.tsx | 125 + apps/web/app/api/auth/logout/route.tsx | 9 + apps/web/app/api/auth/session/route.tsx | 5 + .../web/app/api/auth/session/verify/route.tsx | 5 + apps/web/app/api/cron/check-renders/route.ts | 583 + apps/web/app/api/cron/check-research/route.ts | 1083 + apps/web/app/api/cron/ingest/route.ts | 726 + apps/web/app/api/cron/route.tsx | 43 + .../app/api/cron/sponsor-outreach/route.ts | 137 + apps/web/app/api/dashboard/activity/route.ts | 41 + apps/web/app/api/dashboard/config/route.ts | 175 + apps/web/app/api/dashboard/metrics/route.ts | 47 + apps/web/app/api/dashboard/pipeline/route.ts | 46 + apps/web/app/api/dashboard/review/route.ts | 115 + apps/web/app/api/dashboard/settings/route.ts | 150 + apps/web/app/api/devto/route.tsx | 253 + apps/web/app/api/draft-mode/disable/route.tsx | 9 + apps/web/app/api/draft-mode/enable/route.tsx | 7 + .../app/api/generate-preview-token/route.ts | 44 + apps/web/app/api/get-preview-content/route.ts | 64 + apps/web/app/api/hashnode/route.tsx | 361 + apps/web/app/api/sponsor/opt-out/route.ts | 89 + apps/web/app/api/sponsorship/route.ts | 189 + .../api/sponsorship/sponsorship-template.tsx | 48 + apps/web/app/api/verify-turnstile.ts | 39 + .../app/api/webhooks/sanity-content/route.ts | 105 + .../api/webhooks/sanity-distribute/route.ts | 402 + .../api/webhooks/sanity-revalidate/route.ts | 67 + .../app/api/webhooks/sponsor-inbound/route.ts | 98 + .../app/api/webhooks/stripe-sponsor/route.ts | 172 + apps/web/app/api/youtube/rss.xml/route.tsx | 109 + apps/web/app/api/youtube/views/route.tsx | 51 + apps/web/app/globals.css | 224 + apps/web/app/robots.ts | 17 + apps/web/app/sitemap.ts | 46 + apps/web/astro.config.mjs | 113 - apps/web/biome.json | 54 + apps/web/cache-report.json | 3781 +++ apps/web/components.json | 21 + apps/web/components/algolia-dialog.tsx | 44 + apps/web/components/algolia-search.tsx | 211 + apps/web/components/analytics.tsx | 21 + apps/web/components/animated-hero.tsx | 81 + apps/web/components/app-sidebar.tsx | 123 + apps/web/components/avatar.tsx | 56 + apps/web/components/badge-pro.tsx | 19 + apps/web/components/become-sponsor-popup.tsx | 48 + apps/web/components/block-code-btn.tsx | 29 + apps/web/components/block-code.tsx | 36 + apps/web/components/block-image.tsx | 36 + apps/web/components/block-table.tsx | 28 + apps/web/components/bookmark.tsx | 5 + apps/web/components/breadrumb-links.tsx | 43 + apps/web/components/cloudflare-turnstile.tsx | 25 + apps/web/components/codepen-embed.tsx | 19 + apps/web/components/codesandbox-embed.tsx | 20 + .../components/cookies-provider-client.tsx | 11 + apps/web/components/cookies-provider.tsx | 0 apps/web/components/cover-image.tsx | 47 + apps/web/components/cover-media.tsx | 36 + apps/web/components/cover-video.tsx | 43 + apps/web/components/date.tsx | 9 + apps/web/components/disable-draft-mode.tsx | 25 + apps/web/components/footer.tsx | 124 + apps/web/components/google-ad-banner.tsx | 64 + apps/web/components/html-embed.tsx | 7 + apps/web/components/icons.tsx | 3 + apps/web/components/icons/aj-alt.tsx | 118 + apps/web/components/icons/aj-dark.tsx | 89 + .../components/icons/aj-headphones-alt.tsx | 258 + apps/web/components/icons/aj-headphones.tsx | 253 + apps/web/components/icons/aj-light.tsx | 89 + apps/web/components/icons/aj-primary-alt.tsx | 154 + apps/web/components/icons/aj-primary.tsx | 152 + apps/web/components/icons/checkout-arrow.tsx | 18 + apps/web/components/icons/dribble.svg | 14 + apps/web/components/json-ld.tsx | 15 + apps/web/components/mode-toggle.tsx | 40 + apps/web/components/more-content.tsx | 141 + apps/web/components/more-header.tsx | 32 + apps/web/components/nav-header.tsx | 54 + apps/web/components/nav-link.tsx | 26 + apps/web/components/nav-main.tsx | 50 + apps/web/components/nav-user.tsx | 113 + apps/web/components/onboarding.tsx | 73 + apps/web/components/page-refresh-button.tsx | 32 + apps/web/components/paginate-list.tsx | 75 + apps/web/components/pipeline-status.tsx | 139 + apps/web/components/player-context.tsx | 192 + apps/web/components/player-floating.tsx | 189 + apps/web/components/player-play-button.tsx | 54 + apps/web/components/podcast-open-apple.tsx | 20 + apps/web/components/podcast-open-spotify.tsx | 28 + apps/web/components/podcast-open-youtube.tsx | 28 + apps/web/components/podmatch-badge.tsx | 81 + apps/web/components/portable-text.tsx | 97 + apps/web/components/pro-benefits.tsx | 161 + apps/web/components/quote-embed.tsx | 34 + apps/web/components/recent-activity.tsx | 105 + apps/web/components/section-cards-live.tsx | 131 + apps/web/components/section-cards.tsx | 94 + apps/web/components/site-header.tsx | 53 + apps/web/components/sponsor-card.tsx | 52 + apps/web/components/theme-provider.tsx | 9 + apps/web/components/twitter-embed.tsx | 10 + apps/web/components/ui/accordion.tsx | 57 + apps/web/components/ui/alert-dialog.tsx | 141 + apps/web/components/ui/alert.tsx | 59 + apps/web/components/ui/aspect-ratio.tsx | 7 + apps/web/components/ui/avatar.tsx | 50 + apps/web/components/ui/badge.tsx | 36 + apps/web/components/ui/breadcrumb.tsx | 115 + apps/web/components/ui/button.tsx | 57 + apps/web/components/ui/calendar.tsx | 213 + apps/web/components/ui/card.tsx | 83 + apps/web/components/ui/carousel.tsx | 262 + apps/web/components/ui/chart.tsx | 375 + apps/web/components/ui/checkbox.tsx | 30 + apps/web/components/ui/collapsible.tsx | 11 + apps/web/components/ui/command.tsx | 153 + apps/web/components/ui/context-menu.tsx | 200 + apps/web/components/ui/dialog.tsx | 122 + apps/web/components/ui/drawer.tsx | 118 + apps/web/components/ui/dropdown-menu.tsx | 201 + apps/web/components/ui/form.tsx | 179 + apps/web/components/ui/hover-card.tsx | 29 + apps/web/components/ui/input-otp.tsx | 71 + apps/web/components/ui/input.tsx | 22 + apps/web/components/ui/label.tsx | 26 + apps/web/components/ui/menubar.tsx | 256 + apps/web/components/ui/navigation-menu.tsx | 128 + apps/web/components/ui/pagination.tsx | 117 + apps/web/components/ui/popover.tsx | 33 + apps/web/components/ui/progress.tsx | 28 + apps/web/components/ui/radio-group.tsx | 44 + apps/web/components/ui/resizable.tsx | 69 + apps/web/components/ui/scroll-area.tsx | 48 + apps/web/components/ui/select.tsx | 159 + apps/web/components/ui/separator.tsx | 31 + apps/web/components/ui/sheet.tsx | 140 + apps/web/components/ui/sidebar.tsx | 781 + apps/web/components/ui/skeleton.tsx | 15 + apps/web/components/ui/slider.tsx | 28 + apps/web/components/ui/sonner.tsx | 31 + apps/web/components/ui/switch.tsx | 29 + apps/web/components/ui/table.tsx | 120 + apps/web/components/ui/tabs.tsx | 55 + apps/web/components/ui/textarea.tsx | 22 + apps/web/components/ui/toggle-group.tsx | 61 + apps/web/components/ui/toggle.tsx | 45 + apps/web/components/ui/tooltip.tsx | 32 + apps/web/components/user-buy.tsx | 5 + apps/web/components/user-go-pro-button.tsx | 16 + apps/web/components/user-go-pro.tsx | 10 + apps/web/components/user-related.tsx | 96 + apps/web/components/user-socials.tsx | 183 + apps/web/components/youtube-embed.tsx | 70 + apps/web/components/youtube-short-embed.tsx | 97 + apps/web/components/youtube-short.tsx | 41 + apps/web/components/youtube-shorts.tsx | 70 + apps/web/components/youtube.tsx | 43 + apps/web/drizzle.config.ts | 7 - apps/web/e2e/dashboard.spec.ts | 71 + apps/web/e2e/smoke.spec.ts | 27 + apps/web/hooks/use-mobile.ts | 21 + apps/web/lib/block-content-to-markdown.d.ts | 1 + apps/web/lib/config.ts | 76 + apps/web/lib/gemini-metadata.ts | 15 + apps/web/lib/gemini.ts | 41 + apps/web/lib/hooks.ts | 27 + apps/web/lib/prism.ts | 279 + apps/web/lib/resend-notify.ts | 39 + apps/web/lib/rss.ts | 292 + apps/web/lib/sanity-write-client.ts | 16 + apps/web/lib/sanity/dashboard.ts | 55 + apps/web/lib/services/elevenlabs.ts | 403 + apps/web/lib/services/gcs.ts | 351 + apps/web/lib/services/gemini-infographics.ts | 448 + apps/web/lib/services/gemini-research.ts | 535 + apps/web/lib/services/notebooklm/auth.ts | 280 + apps/web/lib/services/notebooklm/client.ts | 986 + apps/web/lib/services/notebooklm/index.ts | 33 + apps/web/lib/services/notebooklm/rpc.ts | 263 + apps/web/lib/services/notebooklm/types.ts | 135 + apps/web/lib/services/pexels.ts | 421 + apps/web/lib/services/remotion-deploy.ts | 173 + apps/web/lib/services/remotion.ts | 431 + apps/web/lib/services/research.ts | 536 + apps/web/lib/services/sanity-upload.ts | 71 + apps/web/lib/services/trend-discovery.ts | 1128 + apps/web/lib/services/video-pipeline.ts | 380 + apps/web/lib/site.ts | 12 + apps/web/lib/sponsor/email-service.ts | 49 + apps/web/lib/sponsor/gemini-intent.ts | 96 + apps/web/lib/sponsor/gemini-outreach.ts | 113 + apps/web/lib/sponsor/sponsor-bridge.ts | 66 + apps/web/lib/sponsor/stripe-service.ts | 120 + apps/web/lib/sponsorship-schema.ts | 21 + apps/web/lib/stripe.types.ts | 364 + apps/web/lib/structured-data.ts | 149 + apps/web/lib/supabase/client.ts | 18 + apps/web/lib/supabase/server.ts | 63 + apps/web/lib/types.ts | 39 + apps/web/lib/types/config.ts | 94 + apps/web/lib/types/dashboard.ts | 17 + apps/web/lib/types/engine-config.ts | 79 + .../web/lib}/types/video-analytics.ts | 0 apps/web/lib/utils.ts | 31 + apps/web/lib/utils/audio-timestamps.ts | 153 + apps/web/lib/x-social.ts | 157 + apps/web/lib/youtube-stats.ts | 367 + apps/web/lib/youtube-upload.ts | 150 + apps/web/next-env.d.ts | 6 + apps/web/next.config.ts | 23 + apps/web/package-lock.json | 20472 ------------- apps/web/package.json | 159 +- apps/web/playwright.config.ts | 43 + apps/web/postcss.config.js | 5 + apps/web/postcss.config.mjs | 3 - apps/web/proxy.ts | 73 + apps/web/public/ads.txt | 1 + apps/web/public/favicon.svg | 3 - apps/web/public/sanity-icons/apple-icon.png | Bin 0 -> 3682 bytes apps/web/public/sanity-icons/icon.ico | Bin 0 -> 4286 bytes apps/web/public/sanity-icons/icon.png | Bin 0 -> 10466 bytes apps/web/remotion/Root.tsx | 98 + apps/web/remotion/components/CTAScene.tsx | 177 + .../remotion/components/CodeMorphScene.tsx | 388 + .../components/ComparisonGridScene.tsx | 351 + .../remotion/components/DynamicListScene.tsx | 214 + apps/web/remotion/components/HookScene.tsx | 227 + .../remotion/components/InfographicScene.tsx | 236 + .../components/IsometricMockupScene.tsx | 666 + apps/web/remotion/components/Scene.tsx | 84 + apps/web/remotion/components/SceneRouter.tsx | 107 + apps/web/remotion/components/SponsorSlot.tsx | 148 + apps/web/remotion/compositions/MainVideo.tsx | 114 + apps/web/remotion/compositions/ShortVideo.tsx | 107 + apps/web/remotion/constants.ts | 169 + apps/web/remotion/entry.ts | 4 + apps/web/remotion/index.ts | 33 + apps/web/remotion/types.ts | 190 + apps/web/sanity/lib/api.ts | 33 + apps/web/sanity/lib/client.ts | 23 + apps/web/sanity/lib/demo.ts | 59 + apps/web/sanity/lib/image.ts | 9 + apps/web/sanity/lib/live.ts | 91 + apps/web/sanity/lib/queries.ts | 300 + apps/web/sanity/lib/resolveHref.ts | 17 + apps/web/sanity/lib/token.ts | 9 + apps/web/sanity/lib/utils.ts | 31 + apps/web/sanity/types.ts | 8345 +++++ apps/web/scripts/check-cache.ts | 270 + apps/web/scripts/migration/README.md | 203 + .../web/scripts/migration/cleanup-orphans.mjs | 165 + apps/web/scripts/migration/env-example.txt | 14 + apps/web/scripts/migration/migrate.mjs | 1019 + .../scripts/migration/migration-output.log | 53 + apps/web/scripts/migration/package.json | 23 + apps/web/scripts/migration/phase4-output.log | 941 + apps/web/scripts/sanity-post.js | 356 + apps/web/scripts/sanity-update-posts.ts | 28 + apps/web/src/components/Avatar.astro | 35 - apps/web/src/components/Badge.astro | 37 - apps/web/src/components/Button.astro | 39 - apps/web/src/components/Container.astro | 19 - apps/web/src/components/ContentCard.astro | 62 - .../src/components/ContentCardCompact.astro | 46 - .../src/components/ContentCardFeatured.astro | 77 - apps/web/src/components/DraftModeToggle.astro | 51 - apps/web/src/components/Footer.astro | 148 - apps/web/src/components/Header.astro | 258 - apps/web/src/components/Pagination.astro | 56 - apps/web/src/components/PersonDetail.astro | 143 - apps/web/src/components/SocialMeta.astro | 58 - apps/web/src/components/Tag.astro | 23 - apps/web/src/components/ThemeScript.astro | 21 - apps/web/src/components/ThemeToggle.astro | 46 - .../src/components/course/LessonPlayer.tsx | 223 - apps/web/src/env.d.ts | 17 - apps/web/src/layouts/BaseLayout.astro | 75 - apps/web/src/lib/auth-client.ts | 7 - apps/web/src/lib/auth.ts | 32 - apps/web/src/lib/db.ts | 5 - apps/web/src/lib/og-utils.ts | 429 - apps/web/src/lib/queries.ts | 328 - apps/web/src/lib/schema.ts | 67 - apps/web/src/lib/youtube.ts | 23 - apps/web/src/middleware.ts | 99 - apps/web/src/pages/404.astro | 27 - apps/web/src/pages/[slug].astro | 69 - apps/web/src/pages/api/auth/[...all].ts | 14 - .../src/pages/api/draft-mode/allow-enable.ts | 29 - apps/web/src/pages/api/draft-mode/disable.ts | 21 - apps/web/src/pages/api/draft-mode/enable.ts | 118 - apps/web/src/pages/api/og/blog.png.ts | 48 - apps/web/src/pages/api/og/course.png.ts | 48 - apps/web/src/pages/api/og/default.png.ts | 46 - apps/web/src/pages/api/og/podcast.png.ts | 50 - apps/web/src/pages/author/[slug].astro | 34 - apps/web/src/pages/authors.astro | 60 - apps/web/src/pages/blog.astro | 85 - apps/web/src/pages/course/[slug].astro | 174 - .../course/[slug]/lesson/[lessonSlug].astro | 94 - apps/web/src/pages/courses.astro | 82 - apps/web/src/pages/courses/rss.xml.ts | 42 - apps/web/src/pages/dashboard/index.astro | 40 - apps/web/src/pages/feeds.astro | 72 - apps/web/src/pages/guest/[slug].astro | 34 - apps/web/src/pages/guests.astro | 60 - apps/web/src/pages/index.astro | 180 - apps/web/src/pages/login.astro | 35 - .../podcast/[podcastTypeSlug]/rss.xml.ts | 66 - apps/web/src/pages/podcast/[slug].astro | 296 - apps/web/src/pages/podcasts.astro | 86 - apps/web/src/pages/post/[slug].astro | 165 - apps/web/src/pages/rss.xml.ts | 38 - apps/web/src/pages/sitemap.xml.ts | 79 - apps/web/src/pages/sponsor/[slug].astro | 34 - apps/web/src/pages/sponsors.astro | 60 - apps/web/src/scripts/visual-editing.ts | 87 - apps/web/src/styles/global.css | 289 - apps/web/src/utils/sanity.ts | 164 - apps/web/src/utils/xml.ts | 16 - apps/web/supabase/.gitkeep | 0 apps/web/supabase/.temp/cli-latest | 1 + apps/web/supabase/.temp/gotrue-version | 1 + apps/web/supabase/.temp/pooler-url | 1 + apps/web/supabase/.temp/postgres-version | 1 + apps/web/supabase/.temp/project-ref | 1 + apps/web/supabase/.temp/rest-version | 1 + apps/web/supabase/.temp/storage-migration | 1 + apps/web/supabase/.temp/storage-version | 1 + apps/web/supabase/config.toml | 54 + .../supabase/migrations/001_youtube_stats.sql | 50 + .../migrations/002_cron_schedules.sql | 141 + .../20250101000000_create_dashboard_users.sql | 41 + apps/web/tsconfig.json | 43 +- apps/web/vercel.json | 3 + apps/web/wrangler.toml | 53 - package.json | 20 +- pnpm-lock.yaml | 25527 +++++++++++----- pnpm-workspace.yaml | 12 + skills-lock.json | 41 + 650 files changed, 113022 insertions(+), 34392 deletions(-) create mode 100644 .agents/skills/content-modeling-best-practices/SKILL.md create mode 100644 .agents/skills/content-modeling-best-practices/references/content-reuse.md create mode 100644 .agents/skills/content-modeling-best-practices/references/reference-vs-embedding.md create mode 100644 .agents/skills/content-modeling-best-practices/references/separation-of-concerns.md create mode 100644 .agents/skills/content-modeling-best-practices/references/taxonomy-classification.md create mode 100644 .agents/skills/portable-text-conversion/SKILL.md create mode 100644 .agents/skills/portable-text-conversion/rules/html-to-pt.md create mode 100644 .agents/skills/portable-text-conversion/rules/manual-construction.md create mode 100644 .agents/skills/portable-text-conversion/rules/markdown-to-pt.md create mode 100644 .agents/skills/portable-text-serialization/SKILL.md create mode 100644 .agents/skills/portable-text-serialization/rules/astro.md create mode 100644 .agents/skills/portable-text-serialization/rules/html.md create mode 100644 .agents/skills/portable-text-serialization/rules/markdown.md create mode 100644 .agents/skills/portable-text-serialization/rules/plain-text.md create mode 100644 .agents/skills/portable-text-serialization/rules/react.md create mode 100644 .agents/skills/portable-text-serialization/rules/svelte.md create mode 100644 .agents/skills/portable-text-serialization/rules/vue.md create mode 100644 .agents/skills/sanity-best-practices/SKILL.md create mode 100644 .agents/skills/sanity-best-practices/references/angular.md create mode 100644 .agents/skills/sanity-best-practices/references/app-sdk.md create mode 100644 .agents/skills/sanity-best-practices/references/astro.md create mode 100644 .agents/skills/sanity-best-practices/references/blueprints.md create mode 100644 .agents/skills/sanity-best-practices/references/functions.md create mode 100644 .agents/skills/sanity-best-practices/references/get-started.md create mode 100644 .agents/skills/sanity-best-practices/references/groq.md create mode 100644 .agents/skills/sanity-best-practices/references/hydrogen.md create mode 100644 .agents/skills/sanity-best-practices/references/image.md create mode 100644 .agents/skills/sanity-best-practices/references/localization.md create mode 100644 .agents/skills/sanity-best-practices/references/migration-html-import.md create mode 100644 .agents/skills/sanity-best-practices/references/migration.md create mode 100644 .agents/skills/sanity-best-practices/references/nextjs.md create mode 100644 .agents/skills/sanity-best-practices/references/nuxt.md create mode 100644 .agents/skills/sanity-best-practices/references/page-builder.md create mode 100644 .agents/skills/sanity-best-practices/references/portable-text.md create mode 100644 .agents/skills/sanity-best-practices/references/project-structure.md create mode 100644 .agents/skills/sanity-best-practices/references/remix.md create mode 100644 .agents/skills/sanity-best-practices/references/schema.md create mode 100644 .agents/skills/sanity-best-practices/references/seo.md create mode 100644 .agents/skills/sanity-best-practices/references/studio-structure.md create mode 100644 .agents/skills/sanity-best-practices/references/svelte.md create mode 100644 .agents/skills/sanity-best-practices/references/typegen.md create mode 100644 .agents/skills/sanity-best-practices/references/visual-editing.md create mode 100644 .agents/skills/sanity-live-cache-components/SKILL.md create mode 100644 .agents/skills/sanity-live-cache-components/reference/dynamic-segments.md create mode 100644 .agents/skills/sanity-live-cache-components/reference/layouts.md create mode 100644 .agents/skills/sanity-live-cache-components/reference/live-helpers.md create mode 100644 .agents/skills/sanity-live-cache-components/reference/three-layer-pattern.md create mode 100644 .agents/skills/seo-aeo-best-practices/SKILL.md create mode 100644 .agents/skills/seo-aeo-best-practices/references/aeo-considerations.md create mode 100644 .agents/skills/seo-aeo-best-practices/references/eeat-principles.md create mode 100644 .agents/skills/seo-aeo-best-practices/references/structured-data.md create mode 100644 .agents/skills/seo-aeo-best-practices/references/technical-seo.md create mode 100644 .github/.branch-cleanup create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/deploy-workers.yml create mode 100644 .nvmrc create mode 100644 CLAUDE.md create mode 100644 apps/sanity/extract.json create mode 100644 apps/sanity/functions/syndicate-content/index.ts create mode 100644 apps/sanity/plugins/podcast-rss/README.md create mode 100644 apps/sanity/plugins/podcast-rss/components/EpisodePicker.tsx create mode 100644 apps/sanity/plugins/podcast-rss/components/Popup.tsx create mode 100644 apps/sanity/plugins/podcast-rss/components/SearchBar.tsx create mode 100644 apps/sanity/plugins/podcast-rss/components/SearchResults.tsx create mode 100644 apps/sanity/plugins/podcast-rss/constants.ts create mode 100644 apps/sanity/plugins/podcast-rss/hooks/useDebouncedCallback.ts create mode 100644 apps/sanity/plugins/podcast-rss/hooks/useQuery.ts create mode 100644 apps/sanity/plugins/podcast-rss/index.ts create mode 100644 apps/sanity/plugins/podcast-rss/schema/podcastRssEpisode.ts create mode 100644 apps/sanity/plugins/podcast-rss/types.ts create mode 100644 apps/sanity/plugins/podcast-rss/utils/rss.ts create mode 100644 apps/sanity/sanity.blueprint.ts create mode 100644 apps/sanity/schemas/documents/syndication.ts create mode 100644 apps/site/.dev.vars.example create mode 100644 apps/site/.gitignore create mode 100644 apps/site/astro.config.mjs create mode 100644 apps/site/baseline/blog_rss.json create mode 100644 apps/site/baseline/blog_rss.xml create mode 100644 apps/site/baseline/podcasts_rss.json create mode 100644 apps/site/baseline/podcasts_rss.xml create mode 100644 apps/site/baseline/robots.txt create mode 100644 apps/site/baseline/sitemap.xml create mode 100644 apps/site/biome.json create mode 100644 apps/site/package.json rename apps/{web => site}/public/.assetsignore (100%) create mode 100644 apps/site/public/ads.txt create mode 100644 apps/site/public/apple-touch-icon.png create mode 100644 apps/site/public/favicon-16x16.png create mode 100644 apps/site/public/favicon-32x32.png create mode 100644 apps/site/public/favicon.ico create mode 100644 apps/site/public/icon.png create mode 100644 apps/site/public/icon.svg rename apps/{web => site}/src/assets/fonts/Inter-Bold.ttf (100%) rename apps/{web => site}/src/assets/fonts/Inter-Regular.ttf (100%) create mode 100644 apps/site/src/components/AnimatedHero.astro create mode 100644 apps/site/src/components/Avatar.astro create mode 100644 apps/site/src/components/Breadcrumbs.astro create mode 100644 apps/site/src/components/Container.astro create mode 100644 apps/site/src/components/ContentCard.astro create mode 100644 apps/site/src/components/ContentGrid.astro create mode 100644 apps/site/src/components/CoverImage.astro create mode 100644 apps/site/src/components/CoverMedia.astro create mode 100644 apps/site/src/components/DraftModeToggle.astro create mode 100644 apps/site/src/components/Footer.astro create mode 100644 apps/site/src/components/FormattedDate.astro create mode 100644 apps/site/src/components/Header.astro create mode 100644 apps/site/src/components/HomeSection.astro create mode 100644 apps/site/src/components/JsonLd.astro create mode 100644 apps/site/src/components/NavLink.astro create mode 100644 apps/site/src/components/Pagination.astro create mode 100644 apps/site/src/components/PodcastListen.astro create mode 100644 apps/site/src/components/PodcastPicks.astro create mode 100644 apps/site/src/components/PodmatchBadge.astro create mode 100644 apps/site/src/components/SearchModal.astro create mode 100644 apps/site/src/components/SectionHeader.astro create mode 100644 apps/site/src/components/SocialMeta.astro create mode 100644 apps/site/src/components/SponsorCard.astro create mode 100644 apps/site/src/components/ThemeScript.astro create mode 100644 apps/site/src/components/ThemeToggle.astro create mode 100644 apps/site/src/components/UserSocials.astro create mode 100644 apps/site/src/components/icons/AjHeadphones.astro create mode 100644 apps/site/src/components/icons/AjPrimaryAlt.astro create mode 100644 apps/site/src/components/portable-text/PortableTextContent.astro create mode 100644 apps/site/src/components/portable-text/PtCode.astro create mode 100644 apps/site/src/components/portable-text/PtCodePen.astro create mode 100644 apps/site/src/components/portable-text/PtCodeSandbox.astro create mode 100644 apps/site/src/components/portable-text/PtHeading5.astro create mode 100644 apps/site/src/components/portable-text/PtHeading6.astro create mode 100644 apps/site/src/components/portable-text/PtHtml.astro create mode 100644 apps/site/src/components/portable-text/PtImage.astro create mode 100644 apps/site/src/components/portable-text/PtInternalLink.astro create mode 100644 apps/site/src/components/portable-text/PtLink.astro create mode 100644 apps/site/src/components/portable-text/PtQuote.astro create mode 100644 apps/site/src/components/portable-text/PtTable.astro create mode 100644 apps/site/src/components/portable-text/PtTweet.astro create mode 100644 apps/site/src/components/portable-text/PtYouTube.astro create mode 100644 apps/site/src/components/portable-text/PtYouTubeShorts.astro create mode 100644 apps/site/src/env.d.ts create mode 100644 apps/site/src/layouts/BaseLayout.astro create mode 100644 apps/site/src/lib/code-languages.ts create mode 100644 apps/site/src/lib/content.ts create mode 100644 apps/site/src/lib/feeds.ts create mode 100644 apps/site/src/lib/hero-icons.ts create mode 100644 apps/site/src/lib/og-utils.ts create mode 100644 apps/site/src/lib/og.ts create mode 100644 apps/site/src/lib/pagination.ts create mode 100644 apps/site/src/lib/portable-text-html.ts create mode 100644 apps/site/src/lib/sanity/client.ts create mode 100644 apps/site/src/lib/sanity/config.ts create mode 100644 apps/site/src/lib/sanity/context.ts create mode 100644 apps/site/src/lib/sanity/image.ts create mode 100644 apps/site/src/lib/sanity/load-query.ts create mode 100644 apps/site/src/lib/sanity/preview.ts create mode 100644 apps/site/src/lib/sanity/queries/feeds.ts create mode 100644 apps/site/src/lib/sanity/queries/index.ts create mode 100644 apps/site/src/lib/sanity/queries/pages.ts create mode 100644 apps/site/src/lib/sanity/queries/people.ts create mode 100644 apps/site/src/lib/sanity/queries/podcasts.ts create mode 100644 apps/site/src/lib/sanity/queries/posts.ts create mode 100644 apps/site/src/lib/sanity/queries/search.ts create mode 100644 apps/site/src/lib/sanity/queries/shared.ts create mode 100644 apps/site/src/lib/sanity/queries/sponsors.ts create mode 100644 apps/site/src/lib/sanity/resolve-href.ts create mode 100644 apps/site/src/lib/site.ts create mode 100644 apps/site/src/lib/social-icons.ts create mode 100644 apps/site/src/lib/structured-data.ts create mode 100644 apps/site/src/lib/xml.ts create mode 100644 apps/site/src/lib/youtube.ts create mode 100644 apps/site/src/middleware.ts create mode 100644 apps/site/src/pages/404.astro create mode 100644 apps/site/src/pages/[slug].astro create mode 100644 apps/site/src/pages/api/draft-mode/disable.ts create mode 100644 apps/site/src/pages/api/draft-mode/enable.ts create mode 100644 apps/site/src/pages/api/og/blog.png.ts create mode 100644 apps/site/src/pages/api/og/default.png.ts create mode 100644 apps/site/src/pages/api/og/person.png.ts create mode 100644 apps/site/src/pages/api/og/podcast.png.ts create mode 100644 apps/site/src/pages/api/search.ts create mode 100644 apps/site/src/pages/api/sponsorship.ts create mode 100644 apps/site/src/pages/author/[slug].astro create mode 100644 apps/site/src/pages/authors/index.astro create mode 100644 apps/site/src/pages/authors/page/[num].astro create mode 100644 apps/site/src/pages/authors/page/index.astro create mode 100644 apps/site/src/pages/blog/index.astro create mode 100644 apps/site/src/pages/blog/page/[num].astro create mode 100644 apps/site/src/pages/blog/page/index.astro create mode 100644 apps/site/src/pages/blog/rss.json.ts create mode 100644 apps/site/src/pages/blog/rss.xml.ts create mode 100644 apps/site/src/pages/guest/[slug].astro create mode 100644 apps/site/src/pages/guests/index.astro create mode 100644 apps/site/src/pages/guests/page/[num].astro create mode 100644 apps/site/src/pages/guests/page/index.astro create mode 100644 apps/site/src/pages/index.astro create mode 100644 apps/site/src/pages/podcast/[slug].astro create mode 100644 apps/site/src/pages/podcasts/index.astro create mode 100644 apps/site/src/pages/podcasts/page/[num].astro create mode 100644 apps/site/src/pages/podcasts/page/index.astro create mode 100644 apps/site/src/pages/podcasts/rss.json.ts create mode 100644 apps/site/src/pages/podcasts/rss.xml.ts create mode 100644 apps/site/src/pages/post/[slug].astro create mode 100644 apps/site/src/pages/robots.txt.ts create mode 100644 apps/site/src/pages/search.astro create mode 100644 apps/site/src/pages/sitemap.xml.ts create mode 100644 apps/site/src/pages/sponsor/[slug].astro create mode 100644 apps/site/src/pages/sponsors/index.astro create mode 100644 apps/site/src/pages/sponsors/page/[num].astro create mode 100644 apps/site/src/pages/sponsors/page/index.astro create mode 100644 apps/site/src/pages/sponsorships.astro create mode 100644 apps/site/src/styles/global.css create mode 100644 apps/site/tests/README.md create mode 100644 apps/site/tests/cf-mock.mjs create mode 100644 apps/site/tests/font-bold-mock.mjs create mode 100644 apps/site/tests/font-reg-mock.mjs create mode 100644 apps/site/tests/helpers.mjs create mode 100755 apps/site/tests/run-e2e.mjs create mode 100644 apps/site/tests/setup.mjs create mode 100644 apps/site/tests/tier1-features.test.mjs create mode 100644 apps/site/tests/tier2-boundaries.test.mjs create mode 100644 apps/site/tests/tier3-combinations.test.mjs create mode 100644 apps/site/tests/tier4-baseline-contract.test.mjs create mode 100644 apps/site/tsconfig.json create mode 100644 apps/site/wrangler.jsonc delete mode 100644 apps/web/.dev.vars.example create mode 100644 apps/web/.eslintignore create mode 100644 apps/web/.prettierignore delete mode 100644 apps/web/README.md create mode 100644 apps/web/app/(dashboard)/dashboard/actions.ts create mode 100644 apps/web/app/(dashboard)/dashboard/auth/callback/route.ts create mode 100644 apps/web/app/(dashboard)/dashboard/config/config-form.tsx create mode 100644 apps/web/app/(dashboard)/dashboard/config/page.tsx create mode 100644 apps/web/app/(dashboard)/dashboard/content/actions.ts create mode 100644 apps/web/app/(dashboard)/dashboard/content/content-ideas-table.tsx create mode 100644 apps/web/app/(dashboard)/dashboard/content/page.tsx create mode 100644 apps/web/app/(dashboard)/dashboard/login/page.tsx create mode 100644 apps/web/app/(dashboard)/dashboard/page.tsx create mode 100644 apps/web/app/(dashboard)/dashboard/pipeline/page.tsx create mode 100644 apps/web/app/(dashboard)/dashboard/review/[id]/page.tsx create mode 100644 apps/web/app/(dashboard)/dashboard/review/[id]/review-detail-client.tsx create mode 100644 apps/web/app/(dashboard)/dashboard/review/actions.ts create mode 100644 apps/web/app/(dashboard)/dashboard/review/page.tsx create mode 100644 apps/web/app/(dashboard)/dashboard/settings/page.tsx create mode 100644 apps/web/app/(dashboard)/dashboard/settings/settings-form.tsx create mode 100644 apps/web/app/(dashboard)/dashboard/sponsors/actions.ts create mode 100644 apps/web/app/(dashboard)/dashboard/sponsors/page.tsx create mode 100644 apps/web/app/(dashboard)/dashboard/sponsors/sponsor-leads-table.tsx create mode 100644 apps/web/app/(dashboard)/dashboard/sponsors/sponsor-pool-table.tsx create mode 100644 apps/web/app/(dashboard)/dashboard/videos/actions.ts create mode 100644 apps/web/app/(dashboard)/dashboard/videos/page.tsx create mode 100644 apps/web/app/(dashboard)/dashboard/videos/videos-table.tsx create mode 100644 apps/web/app/(dashboard)/layout.tsx create mode 100644 apps/web/app/(main)/(author)/author/[slug]/page.tsx create mode 100644 apps/web/app/(main)/(author)/authors/page.tsx create mode 100644 apps/web/app/(main)/(author)/authors/page/[num]/page.tsx create mode 100644 apps/web/app/(main)/(author)/authors/page/page.tsx create mode 100644 apps/web/app/(main)/(guest)/guest/[slug]/page.tsx create mode 100644 apps/web/app/(main)/(guest)/guests/page.tsx create mode 100644 apps/web/app/(main)/(guest)/guests/page/[num]/page.tsx create mode 100644 apps/web/app/(main)/(guest)/guests/page/page.tsx create mode 100644 apps/web/app/(main)/(podcast)/podcast/Podcast.tsx create mode 100644 apps/web/app/(main)/(podcast)/podcast/[slug]/page.tsx create mode 100644 apps/web/app/(main)/(podcast)/podcast/picks.tsx create mode 100644 apps/web/app/(main)/(podcast)/podcast/preview/[token]/page.tsx create mode 100644 apps/web/app/(main)/(podcast)/podcasts/page.tsx create mode 100644 apps/web/app/(main)/(podcast)/podcasts/page/[num]/page.tsx create mode 100644 apps/web/app/(main)/(podcast)/podcasts/page/page.tsx create mode 100644 apps/web/app/(main)/(podcast)/podcasts/rss.json/route.ts create mode 100644 apps/web/app/(main)/(podcast)/podcasts/rss.xml/route.ts create mode 100644 apps/web/app/(main)/(post)/blog/page.tsx create mode 100644 apps/web/app/(main)/(post)/blog/page/[num]/page.tsx create mode 100644 apps/web/app/(main)/(post)/blog/page/page.tsx create mode 100644 apps/web/app/(main)/(post)/blog/rss.json/route.ts create mode 100644 apps/web/app/(main)/(post)/blog/rss.xml/route.ts create mode 100644 apps/web/app/(main)/(post)/post/[slug]/page.tsx create mode 100644 apps/web/app/(main)/(sponsor)/sponsor/[slug]/page.tsx create mode 100644 apps/web/app/(main)/(sponsor)/sponsors/page.tsx create mode 100644 apps/web/app/(main)/(sponsor)/sponsors/page/[num]/page.tsx create mode 100644 apps/web/app/(main)/(sponsor)/sponsors/page/page.tsx create mode 100644 apps/web/app/(main)/(top-level-pages)/[slug]/page.tsx create mode 100644 apps/web/app/(main)/(top-level-pages)/pro/page.tsx create mode 100644 apps/web/app/(main)/(top-level-pages)/search/page.tsx create mode 100644 apps/web/app/(main)/(top-level-pages)/sponsorships/page.tsx create mode 100644 apps/web/app/(main)/(top-level-pages)/sponsorships/sponsorship-form.tsx create mode 100644 apps/web/app/(main)/icon.svg create mode 100644 apps/web/app/(main)/layout.tsx create mode 100644 apps/web/app/(main)/meta-pixel.tsx create mode 100644 apps/web/app/(main)/page.tsx create mode 100644 apps/web/app/(sponsor-portal)/layout.tsx create mode 100644 apps/web/app/(sponsor-portal)/portal/page.tsx create mode 100644 apps/web/app/api/algolia/route.tsx create mode 100644 apps/web/app/api/auth/logout/route.tsx create mode 100644 apps/web/app/api/auth/session/route.tsx create mode 100644 apps/web/app/api/auth/session/verify/route.tsx create mode 100644 apps/web/app/api/cron/check-renders/route.ts create mode 100644 apps/web/app/api/cron/check-research/route.ts create mode 100644 apps/web/app/api/cron/ingest/route.ts create mode 100644 apps/web/app/api/cron/route.tsx create mode 100644 apps/web/app/api/cron/sponsor-outreach/route.ts create mode 100644 apps/web/app/api/dashboard/activity/route.ts create mode 100644 apps/web/app/api/dashboard/config/route.ts create mode 100644 apps/web/app/api/dashboard/metrics/route.ts create mode 100644 apps/web/app/api/dashboard/pipeline/route.ts create mode 100644 apps/web/app/api/dashboard/review/route.ts create mode 100644 apps/web/app/api/dashboard/settings/route.ts create mode 100644 apps/web/app/api/devto/route.tsx create mode 100644 apps/web/app/api/draft-mode/disable/route.tsx create mode 100644 apps/web/app/api/draft-mode/enable/route.tsx create mode 100644 apps/web/app/api/generate-preview-token/route.ts create mode 100644 apps/web/app/api/get-preview-content/route.ts create mode 100644 apps/web/app/api/hashnode/route.tsx create mode 100644 apps/web/app/api/sponsor/opt-out/route.ts create mode 100644 apps/web/app/api/sponsorship/route.ts create mode 100644 apps/web/app/api/sponsorship/sponsorship-template.tsx create mode 100644 apps/web/app/api/verify-turnstile.ts create mode 100644 apps/web/app/api/webhooks/sanity-content/route.ts create mode 100644 apps/web/app/api/webhooks/sanity-distribute/route.ts create mode 100644 apps/web/app/api/webhooks/sanity-revalidate/route.ts create mode 100644 apps/web/app/api/webhooks/sponsor-inbound/route.ts create mode 100644 apps/web/app/api/webhooks/stripe-sponsor/route.ts create mode 100644 apps/web/app/api/youtube/rss.xml/route.tsx create mode 100644 apps/web/app/api/youtube/views/route.tsx create mode 100644 apps/web/app/globals.css create mode 100644 apps/web/app/robots.ts create mode 100644 apps/web/app/sitemap.ts delete mode 100644 apps/web/astro.config.mjs create mode 100644 apps/web/biome.json create mode 100644 apps/web/cache-report.json create mode 100644 apps/web/components.json create mode 100644 apps/web/components/algolia-dialog.tsx create mode 100644 apps/web/components/algolia-search.tsx create mode 100644 apps/web/components/analytics.tsx create mode 100644 apps/web/components/animated-hero.tsx create mode 100644 apps/web/components/app-sidebar.tsx create mode 100644 apps/web/components/avatar.tsx create mode 100644 apps/web/components/badge-pro.tsx create mode 100644 apps/web/components/become-sponsor-popup.tsx create mode 100644 apps/web/components/block-code-btn.tsx create mode 100644 apps/web/components/block-code.tsx create mode 100644 apps/web/components/block-image.tsx create mode 100644 apps/web/components/block-table.tsx create mode 100644 apps/web/components/bookmark.tsx create mode 100644 apps/web/components/breadrumb-links.tsx create mode 100644 apps/web/components/cloudflare-turnstile.tsx create mode 100644 apps/web/components/codepen-embed.tsx create mode 100644 apps/web/components/codesandbox-embed.tsx create mode 100644 apps/web/components/cookies-provider-client.tsx create mode 100644 apps/web/components/cookies-provider.tsx create mode 100644 apps/web/components/cover-image.tsx create mode 100644 apps/web/components/cover-media.tsx create mode 100644 apps/web/components/cover-video.tsx create mode 100644 apps/web/components/date.tsx create mode 100644 apps/web/components/disable-draft-mode.tsx create mode 100644 apps/web/components/footer.tsx create mode 100644 apps/web/components/google-ad-banner.tsx create mode 100644 apps/web/components/html-embed.tsx create mode 100644 apps/web/components/icons.tsx create mode 100644 apps/web/components/icons/aj-alt.tsx create mode 100644 apps/web/components/icons/aj-dark.tsx create mode 100644 apps/web/components/icons/aj-headphones-alt.tsx create mode 100644 apps/web/components/icons/aj-headphones.tsx create mode 100644 apps/web/components/icons/aj-light.tsx create mode 100644 apps/web/components/icons/aj-primary-alt.tsx create mode 100644 apps/web/components/icons/aj-primary.tsx create mode 100644 apps/web/components/icons/checkout-arrow.tsx create mode 100644 apps/web/components/icons/dribble.svg create mode 100644 apps/web/components/json-ld.tsx create mode 100644 apps/web/components/mode-toggle.tsx create mode 100644 apps/web/components/more-content.tsx create mode 100644 apps/web/components/more-header.tsx create mode 100644 apps/web/components/nav-header.tsx create mode 100644 apps/web/components/nav-link.tsx create mode 100644 apps/web/components/nav-main.tsx create mode 100644 apps/web/components/nav-user.tsx create mode 100644 apps/web/components/onboarding.tsx create mode 100644 apps/web/components/page-refresh-button.tsx create mode 100644 apps/web/components/paginate-list.tsx create mode 100644 apps/web/components/pipeline-status.tsx create mode 100644 apps/web/components/player-context.tsx create mode 100644 apps/web/components/player-floating.tsx create mode 100644 apps/web/components/player-play-button.tsx create mode 100644 apps/web/components/podcast-open-apple.tsx create mode 100644 apps/web/components/podcast-open-spotify.tsx create mode 100644 apps/web/components/podcast-open-youtube.tsx create mode 100644 apps/web/components/podmatch-badge.tsx create mode 100644 apps/web/components/portable-text.tsx create mode 100644 apps/web/components/pro-benefits.tsx create mode 100644 apps/web/components/quote-embed.tsx create mode 100644 apps/web/components/recent-activity.tsx create mode 100644 apps/web/components/section-cards-live.tsx create mode 100644 apps/web/components/section-cards.tsx create mode 100644 apps/web/components/site-header.tsx create mode 100644 apps/web/components/sponsor-card.tsx create mode 100644 apps/web/components/theme-provider.tsx create mode 100644 apps/web/components/twitter-embed.tsx create mode 100644 apps/web/components/ui/accordion.tsx create mode 100644 apps/web/components/ui/alert-dialog.tsx create mode 100644 apps/web/components/ui/alert.tsx create mode 100644 apps/web/components/ui/aspect-ratio.tsx create mode 100644 apps/web/components/ui/avatar.tsx create mode 100644 apps/web/components/ui/badge.tsx create mode 100644 apps/web/components/ui/breadcrumb.tsx create mode 100644 apps/web/components/ui/button.tsx create mode 100644 apps/web/components/ui/calendar.tsx create mode 100644 apps/web/components/ui/card.tsx create mode 100644 apps/web/components/ui/carousel.tsx create mode 100644 apps/web/components/ui/chart.tsx create mode 100644 apps/web/components/ui/checkbox.tsx create mode 100644 apps/web/components/ui/collapsible.tsx create mode 100644 apps/web/components/ui/command.tsx create mode 100644 apps/web/components/ui/context-menu.tsx create mode 100644 apps/web/components/ui/dialog.tsx create mode 100644 apps/web/components/ui/drawer.tsx create mode 100644 apps/web/components/ui/dropdown-menu.tsx create mode 100644 apps/web/components/ui/form.tsx create mode 100644 apps/web/components/ui/hover-card.tsx create mode 100644 apps/web/components/ui/input-otp.tsx create mode 100644 apps/web/components/ui/input.tsx create mode 100644 apps/web/components/ui/label.tsx create mode 100644 apps/web/components/ui/menubar.tsx create mode 100644 apps/web/components/ui/navigation-menu.tsx create mode 100644 apps/web/components/ui/pagination.tsx create mode 100644 apps/web/components/ui/popover.tsx create mode 100644 apps/web/components/ui/progress.tsx create mode 100644 apps/web/components/ui/radio-group.tsx create mode 100644 apps/web/components/ui/resizable.tsx create mode 100644 apps/web/components/ui/scroll-area.tsx create mode 100644 apps/web/components/ui/select.tsx create mode 100644 apps/web/components/ui/separator.tsx create mode 100644 apps/web/components/ui/sheet.tsx create mode 100644 apps/web/components/ui/sidebar.tsx create mode 100644 apps/web/components/ui/skeleton.tsx create mode 100644 apps/web/components/ui/slider.tsx create mode 100644 apps/web/components/ui/sonner.tsx create mode 100644 apps/web/components/ui/switch.tsx create mode 100644 apps/web/components/ui/table.tsx create mode 100644 apps/web/components/ui/tabs.tsx create mode 100644 apps/web/components/ui/textarea.tsx create mode 100644 apps/web/components/ui/toggle-group.tsx create mode 100644 apps/web/components/ui/toggle.tsx create mode 100644 apps/web/components/ui/tooltip.tsx create mode 100644 apps/web/components/user-buy.tsx create mode 100644 apps/web/components/user-go-pro-button.tsx create mode 100644 apps/web/components/user-go-pro.tsx create mode 100644 apps/web/components/user-related.tsx create mode 100644 apps/web/components/user-socials.tsx create mode 100644 apps/web/components/youtube-embed.tsx create mode 100644 apps/web/components/youtube-short-embed.tsx create mode 100644 apps/web/components/youtube-short.tsx create mode 100644 apps/web/components/youtube-shorts.tsx create mode 100644 apps/web/components/youtube.tsx delete mode 100644 apps/web/drizzle.config.ts create mode 100644 apps/web/e2e/dashboard.spec.ts create mode 100644 apps/web/e2e/smoke.spec.ts create mode 100644 apps/web/hooks/use-mobile.ts create mode 100644 apps/web/lib/block-content-to-markdown.d.ts create mode 100644 apps/web/lib/config.ts create mode 100644 apps/web/lib/gemini-metadata.ts create mode 100644 apps/web/lib/gemini.ts create mode 100644 apps/web/lib/hooks.ts create mode 100644 apps/web/lib/prism.ts create mode 100644 apps/web/lib/resend-notify.ts create mode 100644 apps/web/lib/rss.ts create mode 100644 apps/web/lib/sanity-write-client.ts create mode 100644 apps/web/lib/sanity/dashboard.ts create mode 100644 apps/web/lib/services/elevenlabs.ts create mode 100644 apps/web/lib/services/gcs.ts create mode 100644 apps/web/lib/services/gemini-infographics.ts create mode 100644 apps/web/lib/services/gemini-research.ts create mode 100644 apps/web/lib/services/notebooklm/auth.ts create mode 100644 apps/web/lib/services/notebooklm/client.ts create mode 100644 apps/web/lib/services/notebooklm/index.ts create mode 100644 apps/web/lib/services/notebooklm/rpc.ts create mode 100644 apps/web/lib/services/notebooklm/types.ts create mode 100644 apps/web/lib/services/pexels.ts create mode 100644 apps/web/lib/services/remotion-deploy.ts create mode 100644 apps/web/lib/services/remotion.ts create mode 100644 apps/web/lib/services/research.ts create mode 100644 apps/web/lib/services/sanity-upload.ts create mode 100644 apps/web/lib/services/trend-discovery.ts create mode 100644 apps/web/lib/services/video-pipeline.ts create mode 100644 apps/web/lib/site.ts create mode 100644 apps/web/lib/sponsor/email-service.ts create mode 100644 apps/web/lib/sponsor/gemini-intent.ts create mode 100644 apps/web/lib/sponsor/gemini-outreach.ts create mode 100644 apps/web/lib/sponsor/sponsor-bridge.ts create mode 100644 apps/web/lib/sponsor/stripe-service.ts create mode 100644 apps/web/lib/sponsorship-schema.ts create mode 100644 apps/web/lib/stripe.types.ts create mode 100644 apps/web/lib/structured-data.ts create mode 100644 apps/web/lib/supabase/client.ts create mode 100644 apps/web/lib/supabase/server.ts create mode 100644 apps/web/lib/types.ts create mode 100644 apps/web/lib/types/config.ts create mode 100644 apps/web/lib/types/dashboard.ts create mode 100644 apps/web/lib/types/engine-config.ts rename {lib => apps/web/lib}/types/video-analytics.ts (100%) create mode 100644 apps/web/lib/utils.ts create mode 100644 apps/web/lib/utils/audio-timestamps.ts create mode 100644 apps/web/lib/x-social.ts create mode 100644 apps/web/lib/youtube-stats.ts create mode 100644 apps/web/lib/youtube-upload.ts create mode 100644 apps/web/next-env.d.ts create mode 100644 apps/web/next.config.ts delete mode 100644 apps/web/package-lock.json create mode 100644 apps/web/playwright.config.ts create mode 100644 apps/web/postcss.config.js delete mode 100644 apps/web/postcss.config.mjs create mode 100644 apps/web/proxy.ts create mode 100644 apps/web/public/ads.txt delete mode 100644 apps/web/public/favicon.svg create mode 100644 apps/web/public/sanity-icons/apple-icon.png create mode 100644 apps/web/public/sanity-icons/icon.ico create mode 100644 apps/web/public/sanity-icons/icon.png create mode 100644 apps/web/remotion/Root.tsx create mode 100644 apps/web/remotion/components/CTAScene.tsx create mode 100644 apps/web/remotion/components/CodeMorphScene.tsx create mode 100644 apps/web/remotion/components/ComparisonGridScene.tsx create mode 100644 apps/web/remotion/components/DynamicListScene.tsx create mode 100644 apps/web/remotion/components/HookScene.tsx create mode 100644 apps/web/remotion/components/InfographicScene.tsx create mode 100644 apps/web/remotion/components/IsometricMockupScene.tsx create mode 100644 apps/web/remotion/components/Scene.tsx create mode 100644 apps/web/remotion/components/SceneRouter.tsx create mode 100644 apps/web/remotion/components/SponsorSlot.tsx create mode 100644 apps/web/remotion/compositions/MainVideo.tsx create mode 100644 apps/web/remotion/compositions/ShortVideo.tsx create mode 100644 apps/web/remotion/constants.ts create mode 100644 apps/web/remotion/entry.ts create mode 100644 apps/web/remotion/index.ts create mode 100644 apps/web/remotion/types.ts create mode 100644 apps/web/sanity/lib/api.ts create mode 100644 apps/web/sanity/lib/client.ts create mode 100644 apps/web/sanity/lib/demo.ts create mode 100644 apps/web/sanity/lib/image.ts create mode 100644 apps/web/sanity/lib/live.ts create mode 100644 apps/web/sanity/lib/queries.ts create mode 100644 apps/web/sanity/lib/resolveHref.ts create mode 100644 apps/web/sanity/lib/token.ts create mode 100644 apps/web/sanity/lib/utils.ts create mode 100644 apps/web/sanity/types.ts create mode 100644 apps/web/scripts/check-cache.ts create mode 100644 apps/web/scripts/migration/README.md create mode 100644 apps/web/scripts/migration/cleanup-orphans.mjs create mode 100644 apps/web/scripts/migration/env-example.txt create mode 100644 apps/web/scripts/migration/migrate.mjs create mode 100644 apps/web/scripts/migration/migration-output.log create mode 100644 apps/web/scripts/migration/package.json create mode 100644 apps/web/scripts/migration/phase4-output.log create mode 100644 apps/web/scripts/sanity-post.js create mode 100644 apps/web/scripts/sanity-update-posts.ts delete mode 100644 apps/web/src/components/Avatar.astro delete mode 100644 apps/web/src/components/Badge.astro delete mode 100644 apps/web/src/components/Button.astro delete mode 100644 apps/web/src/components/Container.astro delete mode 100644 apps/web/src/components/ContentCard.astro delete mode 100644 apps/web/src/components/ContentCardCompact.astro delete mode 100644 apps/web/src/components/ContentCardFeatured.astro delete mode 100644 apps/web/src/components/DraftModeToggle.astro delete mode 100644 apps/web/src/components/Footer.astro delete mode 100644 apps/web/src/components/Header.astro delete mode 100644 apps/web/src/components/Pagination.astro delete mode 100644 apps/web/src/components/PersonDetail.astro delete mode 100644 apps/web/src/components/SocialMeta.astro delete mode 100644 apps/web/src/components/Tag.astro delete mode 100644 apps/web/src/components/ThemeScript.astro delete mode 100644 apps/web/src/components/ThemeToggle.astro delete mode 100644 apps/web/src/components/course/LessonPlayer.tsx delete mode 100644 apps/web/src/env.d.ts delete mode 100644 apps/web/src/layouts/BaseLayout.astro delete mode 100644 apps/web/src/lib/auth-client.ts delete mode 100644 apps/web/src/lib/auth.ts delete mode 100644 apps/web/src/lib/db.ts delete mode 100644 apps/web/src/lib/og-utils.ts delete mode 100644 apps/web/src/lib/queries.ts delete mode 100644 apps/web/src/lib/schema.ts delete mode 100644 apps/web/src/lib/youtube.ts delete mode 100644 apps/web/src/middleware.ts delete mode 100644 apps/web/src/pages/404.astro delete mode 100644 apps/web/src/pages/[slug].astro delete mode 100644 apps/web/src/pages/api/auth/[...all].ts delete mode 100644 apps/web/src/pages/api/draft-mode/allow-enable.ts delete mode 100644 apps/web/src/pages/api/draft-mode/disable.ts delete mode 100644 apps/web/src/pages/api/draft-mode/enable.ts delete mode 100644 apps/web/src/pages/api/og/blog.png.ts delete mode 100644 apps/web/src/pages/api/og/course.png.ts delete mode 100644 apps/web/src/pages/api/og/default.png.ts delete mode 100644 apps/web/src/pages/api/og/podcast.png.ts delete mode 100644 apps/web/src/pages/author/[slug].astro delete mode 100644 apps/web/src/pages/authors.astro delete mode 100644 apps/web/src/pages/blog.astro delete mode 100644 apps/web/src/pages/course/[slug].astro delete mode 100644 apps/web/src/pages/course/[slug]/lesson/[lessonSlug].astro delete mode 100644 apps/web/src/pages/courses.astro delete mode 100644 apps/web/src/pages/courses/rss.xml.ts delete mode 100644 apps/web/src/pages/dashboard/index.astro delete mode 100644 apps/web/src/pages/feeds.astro delete mode 100644 apps/web/src/pages/guest/[slug].astro delete mode 100644 apps/web/src/pages/guests.astro delete mode 100644 apps/web/src/pages/index.astro delete mode 100644 apps/web/src/pages/login.astro delete mode 100644 apps/web/src/pages/podcast/[podcastTypeSlug]/rss.xml.ts delete mode 100644 apps/web/src/pages/podcast/[slug].astro delete mode 100644 apps/web/src/pages/podcasts.astro delete mode 100644 apps/web/src/pages/post/[slug].astro delete mode 100644 apps/web/src/pages/rss.xml.ts delete mode 100644 apps/web/src/pages/sitemap.xml.ts delete mode 100644 apps/web/src/pages/sponsor/[slug].astro delete mode 100644 apps/web/src/pages/sponsors.astro delete mode 100644 apps/web/src/scripts/visual-editing.ts delete mode 100644 apps/web/src/styles/global.css delete mode 100644 apps/web/src/utils/sanity.ts delete mode 100644 apps/web/src/utils/xml.ts create mode 100644 apps/web/supabase/.gitkeep create mode 100644 apps/web/supabase/.temp/cli-latest create mode 100644 apps/web/supabase/.temp/gotrue-version create mode 100644 apps/web/supabase/.temp/pooler-url create mode 100644 apps/web/supabase/.temp/postgres-version create mode 100644 apps/web/supabase/.temp/project-ref create mode 100644 apps/web/supabase/.temp/rest-version create mode 100644 apps/web/supabase/.temp/storage-migration create mode 100644 apps/web/supabase/.temp/storage-version create mode 100644 apps/web/supabase/config.toml create mode 100644 apps/web/supabase/migrations/001_youtube_stats.sql create mode 100644 apps/web/supabase/migrations/002_cron_schedules.sql create mode 100644 apps/web/supabase/migrations/20250101000000_create_dashboard_users.sql create mode 100644 apps/web/vercel.json delete mode 100644 apps/web/wrangler.toml create mode 100644 skills-lock.json diff --git a/.agents/skills/content-modeling-best-practices/SKILL.md b/.agents/skills/content-modeling-best-practices/SKILL.md new file mode 100644 index 000000000..98d5a7cf1 --- /dev/null +++ b/.agents/skills/content-modeling-best-practices/SKILL.md @@ -0,0 +1,32 @@ +--- +name: content-modeling-best-practices +description: Structured content modeling guidance for schema design, content architecture, content reuse, references versus embedded objects, separation of concerns, and taxonomies across Sanity and other headless CMSes. Use this skill when designing or refactoring content types, deciding field shapes, debating reusable versus nested content, planning omnichannel content models, or reviewing whether a schema is too page-shaped or presentation-driven. +--- + +# Content Modeling Best Practices + +Principles for designing structured content that's flexible, reusable, and maintainable. These concepts apply to any headless CMS but include Sanity-specific implementation notes. + +## When to Apply + +Reference these guidelines when: +- Starting a new project and designing the content model +- Evaluating whether content should be structured or free-form +- Deciding between references and embedded content +- Planning for multi-channel content delivery +- Refactoring existing content structures + +## Core Principles + +1. **Content is data, not pages** — Structure content for meaning, not presentation +2. **Single source of truth** — Avoid content duplication +3. **Future-proof** — Design for channels that don't exist yet +4. **Editor-centric** — Optimize for the people creating content + +## References + +Start with the reference that matches the modeling decision in front of you, instead of loading every topic at once. See `references/` for detailed guidance on specific topics: +- `references/separation-of-concerns.md` — Separating content from presentation +- `references/reference-vs-embedding.md` — When to use references vs embedded objects +- `references/content-reuse.md` — Content reuse patterns and the reuse spectrum +- `references/taxonomy-classification.md` — Flat, hierarchical, and faceted classification diff --git a/.agents/skills/content-modeling-best-practices/references/content-reuse.md b/.agents/skills/content-modeling-best-practices/references/content-reuse.md new file mode 100644 index 000000000..2f31e0a2d --- /dev/null +++ b/.agents/skills/content-modeling-best-practices/references/content-reuse.md @@ -0,0 +1,134 @@ +# Content Reuse Patterns + +Effective content models maximize reuse while minimizing duplication. Here are patterns for achieving both. + +## The Content Reuse Spectrum + +``` +Full Duplication ←————————————————→ Full Reference +(Copy everything) (Link to one source) +``` + +Most real-world content sits somewhere in between. + +## Pattern 1: Shared Components + +Create reusable content blocks that can be embedded anywhere. + +**Use case:** Testimonials, FAQs, CTAs that appear on multiple pages. + +```typescript +// Standalone testimonial documents +defineType({ + name: 'testimonial', + type: 'document', + fields: [ + defineField({ name: 'quote', type: 'text' }), + defineField({ name: 'author', type: 'string' }), + defineField({ name: 'company', type: 'string' }), + ] +}) + +// Reference in page builders +defineField({ + name: 'pageBuilder', + type: 'array', + of: [ + { type: 'reference', to: [{ type: 'testimonial' }] } + ] +}) +``` + +## Pattern 2: Shared Field Sets + +Extract common fields into reusable definitions. + +**Use case:** SEO fields, social metadata, common dates. + +```typescript +// Shared field definition +export const seoFields = [ + defineField({ name: 'seoTitle', type: 'string' }), + defineField({ name: 'seoDescription', type: 'text' }), + defineField({ name: 'ogImage', type: 'image' }), +] + +// Spread into multiple types +defineType({ + name: 'page', + fields: [ + defineField({ name: 'title', type: 'string' }), + ...seoFields + ] +}) + +defineType({ + name: 'post', + fields: [ + defineField({ name: 'title', type: 'string' }), + ...seoFields + ] +}) +``` + +## Pattern 3: Taxonomy References + +Centralize classification for consistent tagging. + +**Use case:** Categories, tags, topics that span content types. + +```typescript +// Central taxonomy +defineType({ + name: 'category', + type: 'document', + fields: [ + defineField({ name: 'title', type: 'string' }), + defineField({ name: 'slug', type: 'slug' }), + ] +}) + +// Used across content types +defineField({ + name: 'categories', + type: 'array', + of: [{ type: 'reference', to: [{ type: 'category' }] }] +}) +``` + +## Pattern 4: Content Fragments + +Small, reusable pieces that combine into larger content. + +**Use case:** Bios, addresses, contact info. + +```typescript +// Fragment type +defineType({ + name: 'contactInfo', + type: 'object', + fields: [ + defineField({ name: 'email', type: 'email' }), + defineField({ name: 'phone', type: 'string' }), + defineField({ name: 'address', type: 'text' }), + ] +}) + +// Reused across types +defineType({ + name: 'office', + fields: [ + defineField({ name: 'name', type: 'string' }), + defineField({ name: 'contact', type: 'contactInfo' }), + ] +}) +``` + +## Anti-Pattern: Over-Abstraction + +Not everything needs to be reusable. If content is only used in one place, embedding is simpler. + +**Signs of over-abstraction:** +- References that are only used once +- Editors navigating multiple documents for one page +- Complex queries joining rarely-shared content diff --git a/.agents/skills/content-modeling-best-practices/references/reference-vs-embedding.md b/.agents/skills/content-modeling-best-practices/references/reference-vs-embedding.md new file mode 100644 index 000000000..52098a531 --- /dev/null +++ b/.agents/skills/content-modeling-best-practices/references/reference-vs-embedding.md @@ -0,0 +1,89 @@ +# Reference vs Embedding Content + +When should content be linked (referenced) vs copied (embedded)? This decision affects reusability, query complexity, and editing workflows. + +## The Trade-offs + +| Aspect | Reference | Embedded Object | +|--------|-----------|-----------------| +| Reusability | ✅ Shared across documents | ❌ Copied per document | +| Single source | ✅ Update once, reflects everywhere | ❌ Must update each copy | +| Query complexity | Requires joins/expansion | Inline, simpler queries | +| Editing UX | Separate editing interface | All fields in one place | +| Independence | Can exist on its own | Only exists within parent | + +## When to Reference + +Use references when content: +- **Is reusable** — Same author across many articles +- **Needs central management** — Update product info once +- **Has its own lifecycle** — Published/draft independent of parent +- **Should stay in sync** — Price changes reflect everywhere + +**Examples:** +- Author profiles +- Product catalog items +- Shared testimonials +- Category taxonomy +- Reusable CTAs + +## When to Embed + +Use embedded objects when content: +- **Is unique to this document** — Page-specific hero +- **Doesn't make sense alone** — SEO metadata +- **Should be copied, not linked** — Historical snapshot +- **Simplifies editing** — All fields in one form + +**Examples:** +- SEO metadata +- Page-specific sections +- Address information +- Social links +- Configuration options + +## Sanity Implementation + +```typescript +// Reference: Author is reusable +defineField({ + name: 'author', + type: 'reference', + to: [{ type: 'author' }] +}) + +// Embedded: SEO is page-specific +defineField({ + name: 'seo', + type: 'object', + fields: [ + defineField({ name: 'title', type: 'string' }), + defineField({ name: 'description', type: 'text' }) + ] +}) +``` + +## The Hybrid Approach + +Sometimes you want both: a reference for the canonical data, plus embedded overrides. + +```typescript +defineField({ + name: 'featuredProduct', + type: 'object', + fields: [ + defineField({ + name: 'product', + type: 'reference', + to: [{ type: 'product' }] + }), + defineField({ + name: 'overrideTitle', + type: 'string', + description: 'Optional: Override the product title for this context' + }), + ] +}) +``` + +Query uses `coalesce(overrideTitle, product->title)`. diff --git a/.agents/skills/content-modeling-best-practices/references/separation-of-concerns.md b/.agents/skills/content-modeling-best-practices/references/separation-of-concerns.md new file mode 100644 index 000000000..5ce2085a5 --- /dev/null +++ b/.agents/skills/content-modeling-best-practices/references/separation-of-concerns.md @@ -0,0 +1,60 @@ +# Separation of Content and Presentation + +The most important principle in structured content: **separate what content IS from how it LOOKS**. + +## The Problem + +When content is tied to presentation: +- Redesigns require content migration +- Content can't be reused across channels (web, mobile, voice) +- Editors make design decisions instead of content decisions +- A/B testing requires duplicate content + +## The Principle + +Model content based on **meaning and purpose**, not visual appearance. + +### Bad: Presentation-Focused + +``` +BigHeroText → What if we want small heroes? +RedButton → What if brand colors change? +ThreeColumnLayout → What if mobile needs one column? +LeftSidebar → Position is a frontend concern +MobileImage → Device-specific content is fragile +``` + +### Good: Meaning-Focused + +``` +Headline → The main message (render however) +CallToAction → An action we want users to take +Features → A list of things (columns decided by frontend) +RelatedContent → Content relationships (position by context) +Image → One image with responsive crops +``` + +## Testing Your Model + +Ask: "If we completely redesigned the site, would these field names still make sense?" + +- `threeColumnFeatures` → ❌ Fails (what if 2 columns?) +- `features` → ✅ Works (describes the content's purpose: a list of product features) +- `blueHighlightBox` → ❌ Fails (what if we go purple?) +- `callout` → ✅ Works (describes the content's role: an attention-grabbing aside) + +## Sanity Implementation + +```typescript +// ❌ Avoid presentation-focused names +defineField({ name: 'bigHeroText', type: 'string' }) +defineField({ name: 'fontSize', type: 'number' }) +defineField({ name: 'backgroundColor', type: 'color' }) + +// ✅ Use meaning-focused names +defineField({ name: 'headline', type: 'string' }) +defineField({ name: 'emphasis', type: 'string', options: { list: ['standard', 'prominent'] } }) +defineField({ name: 'tone', type: 'string', options: { list: ['neutral', 'warning', 'success'] } }) +``` + +The frontend translates `tone: 'warning'` to visual styles. Content stays semantic. diff --git a/.agents/skills/content-modeling-best-practices/references/taxonomy-classification.md b/.agents/skills/content-modeling-best-practices/references/taxonomy-classification.md new file mode 100644 index 000000000..d8c077d0e --- /dev/null +++ b/.agents/skills/content-modeling-best-practices/references/taxonomy-classification.md @@ -0,0 +1,136 @@ +# Taxonomy and Classification + +Organizing content with taxonomies enables filtering, navigation, and content relationships. Well-designed taxonomies scale; poorly designed ones become maintenance nightmares. + +## Types of Classification + +### Flat Taxonomy +Simple list of terms with no hierarchy. + +**Use for:** Tags, simple categories +**Example:** Blog tags: "javascript", "react", "tutorial" + +```typescript +defineType({ + name: 'tag', + type: 'document', + fields: [ + defineField({ name: 'title', type: 'string' }), + defineField({ name: 'slug', type: 'slug' }), + ] +}) +``` + +### Hierarchical Taxonomy +Terms with parent-child relationships. + +**Use for:** Product categories, content sections +**Example:** Electronics > Phones > Smartphones + +```typescript +defineType({ + name: 'category', + type: 'document', + fields: [ + defineField({ name: 'title', type: 'string' }), + defineField({ name: 'slug', type: 'slug' }), + defineField({ + name: 'parent', + type: 'reference', + to: [{ type: 'category' }], + description: 'Parent category (leave empty for top-level)' + }), + ] +}) +``` + +### Faceted Classification +Multiple independent dimensions. + +**Use for:** Complex filtering (e-commerce) +**Example:** Filter by color AND size AND price range + +```typescript +// Multiple taxonomy types +defineField({ name: 'color', type: 'reference', to: [{ type: 'color' }] }) +defineField({ name: 'size', type: 'reference', to: [{ type: 'size' }] }) +defineField({ name: 'material', type: 'reference', to: [{ type: 'material' }] }) +``` + +## Design Principles + +### 1. Mutual Exclusivity (When Appropriate) +Categories should be distinct. If items frequently belong to multiple categories, consider tags instead. + +**Categories:** One primary classification +**Tags:** Many optional classifications + +### 2. User-Centric Naming +Use terms your audience uses, not internal jargon. + +**Bad:** "Content Assets" (internal term) +**Good:** "Resources" or "Downloads" (user term) + +### 3. Balanced Depth +Too shallow: Everything lumped together +Too deep: Users can't find anything + +**Rule of thumb:** 3-4 levels max for hierarchies + +### 4. Scalable Structure +Design for 10x growth. Will your structure work with 10,000 items? + +## Querying Taxonomies + +### Get all items in a category + +```groq +*[_type == "product" && category._ref == $categoryId] +``` + +### Get items in category OR children + +```groq +// First get all descendant category IDs +*[_type == "product" && category._ref in + *[_type == "category" && ( + _id == $categoryId || + parent._ref == $categoryId || + parent->parent._ref == $categoryId + )]._id +] +``` + +### Get category tree + +```groq +*[_type == "category" && !defined(parent)]{ + title, + slug, + "children": *[_type == "category" && parent._ref == ^._id]{ + title, + slug, + "children": *[_type == "category" && parent._ref == ^._id]{ + title, + slug + } + } +} +``` + +## Common Mistakes + +### Over-categorization +Creating a category for everything results in mostly-empty categories. + +**Fix:** Start minimal, add categories as content grows. + +### Inconsistent Granularity +Some categories broad ("Technology"), others narrow ("React 18 Server Components"). + +**Fix:** Define clear criteria for category creation. + +### No Governance +Anyone can create taxonomy terms, leading to duplicates and inconsistency. + +**Fix:** Limit who can create/edit taxonomy documents. Use validation. diff --git a/.agents/skills/portable-text-conversion/SKILL.md b/.agents/skills/portable-text-conversion/SKILL.md new file mode 100644 index 000000000..996587345 --- /dev/null +++ b/.agents/skills/portable-text-conversion/SKILL.md @@ -0,0 +1,65 @@ +--- +name: portable-text-conversion +description: Convert HTML and Markdown content into Portable Text blocks for Sanity. Use when migrating content from legacy CMSs, importing HTML or Markdown into Sanity, building content pipelines that ingest external content, converting rich text between formats, or programmatically creating Portable Text documents. Covers @portabletext/markdown (markdownToPortableText), @portabletext/block-tools (htmlToBlocks), custom deserializers, and the Portable Text specification for manual block construction. +license: MIT +metadata: + author: sanity + version: "1.0.0" +--- + +# Portable Text Conversion + +Convert external content (HTML, Markdown) into Portable Text for Sanity. Three main approaches: + +1. **`markdownToPortableText`** — Convert Markdown directly using `@portabletext/markdown` (recommended for Markdown) +2. **`htmlToBlocks`** — Parse HTML into PT blocks using `@portabletext/block-tools` (for HTML migration) +3. **Manual construction** — Build PT blocks directly from any source (APIs, databases, etc.) + +## Portable Text Specification + +Understand the target format before converting. PT is an array of blocks: + +```json +[ + { + "_type": "block", + "_key": "abc123", + "style": "normal", + "children": [ + {"_type": "span", "_key": "def456", "text": "Hello ", "marks": []}, + {"_type": "span", "_key": "ghi789", "text": "world", "marks": ["strong"]} + ], + "markDefs": [] + }, + { + "_type": "block", + "_key": "jkl012", + "style": "h2", + "children": [ + {"_type": "span", "_key": "mno345", "text": "A heading", "marks": []} + ], + "markDefs": [] + }, + { + "_type": "image", + "_key": "pqr678", + "asset": {"_type": "reference", "_ref": "image-abc-200x200-png"} + } +] +``` + +**Key rules:** +- Every block and span needs `_key` (unique within the array) +- `_type: "block"` is for text blocks; custom types use their own `_type` +- `markDefs` holds annotation data; `marks` on spans reference `markDefs[*]._key` or are decorator strings +- Lists use `listItem` ("bullet" | "number") and `level` (1, 2, 3...) on regular blocks + +## Conversion Rules + +Read the rule file matching your source format: + +- **Markdown → Portable Text**: `rules/markdown-to-pt.md` — `@portabletext/markdown` with `markdownToPortableText` (recommended) +- **HTML → Portable Text**: `rules/html-to-pt.md` — `@portabletext/block-tools` with `htmlToBlocks` +- **Manual PT Construction**: `rules/manual-construction.md` — build blocks programmatically from any source + +> **Note:** `@sanity/block-tools` is the legacy package name. Always use `@portabletext/block-tools` for new projects. The API is the same. diff --git a/.agents/skills/portable-text-conversion/rules/html-to-pt.md b/.agents/skills/portable-text-conversion/rules/html-to-pt.md new file mode 100644 index 000000000..153e88b70 --- /dev/null +++ b/.agents/skills/portable-text-conversion/rules/html-to-pt.md @@ -0,0 +1,242 @@ +--- +title: Convert HTML to Portable Text +description: Use @portabletext/block-tools with htmlToBlocks to convert HTML content into Portable Text blocks +tags: [portable-text, html, conversion, migration, import] +--- + +# Convert HTML to Portable Text + +Use `@portabletext/block-tools` to parse HTML into Portable Text blocks. This is the primary tool for migrating HTML content from legacy CMSs. It has built-in support for content from Google Docs, Microsoft Word, and Notion. + +> **Note:** For Markdown sources, use `@portabletext/markdown` instead — it's simpler and more direct. See `rules/markdown-to-pt.md`. + +> **Note:** `@sanity/block-tools` is the legacy package name. Use `@portabletext/block-tools` for new projects. The API is identical. + +## Setup + +```bash +npm install @portabletext/block-tools jsdom @sanity/schema +``` + +In Node.js, you must provide a `parseHtml` function that returns a DOM `Document`. Use JSDOM for this: + +```ts +import {htmlToBlocks} from '@portabletext/block-tools' +import {JSDOM} from 'jsdom' +import Schema from '@sanity/schema' + +// JSDOM is passed to htmlToBlocks via the parseHtml option: +// htmlToBlocks(html, blockContentType, { +// parseHtml: (html) => new JSDOM(html).window.document, +// }) +``` + +## Define Your Schema + +`htmlToBlocks` needs a compiled Sanity block content type to know which marks, styles, and custom types are valid. Use `@sanity/schema` to compile it: + +```ts +const defaultSchema = Schema.compile({ + name: 'mySchema', + types: [ + { + name: 'post', + type: 'document', + fields: [ + { + name: 'body', + type: 'array', + of: [ + { + type: 'block', + marks: { + decorators: [ + {title: 'Strong', value: 'strong'}, + {title: 'Emphasis', value: 'em'}, + {title: 'Code', value: 'code'}, + ], + annotations: [ + { + name: 'link', + type: 'object', + fields: [{name: 'href', type: 'url'}], + }, + ], + }, + styles: [ + {title: 'Normal', value: 'normal'}, + {title: 'H2', value: 'h2'}, + {title: 'H3', value: 'h3'}, + {title: 'Quote', value: 'blockquote'}, + ], + lists: [ + {title: 'Bullet', value: 'bullet'}, + {title: 'Number', value: 'number'}, + ], + }, + { + name: 'image', + type: 'image', + fields: [{name: 'alt', type: 'string'}], + }, + ], + }, + ], + }, + ], +}) + +const blockContentType = defaultSchema + .get('post') + .fields.find((f) => f.name === 'body').type +``` + +## Basic Conversion + +```ts +const html = '

Hello world

Heading

' + +const blocks = htmlToBlocks(html, blockContentType, { + parseHtml: (html) => new JSDOM(html).window.document, +}) +``` + +## Custom Deserializers + +Handle HTML elements that don't map directly to standard PT: + +```ts +const blocks = htmlToBlocks(html, blockContentType, { + parseHtml: (html) => new JSDOM(html).window.document, + rules: [ + // Convert to image blocks + { + deserialize(el, next, block) { + if (el.tagName?.toLowerCase() !== 'img') return undefined + + return block({ + _type: 'image', + asset: { + _type: 'reference', + _ref: '', // Upload image separately, set ref after + }, + alt: el.getAttribute('alt') || '', + _sanityAsset: `image@${el.getAttribute('src')}`, // for migration tooling + }) + }, + }, + // Convert with custom attributes + { + deserialize(el, next, block) { + if (el.tagName?.toLowerCase() !== 'a') return undefined + + const href = el.getAttribute('href') || '' + const target = el.getAttribute('target') || '' + + return { + _type: '__annotation', + markDef: { + _type: 'link', + href, + ...(target ? {target} : {}), + }, + children: next(el.childNodes), + } + }, + }, + // Convert