From cce15885d814934dcd939c1140a00f941e50c5c8 Mon Sep 17 00:00:00 2001 From: Miriad Date: Fri, 13 Mar 2026 19:05:08 +0000 Subject: [PATCH 01/26] 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. --- lib/services/gemini-infographics.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/services/gemini-infographics.ts b/lib/services/gemini-infographics.ts index a2ea194c..a3d417b7 100644 --- a/lib/services/gemini-infographics.ts +++ b/lib/services/gemini-infographics.ts @@ -107,7 +107,13 @@ async function generateWithGeminiContent( const ai = getAI(); // Enforce brand colors by prepending strict color instruction - const colorEnforcedPrompt = `CRITICAL COLOR RULE: Use ONLY purple (#7c3aed) for all highlighted elements, fills, accents, and glows. Use ONLY white (#FFFFFF) for text, arrows, and lines. Use ONLY black (#000000) for background. NEVER use teal, cyan, green, blue, or any other accent color.\n\n${prompt}`; + const colorEnforcedPrompt = `STRICT RULES — FOLLOW EXACTLY: +1. BACKGROUND: Pure black (#000000) only. NO gray, NO white, NO gradients, NO off-white. The entire background MUST be solid black. +2. ACCENTS: Purple (#7c3aed) only for all boxes, fills, highlights, and glows. NO teal, NO cyan, NO green, NO blue. +3. TEXT & LINES: White (#FFFFFF) only for all text labels, arrows, and connecting lines. +4. VIOLATION: Any color other than black background, purple accents, and white text is REJECTED. + +${prompt}`; const response = await ai.models.generateContent({ model, From fa85914027a8d543b6434a02e393306ebb30c31c Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Mon, 16 Mar 2026 20:29:28 -0400 Subject: [PATCH 02/26] temp --- .github/.branch-cleanup | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/.branch-cleanup diff --git a/.github/.branch-cleanup b/.github/.branch-cleanup new file mode 100644 index 00000000..94467fc4 --- /dev/null +++ b/.github/.branch-cleanup @@ -0,0 +1 @@ +cleanup \ No newline at end of file From 7d9368c8fe9eb0022628b17a6dd082afee6152a5 Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Fri, 29 May 2026 14:21:39 -0400 Subject: [PATCH 03/26] 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 --- .github/workflows/deploy.yml | 35 +- .gitignore | 1 + apps/sanity/extract.json | 14884 +++++++++++ apps/sanity/package.json | 3 +- apps/sanity/sanity.config.ts | 4 +- .../sanity/schemas/singletons/engineConfig.ts | 10 + 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 | 41 + .../(dashboard)/dashboard/content/actions.ts | 16 + .../dashboard/content/content-ideas-table.tsx | 176 + .../(dashboard)/dashboard/content/page.tsx | 56 + .../app/(dashboard)/dashboard/login/page.tsx | 92 + apps/web/app/(dashboard)/dashboard/page.tsx | 36 + .../(dashboard)/dashboard/pipeline/page.tsx | 202 + .../dashboard/review/[id]/page.tsx | 51 + .../review/[id]/review-detail-client.tsx | 305 + .../(dashboard)/dashboard/review/actions.ts | 102 + .../app/(dashboard)/dashboard/review/page.tsx | 109 + .../(dashboard)/dashboard/settings/page.tsx | 115 + .../dashboard/settings/settings-form.tsx | 370 + .../(dashboard)/dashboard/sponsors/actions.ts | 10 + .../(dashboard)/dashboard/sponsors/page.tsx | 109 + .../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 | 63 + .../dashboard/videos/videos-table.tsx | 190 + apps/web/app/(dashboard)/layout.tsx | 91 + .../(main)/(author)/author/[slug]/page.tsx | 104 + apps/web/app/(main)/(author)/authors/page.tsx | 5 + .../(author)/authors/page/[num]/page.tsx | 41 + .../app/(main)/(author)/authors/page/page.tsx | 5 + .../app/(main)/(guest)/guest/[slug]/page.tsx | 104 + apps/web/app/(main)/(guest)/guests/page.tsx | 5 + .../(main)/(guest)/guests/page/[num]/page.tsx | 42 + .../app/(main)/(guest)/guests/page/page.tsx | 5 + .../app/(main)/(podcast)/podcast/Podcast.tsx | 142 + .../(main)/(podcast)/podcast/[slug]/page.tsx | 59 + .../app/(main)/(podcast)/podcast/picks.tsx | 88 + .../podcast/preview/[token]/page.tsx | 32 + .../app/(main)/(podcast)/podcasts/page.tsx | 116 + .../(podcast)/podcasts/page/[num]/page.tsx | 42 + .../(main)/(podcast)/podcasts/page/page.tsx | 5 + .../(podcast)/podcasts/rss.json/route.ts | 16 + .../(podcast)/podcasts/rss.xml/route.ts | 13 + apps/web/app/(main)/(post)/blog/page.tsx | 98 + .../(main)/(post)/blog/page/[num]/page.tsx | 37 + apps/web/app/(main)/(post)/blog/page/page.tsx | 5 + .../app/(main)/(post)/blog/rss.json/route.ts | 16 + .../app/(main)/(post)/blog/rss.xml/route.ts | 16 + .../app/(main)/(post)/post/[slug]/page.tsx | 129 + .../(main)/(sponsor)/sponsor/[slug]/page.tsx | 98 + .../app/(main)/(sponsor)/sponsors/page.tsx | 5 + .../(sponsor)/sponsors/page/[num]/page.tsx | 55 + .../(main)/(sponsor)/sponsors/page/page.tsx | 5 + .../(main)/(top-level-pages)/[slug]/page.tsx | 80 + .../app/(main)/(top-level-pages)/pro/page.tsx | 80 + .../(main)/(top-level-pages)/search/page.tsx | 13 + .../(top-level-pages)/sponsorships/page.tsx | 103 + .../sponsorships/sponsorship-form.tsx | 241 + apps/web/app/(main)/icon.svg | 35 + apps/web/app/(main)/layout.tsx | 163 + apps/web/app/(main)/meta-pixel.tsx | 31 + apps/web/app/(main)/page.tsx | 195 + 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 | 584 + apps/web/app/api/cron/check-research/route.ts | 1084 + apps/web/app/api/cron/ingest/route.ts | 728 + apps/web/app/api/cron/route.tsx | 45 + .../app/api/cron/sponsor-outreach/route.ts | 139 + apps/web/app/api/dashboard/activity/route.ts | 43 + apps/web/app/api/dashboard/config/route.ts | 177 + apps/web/app/api/dashboard/metrics/route.ts | 49 + apps/web/app/api/dashboard/pipeline/route.ts | 48 + apps/web/app/api/dashboard/review/route.ts | 117 + apps/web/app/api/dashboard/settings/route.ts | 152 + 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 | 106 + apps/web/app/api/youtube/views/route.tsx | 53 + apps/web/app/globals.css | 224 + apps/web/app/sitemap.ts | 46 + apps/web/astro.config.mjs | 113 - apps/web/biome.json | 54 + 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 | 22 + apps/web/components/footer.tsx | 120 + 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/mode-toggle.tsx | 40 + apps/web/components/more-content.tsx | 135 + 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 | 84 + 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 | 262 + 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/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/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 | 20 + apps/web/package-lock.json | 20472 ---------------- apps/web/package.json | 163 +- 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/.assetsignore | 1 - 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-typegen.json | 5 + apps/web/sanity.cli.ts | 9 + apps/web/sanity/extract.json | 12564 ++++++++++ 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/fetch.ts | 57 + apps/web/sanity/lib/image.ts | 9 + apps/web/sanity/lib/live.ts | 13 + apps/web/sanity/lib/queries.ts | 265 + apps/web/sanity/lib/resolveHref.ts | 17 + apps/web/sanity/lib/token.ts | 7 + apps/web/sanity/lib/utils.ts | 31 + apps/web/sanity/types.ts | 10473 ++++++++ apps/web/schema.json | 12140 +++++++++ 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/assets/fonts/Inter-Bold.ttf | Bin 420428 -> 0 bytes apps/web/src/assets/fonts/Inter-Regular.ttf | Bin 411640 -> 0 bytes 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 | 4 +- pnpm-lock.yaml | 11865 ++++++--- pnpm-workspace.yaml | 13 + 418 files changed, 96375 insertions(+), 29580 deletions(-) create mode 100644 apps/sanity/extract.json 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/sitemap.ts delete mode 100644 apps/web/astro.config.mjs create mode 100644 apps/web/biome.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/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/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/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 delete mode 100644 apps/web/public/.assetsignore 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-typegen.json create mode 100644 apps/web/sanity.cli.ts create mode 100644 apps/web/sanity/extract.json 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/fetch.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/schema.json 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/assets/fonts/Inter-Bold.ttf delete mode 100644 apps/web/src/assets/fonts/Inter-Regular.ttf 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 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c937ba60..1c81e474 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,4 +1,4 @@ -name: Deploy CodingCat.dev +name: Deploy Sanity Studio on: push: @@ -23,23 +23,6 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Install wrangler - run: pnpm add -Dw wrangler - - - name: Build - run: pnpm --filter @codingcatdev/web build - env: - SANITY_PROJECT_ID: ${{ vars.SANITY_STUDIO_PROJECT_ID }} - SANITY_DATASET: ${{ vars.SANITY_STUDIO_DATASET }} - PUBLIC_SANITY_STUDIO_URL: https://${{ vars.SANITY_STUDIO_HOSTNAME }}.sanity.studio - - - name: Deploy to Cloudflare - run: pnpm exec wrangler deploy - working-directory: apps/web - env: - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - - name: Deploy Sanity Studio run: npx sanity deploy -y working-directory: apps/sanity @@ -65,22 +48,6 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Install wrangler - run: pnpm add -Dw wrangler - - - name: Build - run: pnpm --filter @codingcatdev/web build - env: - SANITY_PROJECT_ID: ${{ vars.SANITY_STUDIO_PROJECT_ID }} - SANITY_DATASET: ${{ vars.SANITY_STUDIO_DATASET }} - - - name: Deploy to Cloudflare - run: pnpm exec wrangler deploy --env production - working-directory: apps/web - env: - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - - name: Deploy Sanity Studio run: npx sanity deploy -y working-directory: apps/sanity diff --git a/.gitignore b/.gitignore index 2059b78a..c14bd61f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ node_modules/ # build output dist/ +.next/ # astro .astro/ diff --git a/apps/sanity/extract.json b/apps/sanity/extract.json new file mode 100644 index 00000000..102eb6dc --- /dev/null +++ b/apps/sanity/extract.json @@ -0,0 +1,14884 @@ +[ + { + "type": "type", + "name": "sanity.imageAsset.reference", + "value": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "sanity.imageAsset" + } + }, + { + "type": "type", + "name": "coverImage", + "value": { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "media" + }, + "optional": true + }, + "hotspot": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageHotspot" + }, + "optional": true + }, + "crop": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageCrop" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "image" + } + } + } + } + }, + { + "type": "type", + "name": "socials", + "value": { + "type": "object", + "attributes": { + "bluesky": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "codepen": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "devto": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "discord": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "dribble": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "facebook": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "github": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "instagram": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "lastfm": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "linkedin": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "email": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "mastodon": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "medium": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "polywork": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "stackoverflow": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "substack": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "tiktok": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "twitch": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "twitter": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "youtube": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + } + }, + { + "type": "type", + "name": "link", + "value": { + "type": "object", + "attributes": { + "href": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "blank": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + } + } + }, + { + "type": "type", + "name": "sanity.fileAsset.reference", + "value": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "sanity.fileAsset" + } + }, + { + "type": "type", + "name": "videoCloudinary", + "value": { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.fileAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "videoCloudinary.media" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "file" + } + } + } + } + }, + { + "type": "type", + "name": "statistics", + "value": { + "type": "object", + "attributes": { + "youtube": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "youtube" + }, + "optional": true + } + } + } + }, + { + "type": "type", + "name": "youtube", + "value": { + "type": "object", + "attributes": { + "commentCount": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "favoriteCount": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "likeCount": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "viewCount": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + } + } + } + }, + { + "type": "type", + "name": "ogImage", + "value": { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "ogImage.media" + }, + "optional": true + }, + "hotspot": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageHotspot" + }, + "optional": true + }, + "crop": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageCrop" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "image" + } + } + } + } + }, + { + "type": "type", + "name": "guid", + "value": { + "type": "object", + "attributes": { + "id": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "isPermaLink": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + } + } + }, + { + "type": "type", + "name": "itunes", + "value": { + "type": "object", + "attributes": { + "summary": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "explicit": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "duration": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "season": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "episode": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "episodeType": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "image": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "itunes.image" + }, + "optional": true + } + } + } + }, + { + "type": "type", + "name": "itunes.image", + "value": { + "type": "object", + "attributes": { + "href": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + } + }, + { + "type": "type", + "name": "post.reference", + "value": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "post" + } + }, + { + "type": "type", + "name": "podcast.reference", + "value": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "podcast" + } + }, + { + "type": "type", + "name": "page.reference", + "value": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "page" + } + }, + { + "type": "type", + "name": "author.reference", + "value": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "author" + } + }, + { + "type": "type", + "name": "sponsor.reference", + "value": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "sponsor" + } + }, + { + "type": "type", + "name": "lesson.reference", + "value": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "lesson" + } + }, + { + "name": "course", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "course" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "coverImage": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "coverImage" + }, + "optional": true + }, + "date": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "slug": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "slug" + }, + "optional": true + }, + "excerpt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "featured": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "content": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "children": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "marks": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "text": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "span" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "style": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "normal" + }, + { + "type": "string", + "value": "h1" + }, + { + "type": "string", + "value": "h2" + }, + { + "type": "string", + "value": "h3" + }, + { + "type": "string", + "value": "h4" + }, + { + "type": "string", + "value": "h5" + }, + { + "type": "string", + "value": "h6" + }, + { + "type": "string", + "value": "blockquote" + } + ] + }, + "optional": true + }, + "listItem": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "bullet" + }, + { + "type": "string", + "value": "number" + } + ] + }, + "optional": true + }, + "markDefs": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "href": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "blank": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "link" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "reference": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "inline", + "name": "post.reference" + }, + { + "type": "inline", + "name": "podcast.reference" + }, + { + "type": "inline", + "name": "page.reference" + } + ] + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "internalLink" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + ] + } + }, + "optional": true + }, + "level": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "block" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "unknown" + }, + "optional": true + }, + "hotspot": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageHotspot" + }, + "optional": true + }, + "crop": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageCrop" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "image" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "code" + } + }, + { + "type": "object", + "attributes": { + "youtube": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "youtube" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "shorts": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "youtubeShorts" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "codepen" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "codesandbox" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "id": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "twitter" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "html": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "htmlBlock" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "content": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "children": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "marks": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "text": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "span" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "style": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "normal" + }, + { + "type": "string", + "value": "h1" + }, + { + "type": "string", + "value": "h2" + }, + { + "type": "string", + "value": "h3" + }, + { + "type": "string", + "value": "h4" + }, + { + "type": "string", + "value": "h5" + }, + { + "type": "string", + "value": "h6" + }, + { + "type": "string", + "value": "blockquote" + } + ] + }, + "optional": true + }, + "listItem": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "bullet" + }, + { + "type": "string", + "value": "number" + } + ] + }, + "optional": true + }, + "markDefs": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "href": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "blank": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "link" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "reference": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "inline", + "name": "post.reference" + }, + { + "type": "inline", + "name": "podcast.reference" + }, + { + "type": "inline", + "name": "page.reference" + } + ] + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "internalLink" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + ] + } + }, + "optional": true + }, + "level": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "block" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "quote" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "table" + } + } + ] + } + }, + "optional": true + }, + "videoCloudinary": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "videoCloudinary" + }, + "optional": true + }, + "youtube": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "author": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "author.reference" + } + } + }, + "optional": true + }, + "sponsor": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "sponsor.reference" + } + } + }, + "optional": true + }, + "devto": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "hashnode": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "statistics": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "statistics" + }, + "optional": true + }, + "stripeProduct": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "sections": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "lesson": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "lesson.reference" + } + } + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "section" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + } + } + }, + { + "name": "lesson", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "lesson" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "coverImage": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "coverImage" + }, + "optional": true + }, + "date": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "slug": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "slug" + }, + "optional": true + }, + "excerpt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "featured": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "content": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "children": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "marks": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "text": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "span" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "style": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "normal" + }, + { + "type": "string", + "value": "h1" + }, + { + "type": "string", + "value": "h2" + }, + { + "type": "string", + "value": "h3" + }, + { + "type": "string", + "value": "h4" + }, + { + "type": "string", + "value": "h5" + }, + { + "type": "string", + "value": "h6" + }, + { + "type": "string", + "value": "blockquote" + } + ] + }, + "optional": true + }, + "listItem": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "bullet" + }, + { + "type": "string", + "value": "number" + } + ] + }, + "optional": true + }, + "markDefs": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "href": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "blank": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "link" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "reference": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "inline", + "name": "post.reference" + }, + { + "type": "inline", + "name": "podcast.reference" + }, + { + "type": "inline", + "name": "page.reference" + } + ] + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "internalLink" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + ] + } + }, + "optional": true + }, + "level": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "block" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "unknown" + }, + "optional": true + }, + "hotspot": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageHotspot" + }, + "optional": true + }, + "crop": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageCrop" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "image" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "code" + } + }, + { + "type": "object", + "attributes": { + "youtube": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "youtube" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "shorts": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "youtubeShorts" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "codepen" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "codesandbox" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "id": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "twitter" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "html": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "htmlBlock" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "content": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "children": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "marks": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "text": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "span" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "style": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "normal" + }, + { + "type": "string", + "value": "h1" + }, + { + "type": "string", + "value": "h2" + }, + { + "type": "string", + "value": "h3" + }, + { + "type": "string", + "value": "h4" + }, + { + "type": "string", + "value": "h5" + }, + { + "type": "string", + "value": "h6" + }, + { + "type": "string", + "value": "blockquote" + } + ] + }, + "optional": true + }, + "listItem": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "bullet" + }, + { + "type": "string", + "value": "number" + } + ] + }, + "optional": true + }, + "markDefs": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "href": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "blank": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "link" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "reference": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "inline", + "name": "post.reference" + }, + { + "type": "inline", + "name": "podcast.reference" + }, + { + "type": "inline", + "name": "page.reference" + } + ] + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "internalLink" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + ] + } + }, + "optional": true + }, + "level": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "block" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "quote" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "table" + } + } + ] + } + }, + "optional": true + }, + "videoCloudinary": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "videoCloudinary" + }, + "optional": true + }, + "youtube": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "author": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "author.reference" + } + } + }, + "optional": true + }, + "sponsor": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "sponsor.reference" + } + } + }, + "optional": true + }, + "devto": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "hashnode": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "statistics": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "statistics" + }, + "optional": true + }, + "locked": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + } + }, + { + "type": "type", + "name": "category.reference", + "value": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "category" + } + }, + { + "name": "short", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "short" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "slug": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "slug" + }, + "optional": true + }, + "youtube": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "thumbnail": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "unknown" + }, + "optional": true + }, + "hotspot": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageHotspot" + }, + "optional": true + }, + "crop": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageCrop" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "image" + } + } + } + }, + "optional": true + }, + "parentEpisode": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "podcast.reference" + }, + "optional": true + }, + "publishedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "duration": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "categories": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "category.reference" + } + } + }, + "optional": true + } + } + }, + { + "name": "sanity.imageCrop", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sanity.imageCrop" + } + }, + "top": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "bottom": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "left": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "right": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + } + } + } + }, + { + "name": "sanity.imageHotspot", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sanity.imageHotspot" + } + }, + "x": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "y": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "height": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "width": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + } + } + } + }, + { + "name": "slug", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "slug" + } + }, + "current": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "source": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + } + }, + { + "name": "category", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "category" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "slug": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "slug" + }, + "optional": true + }, + "description": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "color": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + }, + { + "name": "sponsorPool", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sponsorPool" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "companyName": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "contactName": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "contactEmail": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "website": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "category": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "source": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "curated" + }, + { + "type": "string", + "value": "enriched" + } + ] + }, + "optional": true + }, + "relevanceScore": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "lastContactedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "optedOut": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "notes": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + }, + { + "type": "type", + "name": "automatedVideo.reference", + "value": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "automatedVideo" + } + }, + { + "name": "videoAnalytics", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "videoAnalytics" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "contentRef": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "inline", + "name": "post.reference" + }, + { + "type": "inline", + "name": "podcast.reference" + }, + { + "type": "inline", + "name": "automatedVideo.reference" + } + ] + }, + "optional": true + }, + "contentType": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "post" + }, + { + "type": "string", + "value": "podcast" + }, + { + "type": "string", + "value": "automatedVideo" + } + ] + }, + "optional": true + }, + "youtubeId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "youtubeShortId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "viewCount": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "likeCount": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "commentCount": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "favoriteCount": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "lastFetchedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + }, + { + "name": "mediaAsset", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "mediaAsset" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "image": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "unknown" + }, + "optional": true + }, + "hotspot": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageHotspot" + }, + "optional": true + }, + "crop": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageCrop" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "image" + } + } + } + }, + "optional": true + }, + "source": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "simpleicons" + }, + { + "type": "string", + "value": "brandfetch" + }, + { + "type": "string", + "value": "serper" + }, + { + "type": "string", + "value": "screenshot" + }, + { + "type": "string", + "value": "gemini" + }, + { + "type": "string", + "value": "pexels" + }, + { + "type": "string", + "value": "manual" + } + ] + }, + "optional": true + }, + "attribution": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "license": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "cc0" + }, + { + "type": "string", + "value": "cc-by" + }, + { + "type": "string", + "value": "cc-by-sa" + }, + { + "type": "string", + "value": "fair-use" + }, + { + "type": "string", + "value": "proprietary" + }, + { + "type": "string", + "value": "unknown" + } + ] + }, + "optional": true + }, + "brandOverlayApplied": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "originalUrl": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "tags": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + } + } + }, + { + "type": "type", + "name": "contentIdea.reference", + "value": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "contentIdea" + } + }, + { + "type": "type", + "name": "mediaAsset.reference", + "value": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "mediaAsset" + } + }, + { + "type": "type", + "name": "sponsorLead.reference", + "value": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "sponsorLead" + } + }, + { + "name": "automatedVideo", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "automatedVideo" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "slug": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "slug" + }, + "optional": true + }, + "contentIdea": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "contentIdea.reference" + }, + "optional": true + }, + "script": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "hook": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "scenes": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "sceneNumber": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "narration": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "visualDescription": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "bRollKeywords": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "durationEstimate": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "sceneType": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "narration" + }, + { + "type": "string", + "value": "code" + }, + { + "type": "string", + "value": "list" + }, + { + "type": "string", + "value": "comparison" + }, + { + "type": "string", + "value": "mockup" + } + ] + }, + "optional": true + }, + "imagePrompts": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "code": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "snippet": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "language": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "typescript" + }, + { + "type": "string", + "value": "javascript" + }, + { + "type": "string", + "value": "jsx" + }, + { + "type": "string", + "value": "tsx" + }, + { + "type": "string", + "value": "css" + }, + { + "type": "string", + "value": "html" + }, + { + "type": "string", + "value": "json" + }, + { + "type": "string", + "value": "bash" + } + ] + }, + "optional": true + }, + "highlightLines": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "number" + } + }, + "optional": true + } + } + }, + "optional": true + }, + "list": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "items": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "icon": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + }, + "optional": true + }, + "comparison": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "leftLabel": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "rightLabel": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "rows": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "left": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "right": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + } + } + }, + "optional": true + }, + "mockup": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "deviceType": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "browser" + }, + { + "type": "string", + "value": "phone" + }, + { + "type": "string", + "value": "terminal" + } + ] + }, + "optional": true + }, + "screenContent": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + }, + "optional": true + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "cta": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + }, + "optional": true + }, + "scriptQualityScore": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "status": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "draft" + }, + { + "type": "string", + "value": "researching" + }, + { + "type": "string", + "value": "research_complete" + }, + { + "type": "string", + "value": "generating_media" + }, + { + "type": "string", + "value": "generating_audio" + }, + { + "type": "string", + "value": "assembling_video" + }, + { + "type": "string", + "value": "quality_gate" + }, + { + "type": "string", + "value": "pending_review" + }, + { + "type": "string", + "value": "approved" + }, + { + "type": "string", + "value": "publishing" + }, + { + "type": "string", + "value": "published" + }, + { + "type": "string", + "value": "rejected" + }, + { + "type": "string", + "value": "archived" + }, + { + "type": "string", + "value": "script_ready" + }, + { + "type": "string", + "value": "audio_gen" + }, + { + "type": "string", + "value": "video_gen" + }, + { + "type": "string", + "value": "rendering" + }, + { + "type": "string", + "value": "uploading" + }, + { + "type": "string", + "value": "flagged" + }, + { + "type": "string", + "value": "infographics_generating" + }, + { + "type": "string", + "value": "enriching" + } + ] + }, + "optional": true + }, + "qualityScore": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "qualityIssues": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "reviewedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "reviewedBy": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "mediaAssets": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "mediaAsset.reference" + } + } + }, + "optional": true + }, + "shorts": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "startScene": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "endScene": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "videoUrl": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "thumbnailUrl": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "thumbnailHorizontal": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "unknown" + }, + "optional": true + }, + "hotspot": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageHotspot" + }, + "optional": true + }, + "crop": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageCrop" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "image" + } + } + } + }, + "optional": true + }, + "thumbnailVertical": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "unknown" + }, + "optional": true + }, + "hotspot": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageHotspot" + }, + "optional": true + }, + "crop": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageCrop" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "image" + } + } + } + }, + "optional": true + }, + "blogPostId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "socialPosts": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "platform": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "youtube" + }, + { + "type": "string", + "value": "twitter" + }, + { + "type": "string", + "value": "linkedin" + }, + { + "type": "string", + "value": "tiktok" + }, + { + "type": "string", + "value": "instagram" + }, + { + "type": "string", + "value": "bluesky" + } + ] + }, + "optional": true + }, + "postId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "postUrl": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "postedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "workflowId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "r2Prefix": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "audioFile": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.fileAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "unknown" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "file" + } + } + } + }, + "optional": true + }, + "audioUrl": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "videoFile": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.fileAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "unknown" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "file" + } + } + } + }, + "optional": true + }, + "videoUrl": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "shortFile": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.fileAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "unknown" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "file" + } + } + } + }, + "optional": true + }, + "shortUrl": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "infographics": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "unknown" + }, + "optional": true + }, + "hotspot": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageHotspot" + }, + "optional": true + }, + "crop": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageCrop" + }, + "optional": true + }, + "alt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "caption": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "image" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "infographicsHorizontal": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "image": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "unknown" + }, + "optional": true + }, + "hotspot": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageHotspot" + }, + "optional": true + }, + "crop": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageCrop" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "image" + } + } + } + }, + "optional": true + }, + "prompt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "sceneNumber": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "infographicsVertical": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "image": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "unknown" + }, + "optional": true + }, + "hotspot": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageHotspot" + }, + "optional": true + }, + "crop": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageCrop" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "image" + } + } + } + }, + "optional": true + }, + "prompt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "sceneNumber": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "renderData": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "mainRenderId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "shortRenderId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "bucketName": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "startedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + }, + "optional": true + }, + "youtubeId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "youtubeShortId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "scheduledPublishAt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "sponsorSlot": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sponsorLead.reference" + }, + "optional": true + }, + "researchNotebookId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "researchTaskId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "researchInteractionId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "researchData": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "infographicArtifactIds": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "trendScore": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "trendSources": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "flaggedReason": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "distributionLog": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "step": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "status": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "success" + }, + { + "type": "string", + "value": "failed" + }, + { + "type": "string", + "value": "skipped" + } + ] + }, + "optional": true + }, + "error": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "timestamp": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "result": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + } + } + }, + { + "name": "sponsorLead", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sponsorLead" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "companyName": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "contactName": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "contactEmail": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "source": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "inbound" + }, + { + "type": "string", + "value": "outbound" + } + ] + }, + "optional": true + }, + "status": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "new" + }, + { + "type": "string", + "value": "contacted" + }, + { + "type": "string", + "value": "replied" + }, + { + "type": "string", + "value": "negotiating" + }, + { + "type": "string", + "value": "booked" + }, + { + "type": "string", + "value": "paid" + } + ] + }, + "optional": true + }, + "intent": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "rateCard": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "stripeInvoiceId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "bookedSlot": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "automatedVideo.reference" + }, + "optional": true + }, + "sponsorDocId": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sponsor.reference" + }, + "optional": true + }, + "threadId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "lastEmailAt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "notes": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + }, + { + "name": "contentIdea", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "contentIdea" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "sourceUrl": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "summary": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "topics": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "collectedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "status": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "new" + }, + { + "type": "string", + "value": "approved" + }, + { + "type": "string", + "value": "rejected" + } + ] + }, + "optional": true + }, + "reasonRejected": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + }, + { + "name": "sponsorshipRequest", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sponsorshipRequest" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "fullName": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "email": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "companyName": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "sponsorshipTier": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "message": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + }, + { + "name": "previewSession", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "previewSession" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "token": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "documentId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "expiresAt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + }, + { + "name": "sponsor", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sponsor" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "coverImage": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "coverImage" + }, + "optional": true + }, + "date": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "slug": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "slug" + }, + "optional": true + }, + "excerpt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "featured": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "content": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "children": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "marks": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "text": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "span" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "style": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "normal" + }, + { + "type": "string", + "value": "h1" + }, + { + "type": "string", + "value": "h2" + }, + { + "type": "string", + "value": "h3" + }, + { + "type": "string", + "value": "h4" + }, + { + "type": "string", + "value": "h5" + }, + { + "type": "string", + "value": "h6" + }, + { + "type": "string", + "value": "blockquote" + } + ] + }, + "optional": true + }, + "listItem": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "bullet" + }, + { + "type": "string", + "value": "number" + } + ] + }, + "optional": true + }, + "markDefs": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "href": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "blank": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "link" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "reference": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "inline", + "name": "post.reference" + }, + { + "type": "inline", + "name": "podcast.reference" + }, + { + "type": "inline", + "name": "page.reference" + } + ] + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "internalLink" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + ] + } + }, + "optional": true + }, + "level": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "block" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "unknown" + }, + "optional": true + }, + "hotspot": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageHotspot" + }, + "optional": true + }, + "crop": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageCrop" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "image" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "code" + } + }, + { + "type": "object", + "attributes": { + "youtube": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "youtube" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "shorts": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "youtubeShorts" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "codepen" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "codesandbox" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "id": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "twitter" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "html": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "htmlBlock" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "content": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "children": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "marks": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "text": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "span" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "style": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "normal" + }, + { + "type": "string", + "value": "h1" + }, + { + "type": "string", + "value": "h2" + }, + { + "type": "string", + "value": "h3" + }, + { + "type": "string", + "value": "h4" + }, + { + "type": "string", + "value": "h5" + }, + { + "type": "string", + "value": "h6" + }, + { + "type": "string", + "value": "blockquote" + } + ] + }, + "optional": true + }, + "listItem": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "bullet" + }, + { + "type": "string", + "value": "number" + } + ] + }, + "optional": true + }, + "markDefs": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "href": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "blank": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "link" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "reference": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "inline", + "name": "post.reference" + }, + { + "type": "inline", + "name": "podcast.reference" + }, + { + "type": "inline", + "name": "page.reference" + } + ] + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "internalLink" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + ] + } + }, + "optional": true + }, + "level": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "block" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "quote" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "table" + } + } + ] + } + }, + "optional": true + }, + "ogTitle": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "ogDescription": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "ogImage": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "ogImage" + }, + "optional": true + }, + "twitterCardType": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "summary_large_image" + }, + { + "type": "string", + "value": "summary" + } + ] + }, + "optional": true + }, + "noIndex": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + }, + { + "name": "author", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "author" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "coverImage": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "coverImage" + }, + "optional": true + }, + "date": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "slug": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "slug" + }, + "optional": true + }, + "excerpt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "featured": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "content": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "children": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "marks": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "text": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "span" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "style": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "normal" + }, + { + "type": "string", + "value": "h1" + }, + { + "type": "string", + "value": "h2" + }, + { + "type": "string", + "value": "h3" + }, + { + "type": "string", + "value": "h4" + }, + { + "type": "string", + "value": "h5" + }, + { + "type": "string", + "value": "h6" + }, + { + "type": "string", + "value": "blockquote" + } + ] + }, + "optional": true + }, + "listItem": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "bullet" + }, + { + "type": "string", + "value": "number" + } + ] + }, + "optional": true + }, + "markDefs": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "href": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "blank": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "link" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "reference": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "inline", + "name": "post.reference" + }, + { + "type": "inline", + "name": "podcast.reference" + }, + { + "type": "inline", + "name": "page.reference" + } + ] + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "internalLink" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + ] + } + }, + "optional": true + }, + "level": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "block" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "unknown" + }, + "optional": true + }, + "hotspot": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageHotspot" + }, + "optional": true + }, + "crop": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageCrop" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "image" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "code" + } + }, + { + "type": "object", + "attributes": { + "youtube": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "youtube" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "shorts": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "youtubeShorts" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "codepen" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "codesandbox" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "id": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "twitter" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "html": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "htmlBlock" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "content": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "children": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "marks": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "text": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "span" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "style": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "normal" + }, + { + "type": "string", + "value": "h1" + }, + { + "type": "string", + "value": "h2" + }, + { + "type": "string", + "value": "h3" + }, + { + "type": "string", + "value": "h4" + }, + { + "type": "string", + "value": "h5" + }, + { + "type": "string", + "value": "h6" + }, + { + "type": "string", + "value": "blockquote" + } + ] + }, + "optional": true + }, + "listItem": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "bullet" + }, + { + "type": "string", + "value": "number" + } + ] + }, + "optional": true + }, + "markDefs": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "href": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "blank": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "link" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "reference": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "inline", + "name": "post.reference" + }, + { + "type": "inline", + "name": "podcast.reference" + }, + { + "type": "inline", + "name": "page.reference" + } + ] + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "internalLink" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + ] + } + }, + "optional": true + }, + "level": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "block" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "quote" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "table" + } + } + ] + } + }, + "optional": true + }, + "socials": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "socials" + }, + "optional": true + }, + "websites": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "site": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "link": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "link" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "site" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + } + } + }, + { + "name": "post", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "post" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "coverImage": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "coverImage" + }, + "optional": true + }, + "date": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "slug": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "slug" + }, + "optional": true + }, + "excerpt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "featured": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "content": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "children": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "marks": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "text": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "span" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "style": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "normal" + }, + { + "type": "string", + "value": "h1" + }, + { + "type": "string", + "value": "h2" + }, + { + "type": "string", + "value": "h3" + }, + { + "type": "string", + "value": "h4" + }, + { + "type": "string", + "value": "h5" + }, + { + "type": "string", + "value": "h6" + }, + { + "type": "string", + "value": "blockquote" + } + ] + }, + "optional": true + }, + "listItem": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "bullet" + }, + { + "type": "string", + "value": "number" + } + ] + }, + "optional": true + }, + "markDefs": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "href": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "blank": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "link" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "reference": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "inline", + "name": "post.reference" + }, + { + "type": "inline", + "name": "podcast.reference" + }, + { + "type": "inline", + "name": "page.reference" + } + ] + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "internalLink" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + ] + } + }, + "optional": true + }, + "level": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "block" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "unknown" + }, + "optional": true + }, + "hotspot": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageHotspot" + }, + "optional": true + }, + "crop": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageCrop" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "image" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "code" + } + }, + { + "type": "object", + "attributes": { + "youtube": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "youtube" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "shorts": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "youtubeShorts" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "codepen" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "codesandbox" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "id": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "twitter" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "html": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "htmlBlock" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "content": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "children": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "marks": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "text": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "span" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "style": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "normal" + }, + { + "type": "string", + "value": "h1" + }, + { + "type": "string", + "value": "h2" + }, + { + "type": "string", + "value": "h3" + }, + { + "type": "string", + "value": "h4" + }, + { + "type": "string", + "value": "h5" + }, + { + "type": "string", + "value": "h6" + }, + { + "type": "string", + "value": "blockquote" + } + ] + }, + "optional": true + }, + "listItem": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "bullet" + }, + { + "type": "string", + "value": "number" + } + ] + }, + "optional": true + }, + "markDefs": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "href": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "blank": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "link" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "reference": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "inline", + "name": "post.reference" + }, + { + "type": "inline", + "name": "podcast.reference" + }, + { + "type": "inline", + "name": "page.reference" + } + ] + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "internalLink" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + ] + } + }, + "optional": true + }, + "level": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "block" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "quote" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "table" + } + } + ] + } + }, + "optional": true + }, + "videoCloudinary": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "videoCloudinary" + }, + "optional": true + }, + "youtube": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "author": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "author.reference" + } + } + }, + "optional": true + }, + "sponsor": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "sponsor.reference" + } + } + }, + "optional": true + }, + "devto": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "hashnode": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "statistics": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "statistics" + }, + "optional": true + }, + "ogTitle": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "ogDescription": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "ogImage": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "ogImage" + }, + "optional": true + }, + "twitterCardType": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "summary_large_image" + }, + { + "type": "string", + "value": "summary" + } + ] + }, + "optional": true + }, + "noIndex": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "categories": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "category.reference" + } + } + }, + "optional": true + } + } + }, + { + "type": "type", + "name": "podcastType.reference", + "value": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "podcastType" + } + }, + { + "type": "type", + "name": "guest.reference", + "value": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "guest" + } + }, + { + "type": "type", + "name": "podcastSeries.reference", + "value": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "podcastSeries" + } + }, + { + "type": "type", + "name": "short.reference", + "value": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "short" + } + }, + { + "name": "podcast", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "podcast" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "coverImage": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "coverImage" + }, + "optional": true + }, + "date": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "slug": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "slug" + }, + "optional": true + }, + "excerpt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "featured": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "content": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "children": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "marks": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "text": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "span" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "style": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "normal" + }, + { + "type": "string", + "value": "h1" + }, + { + "type": "string", + "value": "h2" + }, + { + "type": "string", + "value": "h3" + }, + { + "type": "string", + "value": "h4" + }, + { + "type": "string", + "value": "h5" + }, + { + "type": "string", + "value": "h6" + }, + { + "type": "string", + "value": "blockquote" + } + ] + }, + "optional": true + }, + "listItem": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "bullet" + }, + { + "type": "string", + "value": "number" + } + ] + }, + "optional": true + }, + "markDefs": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "href": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "blank": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "link" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "reference": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "inline", + "name": "post.reference" + }, + { + "type": "inline", + "name": "podcast.reference" + }, + { + "type": "inline", + "name": "page.reference" + } + ] + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "internalLink" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + ] + } + }, + "optional": true + }, + "level": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "block" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "unknown" + }, + "optional": true + }, + "hotspot": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageHotspot" + }, + "optional": true + }, + "crop": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageCrop" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "image" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "code" + } + }, + { + "type": "object", + "attributes": { + "youtube": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "youtube" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "shorts": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "youtubeShorts" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "codepen" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "codesandbox" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "id": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "twitter" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "html": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "htmlBlock" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "content": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "children": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "marks": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "text": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "span" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "style": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "normal" + }, + { + "type": "string", + "value": "h1" + }, + { + "type": "string", + "value": "h2" + }, + { + "type": "string", + "value": "h3" + }, + { + "type": "string", + "value": "h4" + }, + { + "type": "string", + "value": "h5" + }, + { + "type": "string", + "value": "h6" + }, + { + "type": "string", + "value": "blockquote" + } + ] + }, + "optional": true + }, + "listItem": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "bullet" + }, + { + "type": "string", + "value": "number" + } + ] + }, + "optional": true + }, + "markDefs": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "href": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "blank": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "link" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "reference": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "inline", + "name": "post.reference" + }, + { + "type": "inline", + "name": "podcast.reference" + }, + { + "type": "inline", + "name": "page.reference" + } + ] + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "internalLink" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + ] + } + }, + "optional": true + }, + "level": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "block" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "quote" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "table" + } + } + ] + } + }, + "optional": true + }, + "videoCloudinary": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "videoCloudinary" + }, + "optional": true + }, + "youtube": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "author": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "author.reference" + } + } + }, + "optional": true + }, + "sponsor": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "sponsor.reference" + } + } + }, + "optional": true + }, + "devto": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "hashnode": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "statistics": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "statistics" + }, + "optional": true + }, + "ogTitle": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "ogDescription": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "ogImage": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "ogImage" + }, + "optional": true + }, + "twitterCardType": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "summary_large_image" + }, + { + "type": "string", + "value": "summary" + } + ] + }, + "optional": true + }, + "noIndex": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "podcastType": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "podcastType.reference" + }, + "optional": true + }, + "season": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "episode": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "recordingDate": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "guest": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "guest.reference" + } + } + }, + "optional": true + }, + "pick": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "user": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "inline", + "name": "guest.reference" + }, + { + "type": "inline", + "name": "author.reference" + } + ] + }, + "optional": true + }, + "name": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "site": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "spotify": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "podcastRssEpisode" + }, + "optional": true + }, + "thumbnail": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "unknown" + }, + "optional": true + }, + "hotspot": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageHotspot" + }, + "optional": true + }, + "crop": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageCrop" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "image" + } + } + } + }, + "optional": true + }, + "duration": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "chapters": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "timestamp": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "seconds": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "series": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "podcastSeries.reference" + }, + "optional": true + }, + "seriesOrder": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "listenLinks": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "youtube": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "spotify": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "apple": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "overcast": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "pocketCasts": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "rss": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + }, + "optional": true + }, + "transcript": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "contentType": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "interview" + }, + { + "type": "string", + "value": "solo" + }, + { + "type": "string", + "value": "tutorial" + }, + { + "type": "string", + "value": "news" + }, + { + "type": "string", + "value": "review" + } + ] + }, + "optional": true + }, + "relatedShorts": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "short.reference" + } + } + }, + "optional": true + }, + "relatedBlogPost": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "post.reference" + }, + "optional": true + } + } + }, + { + "name": "podcastSeries", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "podcastSeries" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "slug": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "slug" + }, + "optional": true + }, + "description": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "coverImage": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "unknown" + }, + "optional": true + }, + "hotspot": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageHotspot" + }, + "optional": true + }, + "crop": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageCrop" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "image" + } + } + } + }, + "optional": true + }, + "youtubePlaylistId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "isActive": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + } + }, + { + "name": "podcastRssEpisode", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "podcastRssEpisode" + } + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "description": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "link": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "guid": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "guid" + }, + "optional": true + }, + "pubDate": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "enclosures": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "length": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "type": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "enclosure" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "itunes": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "itunes" + }, + "optional": true + } + } + } + }, + { + "name": "guest", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "guest" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "coverImage": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "coverImage" + }, + "optional": true + }, + "date": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "slug": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "slug" + }, + "optional": true + }, + "excerpt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "featured": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "content": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "children": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "marks": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "text": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "span" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "style": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "normal" + }, + { + "type": "string", + "value": "h1" + }, + { + "type": "string", + "value": "h2" + }, + { + "type": "string", + "value": "h3" + }, + { + "type": "string", + "value": "h4" + }, + { + "type": "string", + "value": "h5" + }, + { + "type": "string", + "value": "h6" + }, + { + "type": "string", + "value": "blockquote" + } + ] + }, + "optional": true + }, + "listItem": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "bullet" + }, + { + "type": "string", + "value": "number" + } + ] + }, + "optional": true + }, + "markDefs": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "href": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "blank": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "link" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "reference": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "inline", + "name": "post.reference" + }, + { + "type": "inline", + "name": "podcast.reference" + }, + { + "type": "inline", + "name": "page.reference" + } + ] + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "internalLink" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + ] + } + }, + "optional": true + }, + "level": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "block" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "unknown" + }, + "optional": true + }, + "hotspot": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageHotspot" + }, + "optional": true + }, + "crop": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageCrop" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "image" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "code" + } + }, + { + "type": "object", + "attributes": { + "youtube": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "youtube" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "shorts": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "youtubeShorts" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "codepen" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "codesandbox" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "id": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "twitter" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "html": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "htmlBlock" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "content": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "children": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "marks": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "text": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "span" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "style": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "normal" + }, + { + "type": "string", + "value": "h1" + }, + { + "type": "string", + "value": "h2" + }, + { + "type": "string", + "value": "h3" + }, + { + "type": "string", + "value": "h4" + }, + { + "type": "string", + "value": "h5" + }, + { + "type": "string", + "value": "h6" + }, + { + "type": "string", + "value": "blockquote" + } + ] + }, + "optional": true + }, + "listItem": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "bullet" + }, + { + "type": "string", + "value": "number" + } + ] + }, + "optional": true + }, + "markDefs": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "href": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "blank": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "link" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "reference": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "inline", + "name": "post.reference" + }, + { + "type": "inline", + "name": "podcast.reference" + }, + { + "type": "inline", + "name": "page.reference" + } + ] + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "internalLink" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + ] + } + }, + "optional": true + }, + "level": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "block" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "quote" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "table" + } + } + ] + } + }, + "optional": true + }, + "socials": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "socials" + }, + "optional": true + }, + "websites": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "site": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "link": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "link" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "site" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "company": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "role": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + }, + { + "name": "podcastType", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "podcastType" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "coverImage": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "coverImage" + }, + "optional": true + }, + "date": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "slug": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "slug" + }, + "optional": true + }, + "excerpt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "featured": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "content": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "children": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "marks": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "text": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "span" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "style": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "normal" + }, + { + "type": "string", + "value": "h1" + }, + { + "type": "string", + "value": "h2" + }, + { + "type": "string", + "value": "h3" + }, + { + "type": "string", + "value": "h4" + }, + { + "type": "string", + "value": "h5" + }, + { + "type": "string", + "value": "h6" + }, + { + "type": "string", + "value": "blockquote" + } + ] + }, + "optional": true + }, + "listItem": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "bullet" + }, + { + "type": "string", + "value": "number" + } + ] + }, + "optional": true + }, + "markDefs": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "href": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "blank": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "link" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "reference": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "inline", + "name": "post.reference" + }, + { + "type": "inline", + "name": "podcast.reference" + }, + { + "type": "inline", + "name": "page.reference" + } + ] + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "internalLink" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + ] + } + }, + "optional": true + }, + "level": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "block" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "unknown" + }, + "optional": true + }, + "hotspot": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageHotspot" + }, + "optional": true + }, + "crop": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageCrop" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "image" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "code" + } + }, + { + "type": "object", + "attributes": { + "youtube": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "youtube" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "shorts": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "youtubeShorts" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "codepen" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "codesandbox" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "id": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "twitter" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "html": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "htmlBlock" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "content": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "children": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "marks": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "text": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "span" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "style": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "normal" + }, + { + "type": "string", + "value": "h1" + }, + { + "type": "string", + "value": "h2" + }, + { + "type": "string", + "value": "h3" + }, + { + "type": "string", + "value": "h4" + }, + { + "type": "string", + "value": "h5" + }, + { + "type": "string", + "value": "h6" + }, + { + "type": "string", + "value": "blockquote" + } + ] + }, + "optional": true + }, + "listItem": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "bullet" + }, + { + "type": "string", + "value": "number" + } + ] + }, + "optional": true + }, + "markDefs": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "href": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "blank": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "link" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "reference": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "inline", + "name": "post.reference" + }, + { + "type": "inline", + "name": "podcast.reference" + }, + { + "type": "inline", + "name": "page.reference" + } + ] + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "internalLink" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + ] + } + }, + "optional": true + }, + "level": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "block" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "quote" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "table" + } + } + ] + } + }, + "optional": true + } + } + }, + { + "name": "page", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "page" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "coverImage": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "coverImage" + }, + "optional": true + }, + "date": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "slug": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "slug" + }, + "optional": true + }, + "excerpt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "featured": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "content": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "children": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "marks": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "text": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "span" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "style": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "normal" + }, + { + "type": "string", + "value": "h1" + }, + { + "type": "string", + "value": "h2" + }, + { + "type": "string", + "value": "h3" + }, + { + "type": "string", + "value": "h4" + }, + { + "type": "string", + "value": "h5" + }, + { + "type": "string", + "value": "h6" + }, + { + "type": "string", + "value": "blockquote" + } + ] + }, + "optional": true + }, + "listItem": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "bullet" + }, + { + "type": "string", + "value": "number" + } + ] + }, + "optional": true + }, + "markDefs": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "href": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "blank": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "link" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "reference": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "inline", + "name": "post.reference" + }, + { + "type": "inline", + "name": "podcast.reference" + }, + { + "type": "inline", + "name": "page.reference" + } + ] + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "internalLink" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + ] + } + }, + "optional": true + }, + "level": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "block" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "unknown" + }, + "optional": true + }, + "hotspot": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageHotspot" + }, + "optional": true + }, + "crop": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageCrop" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "image" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "code" + } + }, + { + "type": "object", + "attributes": { + "youtube": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "youtube" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "shorts": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "youtubeShorts" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "codepen" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "codesandbox" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "id": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "twitter" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "html": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "htmlBlock" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "content": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "children": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "marks": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "text": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "span" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "style": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "normal" + }, + { + "type": "string", + "value": "h1" + }, + { + "type": "string", + "value": "h2" + }, + { + "type": "string", + "value": "h3" + }, + { + "type": "string", + "value": "h4" + }, + { + "type": "string", + "value": "h5" + }, + { + "type": "string", + "value": "h6" + }, + { + "type": "string", + "value": "blockquote" + } + ] + }, + "optional": true + }, + "listItem": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "bullet" + }, + { + "type": "string", + "value": "number" + } + ] + }, + "optional": true + }, + "markDefs": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "href": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "blank": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "link" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "reference": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "inline", + "name": "post.reference" + }, + { + "type": "inline", + "name": "podcast.reference" + }, + { + "type": "inline", + "name": "page.reference" + } + ] + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "internalLink" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + ] + } + }, + "optional": true + }, + "level": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "block" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "quote" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "table" + } + } + ] + } + }, + "optional": true + }, + "videoCloudinary": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "videoCloudinary" + }, + "optional": true + }, + "youtube": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "author": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "author.reference" + } + } + }, + "optional": true + }, + "sponsor": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "sponsor.reference" + } + } + }, + "optional": true + }, + "devto": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "hashnode": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "statistics": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "statistics" + }, + "optional": true + }, + "ogTitle": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "ogDescription": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "ogImage": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "ogImage" + }, + "optional": true + }, + "twitterCardType": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "summary_large_image" + }, + { + "type": "string", + "value": "summary" + } + ] + }, + "optional": true + }, + "noIndex": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + } + }, + { + "name": "engineConfig", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "engineConfig" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "autoPublish": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "qualityThreshold": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "reviewTimeoutDays": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "reviewNotification": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "email" + }, + { + "type": "string", + "value": "slack" + }, + { + "type": "string", + "value": "webhook" + } + ] + }, + "optional": true + }, + "maxIdeasPerRun": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "stuckTimeoutMinutes": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "longFormPerWeek": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "shortsPerDay": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "blogsPerWeek": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "newsletterFrequency": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "weekly" + }, + { + "type": "string", + "value": "biweekly" + }, + { + "type": "string", + "value": "monthly" + } + ] + }, + "optional": true + }, + "publishDays": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "contentCategories": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "trendSources": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "topicFocus": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "rssFeeds": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "name": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "trendSourcesEnabled": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "hn": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "devto": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "blogs": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "youtube": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "github": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + } + }, + "optional": true + }, + "dedupWindowDays": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "geminiModel": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "infographicModel": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "infographicPromptPrefix": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "systemInstruction": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "deepResearchAgent": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "deepResearchPromptTemplate": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "enableDeepResearch": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "enableHorizontalInfographics": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "thumbnailEnabled": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "infographicInstructions": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "targetVideoDurationSec": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "sceneCountMin": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "sceneCountMax": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "elevenLabsVoiceId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "youtubeEnabled": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "twitterEnabled": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "linkedinEnabled": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "tiktokEnabled": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "instagramEnabled": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "blueskyEnabled": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "newsletterEnabled": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "youtubeUploadVisibility": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "private" + }, + { + "type": "string", + "value": "unlisted" + }, + { + "type": "string", + "value": "public" + } + ] + }, + "optional": true + }, + "youtubeChannelId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "youtubeDescriptionTemplate": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "youtubeDefaultTags": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "notificationEmails": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "resendFromEmail": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "cooldownDays": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "rateCardTiers": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "name": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "description": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "price": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "outreachEmailTemplate": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "maxOutreachPerRun": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "brandPrimary": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "brandBackground": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "brandText": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "awsRegion": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "remotionFunctionName": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "remotionServeUrl": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "gcsBucketName": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "gcsProjectId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + }, + { + "name": "settings", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "settings" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "description": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "children": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "marks": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "text": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "span" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "style": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "normal" + } + ] + }, + "optional": true + }, + "listItem": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [] + }, + "optional": true + }, + "markDefs": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "href": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "link" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "level": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "block" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "navLinks": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "path": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "sideOnly": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "ogImage": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "asset": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageAsset.reference" + }, + "optional": true + }, + "media": { + "type": "objectAttribute", + "value": { + "type": "unknown" + }, + "optional": true + }, + "hotspot": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageHotspot" + }, + "optional": true + }, + "crop": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageCrop" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "image" + } + } + } + }, + "optional": true + } + } + }, + { + "name": "row", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "row" + } + }, + "cells": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + } + } + } + }, + { + "name": "table", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "table" + } + }, + "rows": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "row" + } + } + }, + "optional": true + } + } + } + }, + { + "name": "code", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "code" + } + }, + "language": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "filename": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "code": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "highlightedLines": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "number" + } + }, + "optional": true + } + } + } + }, + { + "name": "media.tag", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "media.tag" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "name": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "slug" + }, + "optional": true + } + } + }, + { + "name": "sanity.assist.instructionTask", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sanity.assist.instructionTask" + } + }, + "path": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "instructionKey": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "started": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "updated": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "info": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + } + }, + { + "name": "sanity.assist.task.status", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sanity.assist.task.status" + } + }, + "tasks": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "sanity.assist.instructionTask" + } + } + }, + "optional": true + } + } + } + }, + { + "name": "sanity.assist.schemaType.annotations", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sanity.assist.schemaType.annotations" + } + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "fields": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "sanity.assist.schemaType.field" + } + } + }, + "optional": true + } + } + } + }, + { + "name": "sanity.assist.output.type", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sanity.assist.output.type" + } + }, + "type": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + } + }, + { + "name": "sanity.assist.output.field", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sanity.assist.output.field" + } + }, + "path": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + } + }, + { + "type": "type", + "name": "assist.instruction.context.reference", + "value": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "assist.instruction.context" + } + }, + { + "name": "sanity.assist.instruction.context", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sanity.assist.instruction.context" + } + }, + "reference": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "assist.instruction.context.reference" + }, + "optional": true + } + } + } + }, + { + "name": "assist.instruction.context", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "assist.instruction.context" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "context": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "children": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "marks": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "text": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "span" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "style": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "normal" + } + ] + }, + "optional": true + }, + "listItem": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [] + }, + "optional": true + }, + "markDefs": { + "type": "objectAttribute", + "value": { + "type": "null" + }, + "optional": true + }, + "level": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "block" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + } + } + }, + { + "name": "sanity.assist.instruction.userInput", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sanity.assist.instruction.userInput" + } + }, + "message": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "description": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + } + }, + { + "name": "sanity.assist.instruction.prompt", + "type": "type", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "children": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "marks": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "string" + } + }, + "optional": true + }, + "text": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "span" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "sanity.assist.instruction.fieldRef" + } + }, + { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "sanity.assist.instruction.context" + } + }, + { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "sanity.assist.instruction.userInput" + } + } + ] + } + }, + "optional": true + }, + "style": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "normal" + } + ] + }, + "optional": true + }, + "listItem": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [] + }, + "optional": true + }, + "markDefs": { + "type": "objectAttribute", + "value": { + "type": "null" + }, + "optional": true + }, + "level": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "block" + } + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + } + }, + { + "name": "sanity.assist.instruction.fieldRef", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sanity.assist.instruction.fieldRef" + } + }, + "path": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + } + }, + { + "name": "sanity.assist.instruction", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sanity.assist.instruction" + } + }, + "prompt": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.assist.instruction.prompt" + }, + "optional": true + }, + "icon": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "userId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "createdById": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "output": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "union", + "of": [ + { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "sanity.assist.output.field" + } + }, + { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "sanity.assist.output.type" + } + } + ] + } + }, + "optional": true + } + } + } + }, + { + "name": "sanity.assist.schemaType.field", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sanity.assist.schemaType.field" + } + }, + "path": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "instructions": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + }, + "rest": { + "type": "inline", + "name": "sanity.assist.instruction" + } + } + }, + "optional": true + } + } + } + }, + { + "name": "sanity.imagePaletteSwatch", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sanity.imagePaletteSwatch" + } + }, + "background": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "foreground": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "population": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + } + }, + { + "name": "sanity.imagePalette", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sanity.imagePalette" + } + }, + "darkMuted": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imagePaletteSwatch" + }, + "optional": true + }, + "lightVibrant": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imagePaletteSwatch" + }, + "optional": true + }, + "darkVibrant": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imagePaletteSwatch" + }, + "optional": true + }, + "vibrant": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imagePaletteSwatch" + }, + "optional": true + }, + "dominant": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imagePaletteSwatch" + }, + "optional": true + }, + "lightMuted": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imagePaletteSwatch" + }, + "optional": true + }, + "muted": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imagePaletteSwatch" + }, + "optional": true + } + } + } + }, + { + "name": "sanity.imageDimensions", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sanity.imageDimensions" + } + }, + "height": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "width": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "aspectRatio": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + } + } + } + }, + { + "name": "sanity.imageMetadata", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sanity.imageMetadata" + } + }, + "location": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "geopoint" + }, + "optional": true + }, + "dimensions": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageDimensions" + }, + "optional": true + }, + "palette": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imagePalette" + }, + "optional": true + }, + "lqip": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "blurHash": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "thumbHash": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "hasAlpha": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "isOpaque": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + } + } + }, + { + "name": "sanity.fileAsset", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sanity.fileAsset" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "originalFilename": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "label": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "description": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "altText": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "sha1hash": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "extension": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "mimeType": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "size": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "assetId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "uploadId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "path": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "source": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.assetSourceData" + }, + "optional": true + } + } + }, + { + "name": "sanity.assetSourceData", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sanity.assetSourceData" + } + }, + "name": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "id": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + } + }, + { + "name": "sanity.imageAsset", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sanity.imageAsset" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "originalFilename": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "label": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "description": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "altText": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "sha1hash": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "extension": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "mimeType": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "size": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "assetId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "uploadId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "path": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "metadata": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.imageMetadata" + }, + "optional": true + }, + "source": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sanity.assetSourceData" + }, + "optional": true + } + } + }, + { + "name": "geopoint", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "geopoint" + } + }, + "lat": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "lng": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "alt": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + } + } + } + } +] diff --git a/apps/sanity/package.json b/apps/sanity/package.json index 9db0ee1e..2fbec41b 100644 --- a/apps/sanity/package.json +++ b/apps/sanity/package.json @@ -6,7 +6,8 @@ "dev": "sanity dev", "build": "sanity build", "deploy": "sanity deploy", - "typegen": "sanity schema extract --path=./extract.json && sanity typegen generate" + "typegen:extract": "sanity schema extract --path=./extract.json --workspace production", + "typegen": "pnpm run typegen:extract" }, "dependencies": { "@codingcatdev/sanity-plugin-podcast-rss": "^1.0.0", diff --git a/apps/sanity/sanity.config.ts b/apps/sanity/sanity.config.ts index bdc63e6d..81ad148a 100644 --- a/apps/sanity/sanity.config.ts +++ b/apps/sanity/sanity.config.ts @@ -60,11 +60,11 @@ const apiVersion = process.env.SANITY_STUDIO_API_VERSION || "2025-09-30"; const presentationEnabled = process.env.SANITY_STUDIO_DISABLE_PRESENTATION !== "true"; -// Use local Astro dev server for presentation preview when running Studio locally +// Use local Next.js dev server for presentation preview when running Studio locally const isLocal = typeof import.meta !== "undefined" && (import.meta as unknown as { env?: { DEV?: boolean } }).env?.DEV; -const localPreviewOrigin = "http://localhost:4321"; +const localPreviewOrigin = "http://localhost:3000"; // ── Shared helpers ─────────────────────────────────────────────────── function resolveHref(type: string, slug?: string): string | undefined { diff --git a/apps/sanity/schemas/singletons/engineConfig.ts b/apps/sanity/schemas/singletons/engineConfig.ts index 953e15f1..9c570a9f 100644 --- a/apps/sanity/schemas/singletons/engineConfig.ts +++ b/apps/sanity/schemas/singletons/engineConfig.ts @@ -104,6 +104,16 @@ export default defineType({ initialValue: 1, validation: (rule) => rule.min(1).max(10), }), + defineField({ + name: 'stuckTimeoutMinutes', + title: 'Stuck Timeout Minutes', + type: 'number', + fieldset: 'pipelineControl', + description: + 'Minutes before a pipeline document is considered stuck and auto-flagged', + initialValue: 30, + validation: (rule) => rule.min(5).max(120), + }), // ─── Content Cadence ───────────────────────────────────────────────── defineField({ diff --git a/apps/web/.dev.vars.example b/apps/web/.dev.vars.example deleted file mode 100644 index 48475b5f..00000000 --- a/apps/web/.dev.vars.example +++ /dev/null @@ -1,15 +0,0 @@ -# Runtime secrets for Cloudflare Worker (wrangler dev / preview). -# Copy to .dev.vars and fill in. In production, set via: wrangler secret put - -# Sanity — viewer token for draft mode / Presentation tool -SANITY_API_READ_TOKEN= -# Optional: set to enable "Turn on draft mode" from site (visit /api/draft-mode/allow-enable?secret=THIS once per session) -# SANITY_PREVIEW_DEV_SECRET= - -# better-auth -BETTER_AUTH_SECRET= -BETTER_AUTH_URL=http://localhost:4321 - -# Google OAuth (better-auth social login) -GOOGLE_CLIENT_ID= -GOOGLE_CLIENT_SECRET= diff --git a/apps/web/.env.example b/apps/web/.env.example index edd325a1..24c919a3 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -1,13 +1,69 @@ -# Build-time env (Astro/Vite). Copy to .env or .env.local. -# Runtime secrets live in .dev.vars (see .dev.vars.example). +# ============================================================================= +# codingcat.dev — Environment Variables +# ============================================================================= +# Copy this file to .env.local and fill in the values. +# See: https://nextjs.org/docs/app/building-your-application/configuring/environment-variables +# Variables prefixed with NEXT_PUBLIC_ are exposed to the browser. +# All other variables are server-only. +# ============================================================================= -# Sanity — used at build; defaults exist in code -SANITY_PROJECT_ID=hfh83o0w -SANITY_DATASET=production +# ----------------------------------------------------------------------------- +# Public (client-side) variables — NEXT_PUBLIC_* +# ----------------------------------------------------------------------------- -# Optional: Visual Editing (local or dev site) -# PUBLIC_SANITY_VISUAL_EDITING_ENABLED=true -# PUBLIC_SANITY_STUDIO_URL=http://localhost:3333 +# Sanity CMS — project configuration +NEXT_PUBLIC_SANITY_PROJECT_ID= # Sanity project ID (from sanity.io/manage) +NEXT_PUBLIC_SANITY_DATASET= # Sanity dataset name (e.g. "prod") +NEXT_PUBLIC_SANITY_API_VERSION= # Sanity API version date (e.g. "2024-01-01") -# Optional: Auth base URL (defaults to http://localhost:4321) -# PUBLIC_APP_URL=http://localhost:4321 +# Site URLs +NEXT_PUBLIC_BASE_URL= # Canonical base URL (e.g. "https://codingcat.dev") +NEXT_PUBLIC_VERCEL_URL= # Vercel preview URL (auto-set by Vercel) + +# Algolia search — public keys +NEXT_PUBLIC_ALGOLIA_APP_ID= # Algolia application ID +NEXT_PUBLIC_ALGOLIA_INDEX= # Algolia index name + +# Analytics +NEXT_PUBLIC_FB_PIXEL_ID= # Facebook Pixel tracking ID + +# Preview mode +NEXT_PUBLIC_PREVIEW_TOKEN_SECRET= # Secret token for Sanity preview mode + +# ----------------------------------------------------------------------------- +# Server-only variables +# ----------------------------------------------------------------------------- + +# Sanity CMS — API tokens +SANITY_API_READ_TOKEN= # Sanity read token (viewer role) +SANITY_API_WRITE_TOKEN= # Sanity write token (editor role) + +# Algolia search — admin keys +PRIVATE_ALGOLIA_ADMIN_API_KEY= # Algolia admin API key (server-side indexing) +PRIVATE_ALGOLIA_WEBOOK_SECRET= # Shared secret for Algolia webhook verification + +# Syndication +PRIVATE_SYNDICATE_WEBOOK_SECRET= # Shared secret for syndication webhook verification +PRIVATE_DEVTO= # Dev.to API key (cross-posting) +PRIVATE_HASHNODE= # Hashnode API key (cross-posting) + +# Cloudflare Turnstile (bot protection) +CLOUDFLARE_TURNSTILE_SECRET_KEY= # Turnstile server-side secret key + +# Cron / scheduled jobs +CRON_SECRET= # Secret for authenticating cron job requests + +# YouTube integration +YOUTUBE_API_KEY= # YouTube Data API v3 key +YOUTUBE_CHANNEL_ID= # YouTube channel ID to fetch videos from +YOUTUBE_UPLOAD_VISIBILITY= # YouTube upload privacy: "public", "private", or "unlisted" (default: "private") + +# Vercel +VERCEL_PROJECT_PRODUCTION_URL= # Production URL (auto-set by Vercel) + +# ----------------------------------------------------------------------------- +# Supabase +# ----------------------------------------------------------------------------- +NEXT_PUBLIC_SUPABASE_URL= # Supabase project URL (e.g. "https://xxx.supabase.co") +NEXT_PUBLIC_SUPABASE_ANON_KEY= # Supabase anonymous/public key +SUPABASE_SERVICE_ROLE_KEY= # Supabase service role key (server-only, bypasses RLS) diff --git a/apps/web/.eslintignore b/apps/web/.eslintignore new file mode 100644 index 00000000..30764a1a --- /dev/null +++ b/apps/web/.eslintignore @@ -0,0 +1,2 @@ +# Ignoring generated files +./sanity.types.ts diff --git a/apps/web/.prettierignore b/apps/web/.prettierignore new file mode 100644 index 00000000..1509c4cd --- /dev/null +++ b/apps/web/.prettierignore @@ -0,0 +1,3 @@ +# Ignoring generated files +./sanity.types.ts +./schema.json diff --git a/apps/web/README.md b/apps/web/README.md deleted file mode 100644 index 27815d7a..00000000 --- a/apps/web/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# CodingCat.dev — Astro 6 Migration - -Fresh Astro 6 project replacing the Next.js site. Deployed to Cloudflare Workers. - -## Stack - -- **Framework:** Astro 6 (SSR mode) -- **Deployment:** Cloudflare Workers via `@astrojs/cloudflare` -- **CMS:** Sanity via `@sanity/astro` -- **Auth:** better-auth + Drizzle + Cloudflare D1 -- **Search:** Sanity Dataset Embeddings -- **Styling:** Tailwind CSS v4 + shadcn/ui -- **Interactive Islands:** React via `@astrojs/react` - -## Getting Started - -From repo root: - -```bash -pnpm install -pnpm --filter @codingcatdev/web dev -``` - -Or from `apps/web`: - -```bash -pnpm install && pnpm dev -``` - -For Cloudflare Workers preview: `pnpm preview` (from `apps/web`). - -## Environment Variables - -- **Build:** Copy `.env.example` to `.env` or `.env.local`. Sanity project/dataset have defaults; optional vars are commented. -- **Runtime (local):** Copy `.dev.vars.example` to `.dev.vars` and set secrets (Sanity token, better-auth, Google OAuth). In production, set these via `wrangler secret put `. - -## Deployment - -```bash -npm run deploy -``` - -Requires `wrangler` CLI authenticated with Cloudflare. diff --git a/apps/web/app/(dashboard)/dashboard/actions.ts b/apps/web/app/(dashboard)/dashboard/actions.ts new file mode 100644 index 00000000..a06586c9 --- /dev/null +++ b/apps/web/app/(dashboard)/dashboard/actions.ts @@ -0,0 +1,10 @@ +"use server"; + +import { redirect } from "next/navigation"; +import { createClient } from "@/lib/supabase/server"; + +export async function signOut() { + const supabase = await createClient(); + await supabase.auth.signOut(); + redirect("/dashboard/login"); +} diff --git a/apps/web/app/(dashboard)/dashboard/auth/callback/route.ts b/apps/web/app/(dashboard)/dashboard/auth/callback/route.ts new file mode 100644 index 00000000..e64179f8 --- /dev/null +++ b/apps/web/app/(dashboard)/dashboard/auth/callback/route.ts @@ -0,0 +1,20 @@ +import { createClient } from "@/lib/supabase/server"; +import { NextResponse } from "next/server"; + +export async function GET(request: Request) { + const { searchParams, origin } = new URL(request.url); + const code = searchParams.get("code"); + const next = searchParams.get("next") ?? "/dashboard"; + + if (code) { + const supabase = await createClient(); + const { error } = await supabase.auth.exchangeCodeForSession(code); + if (!error) { + return NextResponse.redirect(`${origin}${next}`); + } + } + + return NextResponse.redirect( + `${origin}/dashboard/login?error=Could+not+authenticate`, + ); +} diff --git a/apps/web/app/(dashboard)/dashboard/config/config-form.tsx b/apps/web/app/(dashboard)/dashboard/config/config-form.tsx new file mode 100644 index 00000000..01e50f02 --- /dev/null +++ b/apps/web/app/(dashboard)/dashboard/config/config-form.tsx @@ -0,0 +1,597 @@ +"use client"; + +import { useState } from "react"; +import { toast } from "sonner"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { Badge } from "@/components/ui/badge"; +import { Separator } from "@/components/ui/separator"; +import { Loader2, Save, Plus, Trash2, X } from "lucide-react"; +import type { EngineConfig } from "@/lib/types/engine-config"; + +const ALL_DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; + +interface ConfigFormProps { + initialConfig: EngineConfig; +} + +export function ConfigForm({ initialConfig }: ConfigFormProps) { + const [config, setConfig] = useState(initialConfig); + const [saving, setSaving] = useState(false); + const [newCategory, setNewCategory] = useState(""); + + const update = (key: K, value: EngineConfig[K]) => { + setConfig((prev) => ({ ...prev, [key]: value })); + }; + + const handleSave = async () => { + setSaving(true); + try { + const res = await fetch("/api/dashboard/config", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(config), + }); + if (res.ok) { + toast.success("Config saved. Changes propagate within 5 minutes."); + } else { + const data = await res.json(); + toast.error(data.error || "Failed to save config"); + } + } catch { + toast.error("Failed to save config"); + } finally { + setSaving(false); + } + }; + + const toggleDay = (day: string) => { + const days = config.publishDays || []; + update( + "publishDays", + days.includes(day) ? days.filter((d) => d !== day) : [...days, day] + ); + }; + + const addCategory = () => { + const trimmed = newCategory.trim(); + if (!trimmed || (config.contentCategories || []).includes(trimmed)) return; + update("contentCategories", [...(config.contentCategories || []), trimmed]); + setNewCategory(""); + }; + + const removeCategory = (cat: string) => { + update( + "contentCategories", + (config.contentCategories || []).filter((c) => c !== cat) + ); + }; + + const updateTier = ( + index: number, + field: "name" | "description" | "price", + value: string | number + ) => { + const tiers = [...(config.rateCardTiers || [])]; + tiers[index] = { ...tiers[index], [field]: value }; + update("rateCardTiers", tiers); + }; + + const addTier = () => { + update("rateCardTiers", [ + ...(config.rateCardTiers || []), + { name: "", description: "", price: 0 }, + ]); + }; + + const removeTier = (index: number) => { + update( + "rateCardTiers", + (config.rateCardTiers || []).filter((_, i) => i !== index) + ); + }; + + return ( +
+ {/* Pipeline Control */} + + + Pipeline Control + + Control auto-publishing, quality gates, and pipeline limits. + + + +
+ + +
+
+ + + update("qualityThreshold", Number(e.target.value)) + } + className="w-full accent-primary" + /> +
+
+
+ + + update("reviewTimeoutDays", Number(e.target.value)) + } + /> +
+
+ + + update("maxIdeasPerRun", Number(e.target.value)) + } + /> +
+
+
+
+ + {/* Content Cadence */} + + + Content Cadence + + How much content to produce and when to publish. + + + +
+
+ + + update("longFormPerWeek", Number(e.target.value)) + } + /> +
+
+ + + update("shortsPerDay", Number(e.target.value)) + } + /> +
+
+ + + update("blogsPerWeek", Number(e.target.value)) + } + /> +
+
+ +
+ +
+ {ALL_DAYS.map((day) => ( + toggleDay(day)} + > + {day} + + ))} +
+
+ +
+ +
+ {(config.contentCategories || []).map((cat) => ( + + {cat} + + + ))} +
+
+ setNewCategory(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + addCategory(); + } + }} + className="flex-1" + /> + +
+
+
+
+ + {/* AI & Generation */} + + + AI & Generation + + Model selection and system instructions for content generation. + + + +
+
+ + update("geminiModel", e.target.value)} + placeholder="gemini-2.0-flash" + /> +
+
+ + update("infographicModel", e.target.value)} + placeholder="imagen-3.0-generate-002" + /> +
+
+
+ + +
+ + + + +
+ + + diff --git a/apps/site/tests/README.md b/apps/site/tests/README.md new file mode 100644 index 00000000..ef72b680 --- /dev/null +++ b/apps/site/tests/README.md @@ -0,0 +1,92 @@ +# Automated Dual-Track E2E Test Suite Architecture + +## 1. Overview & Architecture + +The test suite for `@codingcatdev/site` implements the **Dual-Track E2E Testing Methodology**, ensuring 100% route, feed, schema, and baseline contract parity between the legacy Next.js application and the Astro 7 SSR Cloudflare Workers target. + +### Dual-Track Strategy +1. **Track 1 — Integrated Route & Handler Contract Verification**: + - Executes Astro 7 endpoints, route logic, pagination resolvers, XML serializers, dynamic OG generators, and draft mode security middleware directly against runtime contracts. + - Verifies HTTP status codes (200, 301, 302, 404), HTTP headers (`Cache-Control`, `X-Robots-Tag`, `Content-Type`), and security protections. +2. **Track 2 — Pre-Migration Baseline Specification Verification**: + - Compares output against canonical baseline snapshots in `apps/site/baseline/`. + - Validates all 472 indexed sitemap URLs, 50 podcast RSS items (with iTunes namespaces and verbatim Sanity `_id` GUIDs), 50 blog RSS items, JSON feed schema v1, and `robots.txt` directives. + +--- + +## 2. Test Suite Organization & Tiers + +The test suite is structured into four progressive tiers located in `apps/site/tests/`: + +| File | Tier | Coverage Scope | Test Count | +|---|---|---|---| +| `apps/site/tests/tier1-features.test.mjs` | **Tier 1** | Feature Coverage across Public Routes, Feeds, Sitemap, Robots, OG Images, Draft Mode | 33 tests | +| `apps/site/tests/tier2-boundaries.test.mjs` | **Tier 2** | Boundary & Corner Cases: Pagination bounds, Non-existent slugs, Podcast GUIDs, Enclosures, XML/CDATA, Stega cleaning | 35 tests | +| `apps/site/tests/tier3-combinations.test.mjs` | **Tier 3** | Cross-Feature Combinations: Redirect rules (301/302), Canonical resolution, Dynamic OG fallback typography, Edge cache poisoning protection | 25 tests | +| `apps/site/tests/tier4-baseline-contract.test.mjs` | **Tier 4** | Real-World Baseline Contracts: 472 Sitemap URLs, 50 Podcast items, 50 Blog items, JSON Feeds, Robots.txt | 22 tests | +| `apps/site/tests/run-e2e.mjs` | **Master Runner** | Orchestrates test execution, tier filtering, and executive summary reporting | 139 checks | + +--- + +## 3. How to Run the Tests + +The test suite is designed for frictionless execution in CI and local developer environments using the native Node.js 22 test runner (`node:test`). No external test dependencies are required. + +### Run All Tiers (Master Runner) +```bash +node apps/site/tests/run-e2e.mjs +``` + +### Run Individual Tiers +```bash +# Run Tier 1 only +node apps/site/tests/run-e2e.mjs --tier=1 + +# Run Tier 2 only +node apps/site/tests/run-e2e.mjs --tier=2 + +# Run Tier 3 only +node apps/site/tests/run-e2e.mjs --tier=3 + +# Run Tier 4 only +node apps/site/tests/run-e2e.mjs --tier=4 +``` + +### Native Node Test Runner Invocation +```bash +node --test apps/site/tests/*.test.mjs +``` + +--- + +## 4. Feature Inventory Matrix + +| Feature Area | Key Routes / Endpoints | Primary Contract | Boundary & Defense Contract | +|---|---|---|---| +| **Public Routes** | `/`, `/blog`, `/post/:slug`, `/podcasts`, `/podcast/:slug`, `/authors`, `/author/:slug`, `/guests`, `/guest/:slug`, `/sponsors`, `/sponsor/:slug`, `/[slug]`, `/404` | Renders responsive layouts, metadata, breadcrumb schemas | 404 on missing entity, server-side status injection | +| **RSS Feeds** | `/blog/rss.xml`, `/podcasts/rss.xml` | RSS 2.0 XML with `dc`, `content`, `itunes`, `atom`, `podcast` namespaces | Exact Sanity `_id` verbatim GUIDs (`isPermaLink="false"`), CDATA split injection defense | +| **JSON Feeds** | `/blog/rss.json`, `/podcasts/rss.json` | JSON Feed v1.0 specification with authors and content | Parity with XML feed IDs and metadata | +| **Sitemap** | `/sitemap.xml` | Dynamic runtime generation of all indexed URLs (`0.9` namespace) | Priority (1, 0.5, 0.1) and changefreq (daily, monthly) contracts | +| **Robots** | `/robots.txt` | Environment-aware: full crawl on prod, `Disallow: /` on dev/worker | Exact byte match against baseline `robots.txt` | +| **Dynamic OG** | `/api/og/default.png`, `/blog.png`, `/podcast.png`, `/person.png` | Satori plain VDOM rendering, cache header `max-age=86400, s-maxage=604800` | Dynamic font scaling: <=40: 56px, 41-60: 48px, >60: 42px | +| **Draft Mode** | `/api/draft-mode/enable`, `/disable` | Cookie `__sanity_preview`, perspective parameter parsing | Fails closed on missing read token, sets `no-store, private` and `noindex, nofollow` | + +--- + +## 5. Implementation Parity & Escalation Log + +The test suite surfaced the following implementation gaps that must be completed by the implementing agent: + +1. **Missing Search Page (`/search`)**: + - **Contract**: `sitemap.xml.ts` advertises `/search`, and `Header.astro` contains a search anchor to `/search`. + - **Current State**: `apps/site/src/pages/search.astro` does not exist. The request falls back to `[slug].astro`, which looks for a Sanity page document with slug `"search"` and returns a 404 response. + - **Action Required**: Create `apps/site/src/pages/search.astro` with the Algolia search island. + +2. **Missing Draft Mode Endpoints (`/api/draft-mode/enable.ts` & `/disable.ts`)**: + - **Contract**: `DraftModeToggle.astro` submits POST to `/api/draft-mode/disable`, and Sanity Studio Presentation navigates to `/api/draft-mode/enable`. + - **Current State**: `apps/site/src/pages/api/draft-mode/` does not exist, causing 404 errors when toggling preview mode. + - **Action Required**: Create `enable.ts` and `disable.ts` in `apps/site/src/pages/api/draft-mode/` to set and clear the `__sanity_preview` cookie. + +3. **Legacy RSS Feed 301 Redirects**: + - **Contract**: Historical subscribers expecting `/rss.xml` and `/podcast/rss.xml` must receive permanent 301 redirects to `/blog/rss.xml` and `/podcasts/rss.xml`. + - **Action Required**: Add explicit 301 redirect rules in `apps/site/astro.config.mjs` or `public/_redirects`. diff --git a/apps/site/tests/cf-mock.mjs b/apps/site/tests/cf-mock.mjs new file mode 100644 index 00000000..1c60631f --- /dev/null +++ b/apps/site/tests/cf-mock.mjs @@ -0,0 +1 @@ +export const env = {}; diff --git a/apps/site/tests/font-bold-mock.mjs b/apps/site/tests/font-bold-mock.mjs new file mode 100644 index 00000000..af7bb692 --- /dev/null +++ b/apps/site/tests/font-bold-mock.mjs @@ -0,0 +1,6 @@ +import fs from "node:fs"; + +const buf = fs.readFileSync( + "/home/alex/codingcat.dev/apps/site/src/assets/fonts/Inter-Bold.ttf", +); +export default new Uint8Array(buf); diff --git a/apps/site/tests/font-reg-mock.mjs b/apps/site/tests/font-reg-mock.mjs new file mode 100644 index 00000000..f5b76655 --- /dev/null +++ b/apps/site/tests/font-reg-mock.mjs @@ -0,0 +1,6 @@ +import fs from "node:fs"; + +const buf = fs.readFileSync( + "/home/alex/codingcat.dev/apps/site/src/assets/fonts/Inter-Regular.ttf", +); +export default new Uint8Array(buf); diff --git a/apps/site/tests/helpers.mjs b/apps/site/tests/helpers.mjs new file mode 100644 index 00000000..e42271e6 --- /dev/null +++ b/apps/site/tests/helpers.mjs @@ -0,0 +1,167 @@ +import fs from "node:fs"; +import { createRequire } from "node:module"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { stegaClean } from "@sanity/client/stega"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const SITE_ROOT = path.resolve(__dirname, ".."); +const BASELINE_DIR = path.resolve(SITE_ROOT, "baseline"); +const SRC_DIR = path.resolve(SITE_ROOT, "src"); + +const require = createRequire(import.meta.url); +const astroPath = require.resolve("astro", { paths: [SITE_ROOT] }); +const jitiPath = require.resolve("jiti", { paths: [astroPath] }); +const { createJiti } = require(jitiPath); + +// Initialize jiti for loading TypeScript modules with @/ alias +const jiti = createJiti(path.resolve(SRC_DIR, "index.ts"), { + alias: { + "@": SRC_DIR, + "cloudflare:workers": path.resolve(__dirname, "cf-mock.mjs"), + [path.resolve(SRC_DIR, "assets/fonts/Inter-Bold.ttf")]: path.resolve( + __dirname, + "font-bold-mock.mjs", + ), + [path.resolve(SRC_DIR, "assets/fonts/Inter-Regular.ttf")]: path.resolve( + __dirname, + "font-reg-mock.mjs", + ), + "../assets/fonts/Inter-Bold.ttf": path.resolve( + __dirname, + "font-bold-mock.mjs", + ), + "../assets/fonts/Inter-Regular.ttf": path.resolve( + __dirname, + "font-reg-mock.mjs", + ), + }, +}); + +export function loadModule(relPath) { + const fullPath = path.resolve(SITE_ROOT, relPath); + return jiti(fullPath); +} + +export function loadBaseline(filename) { + const fullPath = path.resolve(BASELINE_DIR, filename); + return fs.readFileSync(fullPath, "utf-8"); +} + +export function loadBaselineJson(filename) { + return JSON.parse(loadBaseline(filename)); +} + +export { stegaClean, SITE_ROOT, BASELINE_DIR, SRC_DIR }; + +/** + * Creates a mock SanityRequestContext for feed, sitemap, and page testing. + */ +export function createMockSanityContext(overrides = {}) { + const config = { + projectId: "hfh83o0w", + dataset: "production", + apiVersion: "2025-09-30", + studioUrl: "https://codingcat.dev.sanity.studio/production", + ...overrides.config, + }; + + const preview = { + enabled: false, + ...overrides.preview, + }; + + return { + config, + preview, + loadQuery: async (query, params) => { + if (overrides.loadQuery) { + return overrides.loadQuery(query, params); + } + return null; + }, + fetchPublished: async (query, params) => { + if (overrides.fetchPublished) { + return overrides.fetchPublished(query, params); + } + return []; + }, + urlForImage: (source) => { + if (!source) { + return undefined; + } + return { + width: (w) => ({ + height: (h) => ({ + url: () => + `https://cdn.sanity.io/images/${config.projectId}/${config.dataset}/mock-image-${w}x${h}.jpg`, + }), + url: () => + `https://cdn.sanity.io/images/${config.projectId}/${config.dataset}/mock-image-${w}.jpg`, + }), + height: (h) => ({ + url: () => + `https://cdn.sanity.io/images/${config.projectId}/${config.dataset}/mock-image-x${h}.jpg`, + }), + url: () => + `https://cdn.sanity.io/images/${config.projectId}/${config.dataset}/mock-image.jpg`, + }; + }, + resolveOpenGraphImage: (image, width, height) => { + if (!image) { + return undefined; + } + return { + url: `https://cdn.sanity.io/images/${config.projectId}/${config.dataset}/mock-og.png`, + width: width ?? 1200, + height: height ?? 630, + alt: image.alt, + }; + }, + ...overrides, + }; +} + +/** + * Helper to inspect XML tags and attributes + */ +export function extractXmlTags(xml, tagName) { + const regex = new RegExp(`<${tagName}([^>]*)>([\\s\\S]*?)`, "g"); + const matches = []; + let match = regex.exec(xml); + while (match !== null) { + matches.push({ + attributes: match[1].trim(), + content: match[2], + raw: match[0], + }); + match = regex.exec(xml); + } + return matches; +} + +export function extractSelfClosingTags(xml, tagName) { + const regex = new RegExp(`<${tagName}\\s+([^>]*?)\\/>`, "g"); + const matches = []; + let match = regex.exec(xml); + while (match !== null) { + matches.push({ + attributes: match[1].trim(), + raw: match[0], + }); + match = regex.exec(xml); + } + return matches; +} + +export function parseXmlAttributes(attrString) { + const attrs = {}; + const regex = /([a-zA-Z0-9_:-]+)="([^"]*)"/g; + let match = regex.exec(attrString); + while (match !== null) { + attrs[match[1]] = match[2]; + match = regex.exec(attrString); + } + return attrs; +} diff --git a/apps/site/tests/run-e2e.mjs b/apps/site/tests/run-e2e.mjs new file mode 100755 index 00000000..c7d9dcf4 --- /dev/null +++ b/apps/site/tests/run-e2e.mjs @@ -0,0 +1,158 @@ +#!/usr/bin/env node +import path from "node:path"; +import { run } from "node:test"; +import { spec } from "node:test/reporters"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const TIERS = [ + { + tier: 1, + name: "Tier 1 - Feature Coverage", + file: path.resolve(__dirname, "tier1-features.test.mjs"), + areas: [ + "Public content routes (/, /blog, /post/:slug, /podcasts, /podcast/:slug, /authors, /guests, /sponsors, /[slug], /404, /search, /sponsorships)", + "RSS & JSON feeds (/blog/rss.xml, /blog/rss.json, /podcasts/rss.xml, /podcasts/rss.json)", + "Sitemap XML endpoint (/sitemap.xml)", + "Robots text endpoint (/robots.txt)", + "Dynamic OG image endpoints (/api/og/default.png, blog.png, podcast.png, person.png)", + "Draft mode contracts and preview resolution (/api/draft-mode/enable, disable)", + ], + }, + { + tier: 2, + name: "Tier 2 - Boundary & Corner Cases", + file: path.resolve(__dirname, "tier2-boundaries.test.mjs"), + areas: [ + "Pagination bounds checking (page 0, negative pages, non-numeric, decimal/whitespace, out of bounds redirect 302, zero-item clamp)", + "Non-existent slug resolution (404 status contracts across post, podcast, author, guest, sponsor, catch-all page)", + 'Podcast RSS GUID verification (verbatim Sanity _id preservation in , anti-resyndication safety)', + "Audio enclosures validation (MP3 URLs, positive byte lengths, audio/mpeg type, empty fallback, special char escaping)", + "XML entity escaping & CDATA termination defense (predefined XML entities, ]]> splitting injection defense)", + "Stega cleaning enforcement (zero-width characters stripped from attributes, canonicals, OG tags, metadata)", + ], + }, + { + tier: 3, + name: "Tier 3 - Cross-Feature Combinations", + file: path.resolve(__dirname, "tier3-combinations.test.mjs"), + areas: [ + "Redirect rules (listing stubs /blog/page -> /1 (302), /authors (302), /guests (302), /sponsors (302), legacy feeds (301))", + "Canonical URL resolution via Astro.locals.siteUrl (production, dev worker, origin normalization, path joining)", + "Dynamic OG fallback scaling & long titles (<=40: 56px, 41-60: 48px, >60: 42px, extreme length, special chars, monograms)", + "Draft mode cookie handling & cache poisoning prevention (__sanity_preview -> no-store, private & noindex, nofollow)", + ], + }, + { + tier: 4, + name: "Tier 4 - Real-World Application & Baseline Contract", + file: path.resolve(__dirname, "tier4-baseline-contract.test.mjs"), + areas: [ + "Baseline sitemap contract (exact match against 472 URLs: 1 root, 1 search, 69 posts, 194 podcasts, 7 pages, 5 authors, 186 guests, 9 sponsors)", + "Baseline podcast RSS feed contract (exact match against 50 baseline items, iTunes tags, atom self link, GUID fidelity)", + "Baseline blog RSS feed contract (exact match against 50 baseline items, CDATA descriptions, JSON feed parity)", + "Baseline robots.txt contract (byte-for-byte exact line match against baseline in production environment)", + ], + }, +]; + +async function main() { + console.log( + "================================================================================", + ); + console.log( + " @codingcatdev/site DUAL-TRACK AUTOMATED E2E TEST SUITE RUNNER ", + ); + console.log( + "================================================================================", + ); + + const args = process.argv.slice(2); + const tierArg = args.find((a) => a.startsWith("--tier=")); + const selectedTier = tierArg ? parseInt(tierArg.split("=")[1], 10) : null; + + const targetTiers = selectedTier + ? TIERS.filter((t) => t.tier === selectedTier) + : TIERS; + + if (targetTiers.length === 0) { + console.error("Error: Unknown tier. Available tiers: 1, 2, 3, 4."); + process.exit(1); + } + + const filesToRun = targetTiers.map((t) => t.file); + console.log( + "Running target tier(s): " + + targetTiers.map((t) => `Tier ${t.tier}`).join(", ") + + "\n", + ); + + const startTime = Date.now(); + let totalTests = 0; + let passedTests = 0; + let failedTests = 0; + + const stream = run({ + files: filesToRun, + }); + + stream.compose(new spec()).pipe(process.stdout); + + stream.on("test:pass", () => { + passedTests++; + totalTests++; + }); + + stream.on("test:fail", () => { + failedTests++; + totalTests++; + }); + + await new Promise((resolve) => stream.on("end", resolve)); + + const durationMs = Date.now() - startTime; + + console.log( + "\n================================================================================", + ); + console.log( + " E2E TEST EXECUTION SUMMARY ", + ); + console.log( + "================================================================================", + ); + for (const t of targetTiers) { + console.log(`\n• ${t.name}:`); + for (const a of t.areas) { + console.log(` ✓ ${a}`); + } + } + + console.log( + "\n--------------------------------------------------------------------------------", + ); + console.log(`Total Test Cases Executed : ${totalTests}`); + console.log(`Passed : ${passedTests}`); + console.log(`Failed : ${failedTests}`); + console.log(`Duration : ${durationMs}ms`); + console.log( + "--------------------------------------------------------------------------------", + ); + + if (failedTests > 0) { + console.error(`\n❌ TEST SUITE FAILED with ${failedTests} failure(s).`); + process.exit(1); + } else { + console.log( + "\n✅ ALL E2E TEST SUITES PASSED! Dual-Track Verification Complete.", + ); + process.exit(0); + } +} + +main().catch((err) => { + console.error("Test runner encountered an error:", err); + process.exit(1); +}); diff --git a/apps/site/tests/setup.mjs b/apps/site/tests/setup.mjs new file mode 100644 index 00000000..3d6576ea --- /dev/null +++ b/apps/site/tests/setup.mjs @@ -0,0 +1 @@ +export const ok = true; diff --git a/apps/site/tests/tier1-features.test.mjs b/apps/site/tests/tier1-features.test.mjs new file mode 100644 index 00000000..0682b83f --- /dev/null +++ b/apps/site/tests/tier1-features.test.mjs @@ -0,0 +1,1008 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { describe, it } from "node:test"; +import { + createMockSanityContext, + loadBaseline, + loadModule, + SRC_DIR, +} from "./helpers.mjs"; + +describe("Tier 1 - Feature Coverage", () => { + // ── 1. Public Content Routes ────────────────────────────────────────── + describe("Public Content Routes", () => { + it("1.1 Core page files exist in src/pages/", () => { + const requiredPages = [ + "index.astro", + "404.astro", + "[slug].astro", + "blog/index.astro", + "blog/page/[num].astro", + "post/[slug].astro", + "podcasts/index.astro", + "podcasts/page/[num].astro", + "podcast/[slug].astro", + "authors/page/[num].astro", + "author/[slug].astro", + "guests/page/[num].astro", + "guest/[slug].astro", + "sponsors/page/[num].astro", + "sponsor/[slug].astro", + "search.astro", + "sponsorships.astro", + ]; + + for (const p of requiredPages) { + const fullPath = path.resolve(SRC_DIR, "pages", p); + assert.ok( + fs.existsSync(fullPath), + `Required page ${p} should exist on disk`, + ); + } + }); + + it("1.2 [slug].astro catch-all handles page documents and 404s missing slugs", () => { + const slugFile = fs.readFileSync( + path.resolve(SRC_DIR, "pages/[slug].astro"), + "utf-8", + ); + assert.ok( + slugFile.includes("pageQuery"), + "[slug].astro must load pageQuery", + ); + assert.ok( + slugFile.includes("return new Response(null, { status: 404 });"), + "[slug].astro must return 404 when document is not found", + ); + assert.ok( + slugFile.includes("PortableTextContent"), + "[slug].astro must render PortableTextContent", + ); + }); + + it("1.3 Pagination routes use resolvePage helper with numeric bounds", () => { + const blogNumFile = fs.readFileSync( + path.resolve(SRC_DIR, "pages/blog/page/[num].astro"), + "utf-8", + ); + assert.ok( + blogNumFile.includes("resolvePage"), + "Blog pagination must import resolvePage", + ); + assert.ok( + blogNumFile.includes("Astro.params.num"), + "Blog pagination must read num param", + ); + + const podcastsNumFile = fs.readFileSync( + path.resolve(SRC_DIR, "pages/podcasts/page/[num].astro"), + "utf-8", + ); + assert.ok( + podcastsNumFile.includes("resolvePage"), + "Podcasts pagination must import resolvePage", + ); + }); + + it("1.4 Detail routes verify document existence and return 404 on missing entity", () => { + const postFile = fs.readFileSync( + path.resolve(SRC_DIR, "pages/post/[slug].astro"), + "utf-8", + ); + assert.ok( + postFile.includes("return new Response(null, { status: 404 });"), + "post/[slug].astro must 404 missing posts", + ); + + const podcastFile = fs.readFileSync( + path.resolve(SRC_DIR, "pages/podcast/[slug].astro"), + "utf-8", + ); + assert.ok( + podcastFile.includes("return new Response(null, { status: 404 });"), + "podcast/[slug].astro must 404 missing podcasts", + ); + }); + + it("1.5 404 page exists and renders BaseLayout with title and status", () => { + const errPage = fs.readFileSync( + path.resolve(SRC_DIR, "pages/404.astro"), + "utf-8", + ); + assert.ok( + errPage.includes("BaseLayout"), + "404.astro must render BaseLayout", + ); + assert.ok( + errPage.includes("Page not found"), + "404.astro must declare Page not found", + ); + assert.ok( + errPage.includes("Astro.response.status = 404;"), + "404.astro must set status = 404", + ); + assert.ok( + errPage.includes("This page wandered off"), + "404.astro must render error message", + ); + }); + + it("1.6 Route resolution contract check: /search and /sponsorships exist as dedicated routes", () => { + const searchPageExists = fs.existsSync( + path.resolve(SRC_DIR, "pages/search.astro"), + ); + const sponsorshipsPageExists = fs.existsSync( + path.resolve(SRC_DIR, "pages/sponsorships.astro"), + ); + assert.ok(searchPageExists, "search.astro exists"); + assert.ok(sponsorshipsPageExists, "sponsorships.astro exists"); + + const sitemapSrc = fs.readFileSync( + path.resolve(SRC_DIR, "pages/sitemap.xml.ts"), + "utf-8", + ); + assert.ok( + sitemapSrc.includes("/search"), + "sitemap.xml.ts advertises /search route", + ); + }); + + it("1.7 search.astro and SearchModal render semantic search UI with filters, modal, and breadcrumbs", () => { + const searchFile = fs.readFileSync( + path.resolve(SRC_DIR, "pages/search.astro"), + "utf-8", + ); + const modalFile = fs.readFileSync( + path.resolve(SRC_DIR, "components/SearchModal.astro"), + "utf-8", + ); + const apiFile = fs.readFileSync( + path.resolve(SRC_DIR, "pages/api/search.ts"), + "utf-8", + ); + assert.ok(searchFile.includes("BaseLayout"), "Must use BaseLayout"); + assert.ok(searchFile.includes("Breadcrumbs"), "Must render Breadcrumbs"); + assert.ok(searchFile.includes("search-input"), "Must have search input"); + assert.ok(searchFile.includes("/api/search"), "Must query /api/search"); + assert.ok( + searchFile.includes('data-type="post"'), + "Must have post filter", + ); + assert.ok( + searchFile.includes('data-type="podcast"'), + "Must have podcast filter", + ); + assert.ok( + searchFile.includes('data-type="author"'), + "Must have author filter", + ); + assert.ok( + searchFile.includes('data-type="guest"'), + "Must have guest filter", + ); + assert.ok( + searchFile.includes("state-ready"), + "Must have ready empty state", + ); + assert.ok( + modalFile.includes("search-modal"), + "SearchModal must define modal dialog", + ); + assert.ok( + apiFile.includes("semanticSearchQuery"), + "API must utilize semanticSearchQuery", + ); + }); + + it("1.8 sponsorships.astro renders the 5 tiers and inquiry form", () => { + const sponsorFile = fs.readFileSync( + path.resolve(SRC_DIR, "pages/sponsorships.astro"), + "utf-8", + ); + assert.ok(sponsorFile.includes("BaseLayout"), "Must use BaseLayout"); + assert.ok(sponsorFile.includes("Breadcrumbs"), "Must render Breadcrumbs"); + assert.ok(sponsorFile.includes("Dedicated Video"), "Must render Tier 1"); + assert.ok( + sponsorFile.includes("Integrated Mid-Roll Ad"), + "Must render Tier 2", + ); + assert.ok(sponsorFile.includes("Quick Shout-Out"), "Must render Tier 3"); + assert.ok( + sponsorFile.includes("Blog Post / Newsletter"), + "Must render Tier 4", + ); + assert.ok(sponsorFile.includes("Video Series"), "Must render Tier 5"); + assert.ok( + sponsorFile.includes("sponsorship-form"), + "Must include inquiry form", + ); + }); + + it("1.9 /api/sponsorship endpoint validates required fields and handles submissions", async () => { + const sponsorshipModule = loadModule("src/pages/api/sponsorship.ts"); + assert.equal(typeof sponsorshipModule.POST, "function"); + + // Valid JSON submission + const validRes = await sponsorshipModule.POST({ + request: new Request("https://codingcat.dev/api/sponsorship", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: "Jane Dev", + email: "jane@example.com", + company: "Tech Corp", + tier: "Dedicated Video", + message: "Looking for sponsorship in Q4", + }), + }), + }); + assert.equal(validRes.status, 200); + const validJson = await validRes.json(); + assert.equal(validJson.success, true); + assert.equal(validJson.message, "Inquiry received"); + + // Valid form-encoded submission + const formParams = new URLSearchParams({ + name: "John Dev", + email: "john@example.com", + tier: "Quick Shout-Out", + }); + const validFormRes = await sponsorshipModule.POST({ + request: new Request("https://codingcat.dev/api/sponsorship", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: formParams.toString(), + }), + }); + assert.equal(validFormRes.status, 200); + const formJson = await validFormRes.json(); + assert.equal(formJson.success, true); + + // Missing email returns 400 + const missingEmailRes = await sponsorshipModule.POST({ + request: new Request("https://codingcat.dev/api/sponsorship", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: "Jane Dev", tier: "Dedicated Video" }), + }), + }); + assert.equal(missingEmailRes.status, 400); + + // Missing name returns 400 + const missingNameRes = await sponsorshipModule.POST({ + request: new Request("https://codingcat.dev/api/sponsorship", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: "jane@example.com", + tier: "Dedicated Video", + }), + }), + }); + assert.equal(missingNameRes.status, 400); + + // Missing tier returns 400 + const missingTierRes = await sponsorshipModule.POST({ + request: new Request("https://codingcat.dev/api/sponsorship", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: "Jane Dev", email: "jane@example.com" }), + }), + }); + assert.equal(missingTierRes.status, 400); + }); + }); + + // ── 2. RSS & JSON Feeds ─────────────────────────────────────────────── + describe("RSS & JSON Feeds", () => { + const feeds = loadModule("src/lib/feeds.ts"); + + it("2.1 buildRss2 produces valid RSS 2.0 with XML declaration and namespaces", () => { + const mockSanity = createMockSanityContext(); + const now = new Date("2026-08-18T12:00:00Z"); + const xml = feeds.buildRss2({ + origin: "https://codingcat.dev", + type: "post", + items: [ + { + _id: "post-1", + _type: "post", + title: "Test Post", + slug: "test-post", + excerpt: "A test excerpt", + date: "2026-08-10T00:00:00Z", + author: [{ title: "Alex Patterson", slug: "alex-patterson" }], + }, + ], + now, + sanity: mockSanity, + }); + + assert.ok( + xml.startsWith(``), + "Must have XML declaration", + ); + assert.ok(xml.includes(`CodingCat.dev - post feed"), + "Must have channel title", + ); + assert.ok( + xml.includes("https://codingcat.dev/blog"), + "Must have channel link", + ); + }); + + it("2.2 buildRss2 wraps title and excerpt in CDATA and preserves Sanity _id as GUID", () => { + const mockSanity = createMockSanityContext(); + const xml = feeds.buildRss2({ + origin: "https://codingcat.dev", + type: "post", + items: [ + { + _id: "guid-sanity-12345", + _type: "post", + title: "Exploring & Astro", + slug: "exploring-next-astro", + excerpt: "Comparing frameworks", + date: "2026-08-10T00:00:00Z", + }, + ], + now: new Date(), + sanity: mockSanity, + }); + + assert.ok( + xml.includes("<![CDATA[Exploring <Next.js> & Astro]]>"), + "Title must be CDATA wrapped", + ); + assert.ok( + xml.includes( + "frameworks]]>", + ), + "Excerpt must be CDATA wrapped", + ); + assert.ok( + xml.includes(`guid-sanity-12345`), + "Sanity _id must be verbatim GUID with isPermaLink=false", + ); + }); + + it("2.3 buildPodcastRss declares itunes, atom, and podcast namespaces", () => { + const mockSanity = createMockSanityContext(); + const xml = feeds.buildPodcastRss({ + origin: "https://codingcat.dev", + type: "podcast", + items: [], + now: new Date("2026-08-18T12:00:00Z"), + sanity: mockSanity, + }); + + assert.ok( + xml.includes( + `xmlns:itunes="http://www.itunes.apple.com/dtds/podcast-1.0.dtd"`, + ), + "Must declare itunes namespace", + ); + assert.ok( + xml.includes(`xmlns:atom="http://www.w3.org/2005/Atom"`), + "Must declare atom namespace", + ); + assert.ok( + xml.includes(`xmlns:podcast="https://podcastindex.org/namespace/1.0"`), + "Must declare podcast namespace", + ); + assert.ok( + xml.includes( + ``, + ), + "Must have atom self link", + ); + assert.ok( + xml.includes(''), + "Must declare Technology category", + ); + assert.ok( + xml.includes("false"), + "Must declare itunes:explicit", + ); + }); + + it("2.4 buildPodcastRss serializes audio enclosure with url, length, and audio/mpeg type", () => { + const mockSanity = createMockSanityContext(); + const xml = feeds.buildPodcastRss({ + origin: "https://codingcat.dev", + type: "podcast", + items: [ + { + _id: "pod-1", + _type: "podcast", + title: "Episode 1: Astro Migration", + slug: "episode-1-astro-migration", + spotify: { + enclosures: [ + { + url: "https://anchor.fm/s/123/play/456/audio.mp3", + length: 12345678, + type: "audio/mpeg", + }, + ], + itunes: { + duration: "00:45:30", + episodeType: "full", + }, + }, + season: 2, + episode: 5, + }, + ], + now: new Date(), + sanity: mockSanity, + }); + + assert.ok( + xml.includes( + ``, + ), + "Enclosure tag must match", + ); + assert.ok( + xml.includes("00:45:30"), + "Duration tag must match", + ); + assert.ok( + xml.includes("2"), + "Season tag must match", + ); + assert.ok( + xml.includes("5"), + "Episode tag must match", + ); + }); + + it("2.5 buildJsonFeed outputs JSON Feed version 1 compliant structure", () => { + const mockSanity = createMockSanityContext(); + const jsonStr = feeds.buildJsonFeed({ + origin: "https://codingcat.dev", + type: "post", + items: [ + { + _id: "json-1", + _type: "post", + title: "JSON Feed Test", + slug: "json-feed-test", + excerpt: "Summary text", + date: "2026-08-10T00:00:00Z", + }, + ], + now: new Date(), + sanity: mockSanity, + }); + + const feed = JSON.parse(jsonStr); + assert.equal(feed.version, "https://jsonfeed.org/version/1"); + assert.equal(feed.home_page_url, "https://codingcat.dev/blog"); + assert.equal(feed.feed_url, "https://codingcat.dev/blog/rss.json"); + assert.equal(feed.items.length, 1); + assert.equal(feed.items[0].id, "json-1"); + assert.equal( + feed.items[0].url, + "https://codingcat.dev/post/json-feed-test", + ); + assert.equal(feed.items[0].title, "JSON Feed Test"); + }); + + it("2.6 Feeds enforce FEED_LIMIT constant = 50", () => { + assert.equal( + feeds.FEED_LIMIT, + 50, + "FEED_LIMIT must be 50 to avoid feed bloat", + ); + }); + }); + + // ── 3. Sitemap ──────────────────────────────────────────────────────── + describe("Sitemap", () => { + const sitemapModule = loadModule("src/pages/sitemap.xml.ts"); + + it("3.1 sitemap.xml.ts exports GET handler", () => { + assert.equal( + typeof sitemapModule.GET, + "function", + "GET handler must be exported", + ); + }); + + it("3.2 Sitemap response includes correct Content-Type and Cache-Control headers", async () => { + const mockSanity = createMockSanityContext({ + fetchPublished: async () => [], + }); + const locals = { + sanity: mockSanity, + siteUrl: new URL("https://codingcat.dev"), + }; + + const res = await sitemapModule.GET({ locals }); + assert.equal(res.status, 200); + assert.equal( + res.headers.get("content-type"), + "application/xml; charset=utf-8", + ); + assert.equal( + res.headers.get("cache-control"), + "max-age=0, s-maxage=3600", + ); + }); + + it("3.3 Sitemap root and /search entries have correct priority and changefreq", async () => { + const mockSanity = createMockSanityContext({ + fetchPublished: async () => [], + }); + const locals = { + sanity: mockSanity, + siteUrl: new URL("https://codingcat.dev"), + }; + + const res = await sitemapModule.GET({ locals }); + const xml = await res.text(); + + assert.ok( + xml.includes("https://codingcat.dev"), + "Root URL must be present", + ); + assert.ok( + xml.includes("1"), + "Root URL priority must be 1", + ); + assert.ok( + xml.includes("https://codingcat.dev/search"), + "/search must be present", + ); + assert.ok( + xml.includes("0.1"), + "/search priority must be 0.1", + ); + assert.ok( + xml.includes("daily"), + "/search changefreq must be daily", + ); + }); + + it("3.4 Sitemap maps page types to /:slug and content types to /:type/:slug", async () => { + const mockSanity = createMockSanityContext({ + fetchPublished: async () => [ + { + _type: "page", + slug: "terms-of-use", + _updatedAt: "2026-08-01T00:00:00.000Z", + }, + { + _type: "post", + slug: "sample-tutorial", + _updatedAt: "2026-08-02T00:00:00.000Z", + }, + { + _type: "podcast", + slug: "sample-episode", + _updatedAt: "2026-08-03T00:00:00.000Z", + }, + ], + }); + const locals = { + sanity: mockSanity, + siteUrl: new URL("https://codingcat.dev"), + }; + + const res = await sitemapModule.GET({ locals }); + const xml = await res.text(); + + assert.ok( + xml.includes("https://codingcat.dev/terms-of-use"), + "page type maps to /terms-of-use", + ); + assert.ok( + xml.includes("https://codingcat.dev/post/sample-tutorial"), + "post type maps to /post/sample-tutorial", + ); + assert.ok( + xml.includes("https://codingcat.dev/podcast/sample-episode"), + "podcast type maps to /podcast/sample-episode", + ); + }); + + it("3.5 Sitemap conforms to urlset schema xmlns http://www.sitemaps.org/schemas/sitemap/0.9", async () => { + const mockSanity = createMockSanityContext({ + fetchPublished: async () => [], + }); + const locals = { + sanity: mockSanity, + siteUrl: new URL("https://codingcat.dev"), + }; + + const res = await sitemapModule.GET({ locals }); + const xml = await res.text(); + assert.ok( + xml.includes( + ``, + ), + "Must declare sitemap schema namespace", + ); + }); + }); + + // ── 4. Robots.txt ───────────────────────────────────────────────────── + describe("Robots.txt", () => { + const robotsModule = loadModule("src/pages/robots.txt.ts"); + + it("4.1 robots.txt.ts exports GET handler", () => { + assert.equal( + typeof robotsModule.GET, + "function", + "GET handler must be exported", + ); + }); + + it("4.2 Production robots allows root and disallows /api/ and /dashboard/", async () => { + const locals = { siteUrl: new URL("https://codingcat.dev") }; + const res = await robotsModule.GET({ locals }); + const body = await res.text(); + + assert.ok(body.includes("User-Agent: *")); + assert.ok(body.includes("Allow: /")); + assert.ok(body.includes("Disallow: /api/")); + assert.ok(body.includes("Disallow: /dashboard/")); + assert.ok(body.includes("Host: https://codingcat.dev")); + assert.ok(body.includes("Sitemap: https://codingcat.dev/sitemap.xml")); + }); + + it("4.3 Non-production robots disallows all crawling (Disallow: /)", async () => { + const locals = { siteUrl: new URL("https://codingcatdev.workers.dev") }; + const res = await robotsModule.GET({ locals }); + const body = await res.text(); + + assert.ok(body.includes("User-Agent: *")); + assert.ok(body.includes("Disallow: /")); + assert.ok( + !body.includes("Allow: /"), + "Non-production must not contain Allow: /", + ); + assert.ok( + !body.includes("Host:"), + "Non-production must not contain Host:", + ); + }); + + it("4.4 Robots response headers set text/plain; charset=utf-8 and cache-control", async () => { + const locals = { siteUrl: new URL("https://codingcat.dev") }; + const res = await robotsModule.GET({ locals }); + + assert.equal(res.status, 200); + assert.equal( + res.headers.get("content-type"), + "text/plain; charset=utf-8", + ); + assert.equal( + res.headers.get("cache-control"), + "max-age=0, s-maxage=3600", + ); + }); + + it("4.5 Production output matches baseline robots.txt byte-for-byte", async () => { + const baseline = loadBaseline("robots.txt"); + const locals = { siteUrl: new URL("https://codingcat.dev") }; + const res = await robotsModule.GET({ locals }); + const body = await res.text(); + + assert.equal( + body.trim(), + baseline.trim(), + "Production robots.txt output must match baseline snapshot", + ); + }); + }); + + // ── 5. Dynamic OG Images ────────────────────────────────────────────── + describe("Dynamic OG Images", () => { + const ogUtils = loadModule("src/lib/og-utils.ts"); + + it("5.1 OG cache header matches contract (public, max-age=86400, s-maxage=604800)", () => { + assert.equal( + ogUtils.OG_CACHE_HEADER, + "public, max-age=86400, s-maxage=604800", + ); + }); + + it("5.2 Brand tokens define exact hex values for Satori compatibility", () => { + assert.equal(ogUtils.BRAND.bg, "#000000"); + assert.equal(ogUtils.BRAND.primary, "#7c3aed"); + assert.equal(ogUtils.BRAND.text, "#ffffff"); + assert.equal(ogUtils.TYPE_COLORS.Blog, "#3b82f6"); + assert.equal(ogUtils.TYPE_COLORS.Podcast, "#f59e0b"); + assert.equal(ogUtils.TYPE_COLORS.Author, "#ec4899"); + }); + + it("5.3 titleFontSize dynamically scales for lengths: <=40 (56), 41-60 (48), >60 (42)", () => { + assert.equal(ogUtils.titleFontSize("Short Title"), 56); + assert.equal(ogUtils.titleFontSize("A".repeat(40)), 56); + assert.equal(ogUtils.titleFontSize("A".repeat(41)), 48); + assert.equal(ogUtils.titleFontSize("A".repeat(60)), 48); + assert.equal(ogUtils.titleFontSize("A".repeat(61)), 42); + assert.equal(ogUtils.titleFontSize("A".repeat(120)), 42); + }); + + it("5.4 All 4 OG image endpoints exist and export GET handler with parameter parsing", () => { + const endpoints = [ + { file: "default.png.ts", expectedParams: ["title", "subtitle"] }, + { file: "blog.png.ts", expectedParams: ["title", "author", "type"] }, + { + file: "podcast.png.ts", + expectedParams: ["title", "author", "episode"], + }, + { file: "person.png.ts", expectedParams: ["title", "type"] }, + ]; + + for (const { file, expectedParams } of endpoints) { + const fullPath = path.resolve(SRC_DIR, "pages/api/og", file); + assert.ok(fs.existsSync(fullPath), `OG endpoint ${file} must exist`); + const fileContent = fs.readFileSync(fullPath, "utf-8"); + assert.ok( + fileContent.includes("export const GET"), + `${file} must export GET handler`, + ); + assert.ok( + fileContent.includes("ImageResponse"), + `${file} must return ImageResponse`, + ); + assert.ok( + fileContent.includes("OG_CACHE_HEADER"), + `${file} must set OG_CACHE_HEADER`, + ); + for (const param of expectedParams) { + assert.ok( + fileContent.includes(param), + `${file} must parse param ${param}`, + ); + } + } + }); + + it("5.5 Font loader loads Inter Bold and Regular fonts with correct metadata", () => { + const fonts = ogUtils.loadFonts(); + assert.equal(fonts.length, 3); + assert.equal(fonts[0].name, "Inter"); + assert.equal(fonts[0].weight, 700); + assert.equal(fonts[1].weight, 400); + assert.equal(fonts[2].weight, 500); + }); + }); + + // ── 6. Draft Mode ───────────────────────────────────────────────────── + describe("Draft Mode", () => { + const previewModule = loadModule("src/lib/sanity/preview.ts"); + + it("6.1 PREVIEW_COOKIE and PERSPECTIVE_PARAM constants are properly defined", () => { + assert.equal(previewModule.PREVIEW_COOKIE, "__sanity_preview"); + assert.equal( + previewModule.PERSPECTIVE_PARAM, + "sanity-preview-perspective", + ); + }); + + it("6.2 resolvePreview disables preview when cookie is absent", () => { + const result = previewModule.resolvePreview({ + env: { SANITY_API_READ_TOKEN: "valid-token" }, + hasPreviewCookie: false, + perspectiveParam: null, + }); + assert.deepEqual(result, { enabled: false }); + }); + + it("6.3 resolvePreview fails closed when read token is missing despite cookie", () => { + const result = previewModule.resolvePreview({ + env: {}, // No SANITY_API_READ_TOKEN + hasPreviewCookie: true, + perspectiveParam: "drafts", + }); + assert.deepEqual( + result, + { enabled: false }, + "Must fail closed if SANITY_API_READ_TOKEN is missing", + ); + }); + + it("6.4 resolvePreview enables preview with drafts perspective when token and cookie are present", () => { + const result = previewModule.resolvePreview({ + env: { SANITY_API_READ_TOKEN: "sk_secret_123" }, + hasPreviewCookie: true, + perspectiveParam: null, // default + }); + assert.equal(result.enabled, true); + assert.equal(result.token, "sk_secret_123"); + assert.equal(result.perspective, "drafts"); + }); + + it("6.5 resolvePreview parses perspective parameter correctly (published, drafts, releases)", () => { + const resPub = previewModule.resolvePreview({ + env: { SANITY_API_READ_TOKEN: "tok" }, + hasPreviewCookie: true, + perspectiveParam: "published", + }); + assert.equal(resPub.perspective, "published"); + + const resRelease = previewModule.resolvePreview({ + env: { SANITY_API_READ_TOKEN: "tok" }, + hasPreviewCookie: true, + perspectiveParam: "summer-2026,autumn-2026", + }); + assert.deepEqual(resRelease.perspective, ["summer-2026", "autumn-2026"]); + }); + + it("6.6 GET /api/draft-mode/enable sets __sanity_preview cookie and redirects", async () => { + const enableModule = loadModule("src/pages/api/draft-mode/enable.ts"); + assert.equal(typeof enableModule.GET, "function"); + + let setCookieName = null; + let setCookieVal = null; + let setCookieOpts = null; + + const res = await enableModule.GET({ + url: new URL( + "https://codingcat.dev/api/draft-mode/enable?returnTo=/post/sample-draft", + ), + cookies: { + set: (name, val, opts) => { + setCookieName = name; + setCookieVal = val; + setCookieOpts = opts; + }, + }, + redirect: (dest, status) => + new Response(null, { status, headers: { Location: dest } }), + }); + + assert.equal(res.status, 307); + assert.equal(res.headers.get("Location"), "/post/sample-draft"); + assert.equal(setCookieName, "__sanity_preview"); + assert.equal(setCookieVal, "true"); + assert.equal(setCookieOpts.httpOnly, true); + assert.equal(setCookieOpts.secure, true); + assert.equal(setCookieOpts.sameSite, "lax"); + }); + + it("6.7 POST and GET /api/draft-mode/disable clears __sanity_preview cookie and redirects", async () => { + const disableModule = loadModule("src/pages/api/draft-mode/disable.ts"); + assert.equal(typeof disableModule.POST, "function"); + assert.equal(typeof disableModule.GET, "function"); + + let deletedCookie = null; + const mockCookies = { + delete: (name) => { + deletedCookie = name; + }, + set: () => {}, + }; + + // POST request with form-encoded returnTo + const formBody = new URLSearchParams({ returnTo: "/blog/page/1" }); + const postRes = await disableModule.POST({ + request: new Request("https://codingcat.dev/api/draft-mode/disable", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: formBody.toString(), + }), + url: new URL("https://codingcat.dev/api/draft-mode/disable"), + cookies: mockCookies, + redirect: (dest, status) => + new Response(null, { status, headers: { Location: dest } }), + }); + + assert.equal(postRes.status, 303); + assert.equal(postRes.headers.get("Location"), "/blog/page/1"); + assert.equal(deletedCookie, "__sanity_preview"); + + // GET request with query param returnTo + deletedCookie = null; + const getRes = await disableModule.GET({ + url: new URL( + "https://codingcat.dev/api/draft-mode/disable?returnTo=/podcasts/page/1", + ), + cookies: mockCookies, + redirect: (dest, status) => + new Response(null, { status, headers: { Location: dest } }), + }); + + assert.equal(getRes.status, 307); + assert.equal(getRes.headers.get("Location"), "/podcasts/page/1"); + assert.equal(deletedCookie, "__sanity_preview"); + }); + + it("6.8 Draft mode endpoints reject backslashes and protocol-relative destinations to prevent open redirects", async () => { + const enableModule = loadModule("src/pages/api/draft-mode/enable.ts"); + const disableModule = loadModule("src/pages/api/draft-mode/disable.ts"); + + const mockCookies = { + set: () => {}, + delete: () => {}, + }; + const mockRedirect = (dest, status) => + new Response(null, { status, headers: { Location: dest } }); + + // 1. enable with returnTo=/\\evil.com + const resEnableBackslash = await enableModule.GET({ + url: new URL( + "https://codingcat.dev/api/draft-mode/enable?returnTo=/\\evil.com", + ), + cookies: mockCookies, + redirect: mockRedirect, + }); + assert.equal(resEnableBackslash.headers.get("Location"), "/"); + + // 2. enable with slug=\\evil.com + const resEnableSlugBackslash = await enableModule.GET({ + url: new URL( + "https://codingcat.dev/api/draft-mode/enable?slug=\\evil.com", + ), + cookies: mockCookies, + redirect: mockRedirect, + }); + assert.equal(resEnableSlugBackslash.headers.get("Location"), "/"); + + // 3. enable with protocol-relative returnTo=//evil.com + const resEnableProtocolRelative = await enableModule.GET({ + url: new URL( + "https://codingcat.dev/api/draft-mode/enable?returnTo=//evil.com", + ), + cookies: mockCookies, + redirect: mockRedirect, + }); + assert.equal(resEnableProtocolRelative.headers.get("Location"), "/"); + + // 4. disable GET with returnTo=/\\evil.com + const resDisableBackslash = await disableModule.GET({ + url: new URL( + "https://codingcat.dev/api/draft-mode/disable?returnTo=/\\evil.com", + ), + cookies: mockCookies, + redirect: mockRedirect, + }); + assert.equal(resDisableBackslash.headers.get("Location"), "/"); + + // 5. disable POST form with returnTo=/\\evil.com + const resDisablePostForm = await disableModule.POST({ + request: new Request("https://codingcat.dev/api/draft-mode/disable", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ returnTo: "/\\evil.com" }).toString(), + }), + url: new URL("https://codingcat.dev/api/draft-mode/disable"), + cookies: mockCookies, + redirect: mockRedirect, + }); + assert.equal(resDisablePostForm.headers.get("Location"), "/"); + + // 6. disable POST json with returnTo=/\\evil.com + const resDisablePostJson = await disableModule.POST({ + request: new Request("https://codingcat.dev/api/draft-mode/disable", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ returnTo: "/\\evil.com" }), + }), + url: new URL("https://codingcat.dev/api/draft-mode/disable"), + cookies: mockCookies, + redirect: mockRedirect, + }); + assert.equal(resDisablePostJson.headers.get("Location"), "/"); + }); + }); +}); diff --git a/apps/site/tests/tier2-boundaries.test.mjs b/apps/site/tests/tier2-boundaries.test.mjs new file mode 100644 index 00000000..040308b2 --- /dev/null +++ b/apps/site/tests/tier2-boundaries.test.mjs @@ -0,0 +1,608 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { describe, it } from "node:test"; +import { + createMockSanityContext, + extractXmlTags, + loadBaseline, + loadModule, + parseXmlAttributes, + SRC_DIR, + stegaClean, +} from "./helpers.mjs"; + +describe("Tier 2 - Boundary & Corner Cases", () => { + // ── 1. Pagination Bounds Check ──────────────────────────────────────── + describe("Pagination Bounds Check", () => { + const pagination = loadModule("src/lib/pagination.ts"); + + it("1.1 Page 0 is rejected with 404 response", () => { + const res = pagination.resolvePage({ + raw: "0", + count: 50, + base: "blog", + redirect: (p, s) => + new Response(null, { status: s, headers: { Location: p } }), + }); + assert.equal(res.ok, false); + assert.equal(res.response.status, 404); + }); + + it("1.2 Negative page numbers are rejected with 404 response", () => { + for (const raw of ["-1", "-10", "-999"]) { + const res = pagination.resolvePage({ + raw, + count: 50, + base: "blog", + redirect: () => new Response(), + }); + assert.equal(res.ok, false); + assert.equal(res.response.status, 404); + } + }); + + it("1.3 Non-numeric and malformed string pages return 404", () => { + for (const raw of [ + "abc", + "page1", + "1a", + "NaN", + "undefined", + "null", + "", + ]) { + const res = pagination.resolvePage({ + raw, + count: 50, + base: "blog", + redirect: () => new Response(), + }); + assert.equal(res.ok, false); + assert.equal(res.response.status, 404); + } + }); + + it("1.4 Decimal, whitespace, and exponential notations return 404", () => { + for (const raw of ["1.5", "2.0", " 1 ", "1 ", " 1", "1e2", "01"]) { + const res = pagination.resolvePage({ + raw, + count: 50, + base: "blog", + redirect: () => new Response(), + }); + assert.equal(res.ok, false, `Raw input "${raw}" should be rejected`); + assert.equal(res.response.status, 404); + } + }); + + it("1.5 Page beyond total count returns 302 redirect to /:base/page/:totalPages", () => { + const res = pagination.resolvePage({ + raw: "999", + count: 25, // totalPages = 3 + base: "blog", + redirect: (dest, status) => + new Response(null, { status, headers: { Location: dest } }), + }); + assert.equal(res.ok, false); + assert.equal(res.response.status, 302); + assert.equal(res.response.headers.get("Location"), "/blog/page/3"); + }); + + it("1.6 Zero count items clamps totalPages to 1 with page 1 bounds valid", () => { + const res = pagination.resolvePage({ + raw: "1", + count: 0, + base: "podcasts", + redirect: () => new Response(), + }); + assert.equal(res.ok, true); + assert.deepEqual(res.bounds, { + page: 1, + totalPages: 1, + offset: 0, + limit: 10, + }); + }); + + it("1.7 Valid middle and last page calculations are precise", () => { + const count = 55; // 6 total pages + const resPage3 = pagination.resolvePage({ + raw: "3", + count, + base: "authors", + redirect: () => new Response(), + }); + assert.equal(resPage3.ok, true); + assert.deepEqual(resPage3.bounds, { + page: 3, + totalPages: 6, + offset: 20, + limit: 30, + }); + + const resPage6 = pagination.resolvePage({ + raw: "6", + count, + base: "authors", + redirect: () => new Response(), + }); + assert.equal(resPage6.ok, true); + assert.deepEqual(resPage6.bounds, { + page: 6, + totalPages: 6, + offset: 50, + limit: 60, + }); + }); + }); + + // ── 2. Non-existent Slugs ───────────────────────────────────────────── + describe("Non-existent Slugs", () => { + it("2.1 Non-existent post returns 404 in [slug].astro handler contract", () => { + const content = fs.readFileSync( + path.resolve(SRC_DIR, "pages/post/[slug].astro"), + "utf-8", + ); + assert.ok(content.includes("postQuery"), "Must query postQuery"); + assert.ok( + content.includes("!post?._id"), + "Checks post existence via _id", + ); + assert.ok( + content.includes("new Response(null, { status: 404 })"), + "Returns 404 on missing post", + ); + }); + + it("2.2 Non-existent podcast returns 404 in [slug].astro handler contract", () => { + const content = fs.readFileSync( + path.resolve(SRC_DIR, "pages/podcast/[slug].astro"), + "utf-8", + ); + assert.ok(content.includes("podcastQuery"), "Must query podcastQuery"); + assert.ok( + content.includes("!podcast?._id"), + "Checks podcast existence via _id", + ); + assert.ok( + content.includes("new Response(null, { status: 404 })"), + "Returns 404 on missing podcast", + ); + }); + + it("2.3 Non-existent author returns 404 in [slug].astro handler contract", () => { + const content = fs.readFileSync( + path.resolve(SRC_DIR, "pages/author/[slug].astro"), + "utf-8", + ); + assert.ok(content.includes("authorQuery"), "Must query authorQuery"); + assert.ok( + content.includes("!person?._id"), + "Checks author existence via _id", + ); + assert.ok( + content.includes("new Response(null, { status: 404 })"), + "Returns 404 on missing author", + ); + }); + + it("2.4 Non-existent guest returns 404 in [slug].astro handler contract", () => { + const content = fs.readFileSync( + path.resolve(SRC_DIR, "pages/guest/[slug].astro"), + "utf-8", + ); + assert.ok(content.includes("guestQuery"), "Must query guestQuery"); + assert.ok( + content.includes("!person?._id"), + "Checks guest existence via _id", + ); + assert.ok( + content.includes("new Response(null, { status: 404 })"), + "Returns 404 on missing guest", + ); + }); + + it("2.5 Non-existent sponsor returns 404 in [slug].astro handler contract", () => { + const content = fs.readFileSync( + path.resolve(SRC_DIR, "pages/sponsor/[slug].astro"), + "utf-8", + ); + assert.ok(content.includes("sponsorQuery"), "Must query sponsorQuery"); + assert.ok( + content.includes("!sponsor?._id"), + "Checks sponsor existence via _id", + ); + assert.ok( + content.includes("new Response(null, { status: 404 })"), + "Returns 404 on missing sponsor", + ); + }); + + it("2.6 Non-existent catch-all static page slug returns 404", () => { + const content = fs.readFileSync( + path.resolve(SRC_DIR, "pages/[slug].astro"), + "utf-8", + ); + assert.ok(content.includes("pageQuery"), "Must query pageQuery"); + assert.ok( + content.includes("!page?._id"), + "Checks page existence via _id", + ); + assert.ok( + content.includes("new Response(null, { status: 404 })"), + "Returns 404 on missing page document", + ); + }); + }); + + // ── 3. Podcast RSS GUID Verification ────────────────────────────────── + describe("Podcast RSS GUID Verification", () => { + const feeds = loadModule("src/lib/feeds.ts"); + + it("3.1 Podcast RSS strictly preserves Sanity _id verbatim inside ", () => { + const mockSanity = createMockSanityContext(); + const testId = "9876abcd-ef01-2345-6789-abcdef012345"; + const xml = feeds.buildPodcastRss({ + origin: "https://codingcat.dev", + type: "podcast", + items: [ + { + _id: testId, + _type: "podcast", + title: "Test Episode", + slug: "test-episode", + }, + ], + now: new Date(), + sanity: mockSanity, + }); + + const guidTags = extractXmlTags(xml, "guid"); + assert.equal(guidTags.length, 1); + assert.equal(guidTags[0].content, testId); + const attrs = parseXmlAttributes(guidTags[0].attributes); + assert.equal(attrs.isPermaLink, "false"); + }); + + it("3.2 Safeguard: GUID is never transformed into URL or slug", () => { + const mockSanity = createMockSanityContext(); + const xml = feeds.buildPodcastRss({ + origin: "https://codingcat.dev", + type: "podcast", + items: [ + { + _id: "sanity-doc-id-xyz", + _type: "podcast", + title: "Episode with Custom Slug", + slug: "custom-slug-name", + }, + ], + now: new Date(), + sanity: mockSanity, + }); + + const guidTags = extractXmlTags(xml, "guid"); + assert.ok( + !guidTags[0].content.includes("https://"), + "GUID must not be a URL", + ); + assert.ok( + !guidTags[0].content.includes("custom-slug-name"), + "GUID must not be a slug", + ); + assert.equal(guidTags[0].content, "sanity-doc-id-xyz"); + }); + + it("3.3 All 50 baseline podcast GUIDs use isPermaLink=false and match baseline snapshot", () => { + const baselineXml = loadBaseline("podcasts_rss.xml"); + const guidTags = extractXmlTags(baselineXml, "guid"); + assert.equal(guidTags.length, 50, "Baseline has 50 podcast GUIDs"); + + for (const g of guidTags) { + const attrs = parseXmlAttributes(g.attributes); + assert.equal( + attrs.isPermaLink, + "false", + `GUID ${g.content} must have isPermaLink=false`, + ); + assert.ok( + g.content.length > 5, + "GUID must be non-empty valid Sanity id", + ); + } + }); + + it("3.4 Special characters in _id are escaped in XML guid tag", () => { + const mockSanity = createMockSanityContext(); + const xml = feeds.buildPodcastRss({ + origin: "https://codingcat.dev", + type: "podcast", + items: [ + { + _id: "drafts.foo&bar", + _type: "podcast", + title: "Draft Episode", + slug: "draft-ep", + }, + ], + now: new Date(), + sanity: mockSanity, + }); + + const guidTags = extractXmlTags(xml, "guid"); + assert.equal(guidTags[0].content, "drafts.foo&bar<test>"); + }); + + it("3.5 All GUIDs in feed items are unique", () => { + const baselineXml = loadBaseline("podcasts_rss.xml"); + const guidTags = extractXmlTags(baselineXml, "guid"); + const guidSet = new Set(guidTags.map((g) => g.content)); + assert.equal(guidSet.size, 50, "All 50 GUIDs must be unique"); + }); + }); + + // ── 4. Audio Enclosures ─────────────────────────────────────────────── + describe("Audio Enclosures", () => { + const feeds = loadModule("src/lib/feeds.ts"); + + it("4.1 Audio enclosure outputs url, length, and audio/mpeg type", () => { + const mockSanity = createMockSanityContext(); + const enclosureData = { + url: "https://d3ctxlq1ktw2nl.cloudfront.net/staging/2026/test.mp3", + length: 45678901, + type: "audio/mpeg", + }; + + const xml = feeds.buildPodcastRss({ + origin: "https://codingcat.dev", + type: "podcast", + items: [ + { + _id: "ep-audio", + _type: "podcast", + title: "Audio Test", + slug: "audio-test", + spotify: { enclosures: [enclosureData] }, + }, + ], + now: new Date(), + sanity: mockSanity, + }); + + assert.ok(xml.includes(`url="${enclosureData.url}"`)); + assert.ok(xml.includes(`length="${enclosureData.length}"`)); + assert.ok(xml.includes(`type="audio/mpeg"`)); + }); + + it("4.2 Missing enclosure length defaults to 0 and type defaults to audio/mpeg", () => { + const mockSanity = createMockSanityContext(); + const xml = feeds.buildPodcastRss({ + origin: "https://codingcat.dev", + type: "podcast", + items: [ + { + _id: "ep-audio-defaults", + _type: "podcast", + title: "Defaults Test", + slug: "defaults-test", + spotify: { + enclosures: [{ url: "https://example.com/podcast.mp3" }], + }, + }, + ], + now: new Date(), + sanity: mockSanity, + }); + + assert.ok( + xml.includes( + ``, + ), + ); + }); + + it("4.3 Enclosure tag is omitted when spotify enclosures array is missing or empty", () => { + const mockSanity = createMockSanityContext(); + const xmlNoSpotify = feeds.buildPodcastRss({ + origin: "https://codingcat.dev", + type: "podcast", + items: [ + { + _id: "ep-no-spotify", + _type: "podcast", + title: "No Spotify Test", + slug: "no-spotify-test", + spotify: null, + }, + ], + now: new Date(), + sanity: mockSanity, + }); + assert.ok( + !xmlNoSpotify.includes(" { + const mockSanity = createMockSanityContext(); + const rawUrl = + "https://example.com/audio.mp3?token=abc&session=123&track=5"; + const xml = feeds.buildPodcastRss({ + origin: "https://codingcat.dev", + type: "podcast", + items: [ + { + _id: "ep-query-url", + _type: "podcast", + title: "Query URL Test", + slug: "query-url-test", + spotify: { enclosures: [{ url: rawUrl, length: 1000 }] }, + }, + ], + now: new Date(), + sanity: mockSanity, + }); + + assert.ok( + xml.includes("token=abc&session=123&track=5"), + "Query ampersands must be escaped", + ); + assert.ok( + !xml.includes("&session="), + "Unescaped ampersand must not exist in enclosure", + ); + }); + + it("4.5 imageMimeType helper maps image extensions accurately", () => { + const xmlLib = loadModule("src/lib/xml.ts"); + assert.equal( + xmlLib.imageMimeType("https://cdn.example.com/img.jpg"), + "image/jpg", + ); + assert.equal( + xmlLib.imageMimeType("https://cdn.example.com/img.jpeg"), + "image/jpeg", + ); + assert.equal( + xmlLib.imageMimeType("https://cdn.example.com/img.png"), + "image/png", + ); + assert.equal( + xmlLib.imageMimeType("https://cdn.example.com/img.webp"), + "image/webp", + ); + assert.equal( + xmlLib.imageMimeType("https://cdn.example.com/img.svg"), + "image/svg", + ); + assert.equal( + xmlLib.imageMimeType("https://cdn.example.com/img.custom"), + "image/custom", + ); + }); + }); + + // ── 5. XML Escaping & CDATA Termination ─────────────────────────────── + describe("XML Escaping & CDATA Termination", () => { + const xmlLib = loadModule("src/lib/xml.ts"); + + it("5.1 escapeXml escapes all 5 XML predefined entities", () => { + assert.equal(xmlLib.escapeXml("&"), "&"); + assert.equal(xmlLib.escapeXml("<"), "<"); + assert.equal(xmlLib.escapeXml(">"), ">"); + assert.equal(xmlLib.escapeXml(`"`), """); + assert.equal(xmlLib.escapeXml("'"), "'"); + }); + + it("5.2 escapeXml handles complex mixed strings without double escaping", () => { + const input = `
Tom & Jerry > Sylvester
`; + const expected = + "<div class="sample" id='test'>Tom & Jerry > Sylvester</div>"; + assert.equal(xmlLib.escapeXml(input), expected); + }); + + it("5.3 cdata wraps text in CDATA enclosure", () => { + assert.equal(xmlLib.cdata("Hello World"), ""); + }); + + it("5.4 cdata neutralizes premature ]]> termination attack by splitting", () => { + const attack = "Safe text ]]> Malicious XML injection"; + const result = xmlLib.cdata(attack); + assert.equal( + result, + " Malicious XML injection]]>", + ); + // Verify no raw ]]> exists outside proper enclosure + assert.ok(!result.includes("Safe text ]]>")); + }); + + it("5.5 cdata handles multiple sequential ]]> instances", () => { + const multi = "Alpha ]]> Beta ]]> Gamma ]]> Delta"; + const result = xmlLib.cdata(multi); + assert.equal( + result, + " Beta ]]]]> Gamma ]]]]> Delta]]>", + ); + }); + + it("5.6 escapeXml and cdata handle empty strings gracefully", () => { + assert.equal(xmlLib.escapeXml(""), ""); + assert.equal(xmlLib.cdata(""), ""); + }); + + it("5.7 Unicode and emojis survive escapeXml and cdata intact", () => { + const unicode = "🐱 CodingCat — 100% 🚀 & 💖"; + assert.equal( + xmlLib.escapeXml(unicode), + "🐱 CodingCat — 100% 🚀 & 💖", + ); + assert.equal( + xmlLib.cdata(unicode), + "", + ); + }); + }); + + // ── 6. Stega Cleaning ───────────────────────────────────────────────── + describe("Stega Cleaning", () => { + const STEGA_CHARS = "\u200B\u200C\u200D\uFEFF"; + + it("6.1 stegaClean strips zero-width spaces and zero-width joiners from attributes", () => { + const dirtyHref = `https://codingcat.dev/post/foo${STEGA_CHARS}`; + const cleanHref = stegaClean(dirtyHref); + assert.equal(cleanHref, "https://codingcat.dev/post/foo"); + }); + + it("6.2 stegaClean ensures canonical URLs are pristine without stega markers", () => { + const dirtyCanonical = `/blog/page/1${STEGA_CHARS}`; + const cleanCanonical = stegaClean(dirtyCanonical); + assert.equal(cleanCanonical, "/blog/page/1"); + assert.ok(!cleanCanonical.includes("\u200B")); + }); + + it("6.3 stegaClean cleans OpenGraph titles and descriptions", () => { + const dirtyTitle = `Building Astro${STEGA_CHARS} Sites`; + const cleanTitle = stegaClean(dirtyTitle); + assert.equal(cleanTitle, "Building Astro Sites"); + }); + + it("6.4 stegaClean cleans object values recursively or handles primitive types", () => { + assert.equal(stegaClean(null), null); + assert.equal(stegaClean(undefined), undefined); + assert.equal(stegaClean("Clean string"), "Clean string"); + assert.equal(stegaClean(""), ""); + }); + + it("6.5 stegaClean prevents zero-width corruption in feed GUIDs and audio URLs", () => { + const dirtyGuid = `guid-1234${STEGA_CHARS}-5678`; + const cleanGuid = stegaClean(dirtyGuid); + assert.equal(cleanGuid, "guid-1234-5678"); + + const dirtyAudio = `https://anchor.fm/test.mp3${STEGA_CHARS}`; + const cleanAudio = stegaClean(dirtyAudio); + assert.equal(cleanAudio, "https://anchor.fm/test.mp3"); + }); + }); +}); diff --git a/apps/site/tests/tier3-combinations.test.mjs b/apps/site/tests/tier3-combinations.test.mjs new file mode 100644 index 00000000..c9ee71a2 --- /dev/null +++ b/apps/site/tests/tier3-combinations.test.mjs @@ -0,0 +1,371 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { describe, it } from "node:test"; +import { loadModule, SITE_ROOT, SRC_DIR } from "./helpers.mjs"; + +describe("Tier 3 - Cross-Feature Combinations", () => { + // ── 1. Redirect Rules ───────────────────────────────────────────────── + describe("Redirect Rules", () => { + const astroConfigPath = path.resolve(SITE_ROOT, "astro.config.mjs"); + const astroConfigContent = fs.readFileSync(astroConfigPath, "utf-8"); + + it("1.1 /blog/page redirects to /blog/page/1 with 302 temporary status", () => { + const filePath = path.resolve(SRC_DIR, "pages/blog/page/index.astro"); + assert.ok(fs.existsSync(filePath), "blog/page/index.astro must exist"); + const content = fs.readFileSync(filePath, "utf-8"); + assert.ok( + content.includes('return Astro.redirect("/blog/page/1", 302);'), + "/blog/page must 302 redirect to /blog/page/1 via SSR redirect", + ); + }); + + it("1.2 /podcasts/page redirects to /podcasts/page/1 with 302 status", () => { + const filePath = path.resolve(SRC_DIR, "pages/podcasts/page/index.astro"); + assert.ok( + fs.existsSync(filePath), + "podcasts/page/index.astro must exist", + ); + const content = fs.readFileSync(filePath, "utf-8"); + assert.ok( + content.includes('return Astro.redirect("/podcasts/page/1", 302);'), + "/podcasts/page must 302 redirect to /podcasts/page/1 via SSR redirect", + ); + }); + + it("1.3 /authors and /authors/page redirect to /authors/page/1 with 302 status", () => { + const authorsIndex = path.resolve(SRC_DIR, "pages/authors/index.astro"); + assert.ok(fs.existsSync(authorsIndex), "authors/index.astro must exist"); + const authorsContent = fs.readFileSync(authorsIndex, "utf-8"); + assert.ok( + authorsContent.includes( + 'return Astro.redirect("/authors/page/1", 302);', + ), + "/authors must 302 redirect to /authors/page/1 via SSR redirect", + ); + + const authorsPageIndex = path.resolve( + SRC_DIR, + "pages/authors/page/index.astro", + ); + assert.ok( + fs.existsSync(authorsPageIndex), + "authors/page/index.astro must exist", + ); + const authorsPageContent = fs.readFileSync(authorsPageIndex, "utf-8"); + assert.ok( + authorsPageContent.includes( + 'return Astro.redirect("/authors/page/1", 302);', + ), + "/authors/page must 302 redirect to /authors/page/1 via SSR redirect", + ); + }); + + it("1.4 /guests and /guests/page redirect to /guests/page/1 with 302 status", () => { + const guestsIndex = path.resolve(SRC_DIR, "pages/guests/index.astro"); + assert.ok(fs.existsSync(guestsIndex), "guests/index.astro must exist"); + const guestsContent = fs.readFileSync(guestsIndex, "utf-8"); + assert.ok( + guestsContent.includes('return Astro.redirect("/guests/page/1", 302);'), + "/guests must 302 redirect to /guests/page/1 via SSR redirect", + ); + + const guestsPageIndex = path.resolve( + SRC_DIR, + "pages/guests/page/index.astro", + ); + assert.ok( + fs.existsSync(guestsPageIndex), + "guests/page/index.astro must exist", + ); + const guestsPageContent = fs.readFileSync(guestsPageIndex, "utf-8"); + assert.ok( + guestsPageContent.includes( + 'return Astro.redirect("/guests/page/1", 302);', + ), + "/guests/page must 302 redirect to /guests/page/1 via SSR redirect", + ); + }); + + it("1.5 /sponsors and /sponsors/page redirect to /sponsors/page/1 with 302 status", () => { + const sponsorsIndex = path.resolve(SRC_DIR, "pages/sponsors/index.astro"); + assert.ok( + fs.existsSync(sponsorsIndex), + "sponsors/index.astro must exist", + ); + const sponsorsContent = fs.readFileSync(sponsorsIndex, "utf-8"); + assert.ok( + sponsorsContent.includes( + 'return Astro.redirect("/sponsors/page/1", 302);', + ), + "/sponsors must 302 redirect to /sponsors/page/1 via SSR redirect", + ); + + const sponsorsPageIndex = path.resolve( + SRC_DIR, + "pages/sponsors/page/index.astro", + ); + assert.ok( + fs.existsSync(sponsorsPageIndex), + "sponsors/page/index.astro must exist", + ); + const sponsorsPageContent = fs.readFileSync(sponsorsPageIndex, "utf-8"); + assert.ok( + sponsorsPageContent.includes( + 'return Astro.redirect("/sponsors/page/1", 302);', + ), + "/sponsors/page must 302 redirect to /sponsors/page/1 via SSR redirect", + ); + }); + + it("1.6 Legacy feeds 301 redirects are configured in astro.config.mjs", () => { + assert.ok( + astroConfigContent.includes( + '"/rss.xml": { status: 301, destination: "/blog/rss.xml" }', + ), + "/rss.xml must 301 redirect to /blog/rss.xml in astro.config.mjs", + ); + assert.ok( + astroConfigContent.includes( + '"/podcast/rss.xml": { status: 301, destination: "/podcasts/rss.xml" }', + ), + "/podcast/rss.xml must 301 redirect to /podcasts/rss.xml in astro.config.mjs", + ); + assert.ok( + astroConfigContent.includes( + '"/feed.xml": { status: 301, destination: "/blog/rss.xml" }', + ), + "/feed.xml must 301 redirect to /blog/rss.xml in astro.config.mjs", + ); + }); + + it("1.7 Listing stubs use 302 status to remain uncacheable as content grows", () => { + const blogPage = fs.readFileSync( + path.resolve(SRC_DIR, "pages/blog/page/index.astro"), + "utf-8", + ); + assert.ok( + blogPage.includes("302"), + "Blog listing stub must redirect with 302", + ); + assert.ok( + !astroConfigContent.includes('"/blog/page"'), + "Config-level /blog/page redirect removed to prevent 301 fallback in Astro 7 SSR", + ); + }); + + it("1.8 Cloudflare KV session injection disabled (session: false)", () => { + assert.ok( + astroConfigContent.includes("session: false"), + "astro.config.mjs must set session: false to prevent dummy KV SESSION injection", + ); + }); + }); + + // ── 2. Canonical URL Resolution via Astro.locals.siteUrl ────────────── + describe("Canonical URL Resolution via Astro.locals.siteUrl", () => { + const siteLib = loadModule("src/lib/site.ts"); + + it("2.1 Production environment resolves origin to https://codingcat.dev", () => { + const url = siteLib.resolveSiteUrl("https://codingcat.dev", undefined); + assert.equal(url.origin, "https://codingcat.dev"); + }); + + it("2.2 Dev environment resolves origin to Cloudflare worker URL", () => { + const devWorker = "https://codingcatdev.workers.dev"; + const url = siteLib.resolveSiteUrl(devWorker, undefined); + assert.equal(url.origin, devWorker); + }); + + it("2.3 Trailing slash in runtime SITE_URL is normalized without affecting origin", () => { + const url = siteLib.resolveSiteUrl("https://codingcat.dev/", undefined); + assert.equal(url.origin, "https://codingcat.dev"); + }); + + it("2.4 Fallback to buildTimeSite when runtime SITE_URL is absent", () => { + const buildTimeSite = new URL("https://preview.codingcat.dev"); + const url = siteLib.resolveSiteUrl(undefined, buildTimeSite); + assert.equal(url.origin, "https://preview.codingcat.dev"); + }); + + it("2.5 absoluteUrl joins relative paths cleanly without duplicate slashes", () => { + const base = new URL("https://codingcat.dev"); + assert.equal( + siteLib.absoluteUrl("/blog/page/1", base), + "https://codingcat.dev/blog/page/1", + ); + assert.equal( + siteLib.absoluteUrl("blog/page/1", base), + "https://codingcat.dev/blog/page/1", + ); + assert.equal( + siteLib.absoluteUrl("/post/my-post", base), + "https://codingcat.dev/post/my-post", + ); + assert.equal( + siteLib.absoluteUrl("https://external.com/test", base), + "https://external.com/test", + ); + }); + + it("2.6 Malformed runtime SITE_URL falls through safely without throwing", () => { + const fallbackSite = new URL("https://codingcat.dev"); + const url = siteLib.resolveSiteUrl("not-a-valid-url", fallbackSite); + assert.equal(url.origin, fallbackSite.origin); + }); + }); + + // ── 3. Dynamic OG Fallback Scaling & Long Titles ─────────────────────── + describe("Dynamic OG Fallback Scaling & Long Titles", () => { + const ogUtils = loadModule("src/lib/og-utils.ts"); + + it("3.1 Short title (<= 40 chars) receives 56px font size", () => { + const title = "Astro vs Next.js"; + assert.equal(title.length < 40, true); + assert.equal(ogUtils.titleFontSize(title), 56); + }); + + it("3.2 Medium title (41-60 chars) receives 48px font size", () => { + const title = "Complete Guide to Cloudflare Workers with Astro SSR"; + assert.ok(title.length >= 41 && title.length <= 60); + assert.equal(ogUtils.titleFontSize(title), 48); + }); + + it("3.3 Long title (> 60 chars) receives 42px font size", () => { + const title = + "Why We Are Absolutely Obsessed With Sticker Mule's Holographic Stickers"; + assert.ok(title.length > 60); + assert.equal(ogUtils.titleFontSize(title), 42); + }); + + it("3.4 Extreme long title (> 120 chars) remains stable at 42px", () => { + const title = "A".repeat(150); + assert.equal(ogUtils.titleFontSize(title), 42); + }); + + it("3.5 fallbackOgImage safely generates OG image URL with query params and stega cleaning", () => { + const ogLib = loadModule("src/lib/og.ts"); + const res = ogLib.fallbackOgImage("https://codingcat.dev", { + title: "Building & Designing: The 'Ultimate' Guide — 100% Free", + type: "Blog", + author: "Alex Patterson", + }); + assert.ok(res.url.startsWith("https://codingcat.dev/api/og/blog.png?")); + assert.ok(res.url.includes("title=")); + assert.equal(res.width, 1200); + assert.equal(res.height, 630); + assert.equal( + res.alt, + "Building & Designing: The 'Ultimate' Guide — 100% Free", + ); + }); + + it("3.6 authorInitials helper produces 2-letter uppercase monogram", () => { + const author = "Alex Patterson"; + const initials = author + .split(" ") + .map((n) => n[0]) + .join("") + .slice(0, 2) + .toUpperCase(); + assert.equal(initials, "AP"); + + const singleName = "Cher"; + const singleInitials = singleName + .split(" ") + .map((n) => n[0]) + .join("") + .slice(0, 2) + .toUpperCase(); + assert.equal(singleInitials, "C"); + }); + }); + + // ── 4. Draft Mode Cookie Handling & Edge Cache Protection ───────────── + describe("Draft Mode Cookie Handling & Edge Cache Protection", () => { + it("4.1 Middleware file sets Cache-Control: no-store, private when preview is enabled", () => { + const middlewareSrc = fs.readFileSync( + path.resolve(SRC_DIR, "middleware.ts"), + "utf-8", + ); + assert.ok( + middlewareSrc.includes("context.locals.sanity.preview.enabled"), + "Must check preview.enabled", + ); + assert.ok( + middlewareSrc.includes( + 'response.headers.set("Cache-Control", "no-store, private")', + ), + "Must set Cache-Control: no-store, private", + ); + }); + + it("4.2 Middleware sets X-Robots-Tag: noindex, nofollow to prevent indexing drafts", () => { + const middlewareSrc = fs.readFileSync( + path.resolve(SRC_DIR, "middleware.ts"), + "utf-8", + ); + assert.ok( + middlewareSrc.includes( + 'response.headers.set("X-Robots-Tag", "noindex, nofollow")', + ), + "Must set X-Robots-Tag: noindex, nofollow for preview", + ); + }); + + it("4.3 Middleware reads PREVIEW_COOKIE (__sanity_preview) from cookies", () => { + const middlewareSrc = fs.readFileSync( + path.resolve(SRC_DIR, "middleware.ts"), + "utf-8", + ); + assert.ok( + middlewareSrc.includes("PREVIEW_COOKIE"), + "Must check PREVIEW_COOKIE", + ); + assert.ok( + middlewareSrc.includes("context.cookies.has(PREVIEW_COOKIE)"), + "Reads cookie from context.cookies", + ); + }); + + it("4.4 Context creation wires preview into locals.sanity", () => { + const contextSrc = fs.readFileSync( + path.resolve(SRC_DIR, "lib/sanity/context.ts"), + "utf-8", + ); + assert.ok(contextSrc.includes("resolvePreview"), "Calls resolvePreview"); + assert.ok( + contextSrc.includes("preview,"), + "Exposes preview on sanity context", + ); + }); + + it("4.5 When preview is disabled, public response cache headers are undisturbed", () => { + const middlewareSrc = fs.readFileSync( + path.resolve(SRC_DIR, "middleware.ts"), + "utf-8", + ); + const ifIdx = middlewareSrc.indexOf( + "if (context.locals.sanity.preview.enabled)", + ); + const headerIdx = middlewareSrc.indexOf( + 'response.headers.set("Cache-Control", "no-store, private")', + ); + assert.ok( + ifIdx !== -1 && headerIdx > ifIdx, + "Cache-Control only altered when preview is enabled", + ); + }); + + it("4.6 Perspective parameter is correctly read from URL search params", () => { + const contextSrc = fs.readFileSync( + path.resolve(SRC_DIR, "lib/sanity/context.ts"), + "utf-8", + ); + assert.ok( + contextSrc.includes("url.searchParams.get(PERSPECTIVE_PARAM)"), + "Reads PERSPECTIVE_PARAM from searchParams", + ); + }); + }); +}); diff --git a/apps/site/tests/tier4-baseline-contract.test.mjs b/apps/site/tests/tier4-baseline-contract.test.mjs new file mode 100644 index 00000000..69e0a7fa --- /dev/null +++ b/apps/site/tests/tier4-baseline-contract.test.mjs @@ -0,0 +1,367 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { + extractXmlTags, + loadBaseline, + loadBaselineJson, + loadModule, + parseXmlAttributes, +} from "./helpers.mjs"; + +describe("Tier 4 - Real-World Application & Baseline Contract", () => { + // ── 1. Baseline Sitemap Contract ────────────────────────────────────── + describe("Baseline Sitemap Contract (apps/site/baseline/sitemap.xml)", () => { + const sitemapXml = loadBaseline("sitemap.xml"); + const urls = extractXmlTags(sitemapXml, "url"); + + it("1.1 Baseline sitemap contains exactly 472 indexed URLs", () => { + assert.equal(urls.length, 472, "Total indexed URLs must be exactly 472"); + }); + + it("1.2 URL distribution and classification matches baseline contract", () => { + const counts = { + root: 0, + search: 0, + post: 0, + podcast: 0, + page: 0, + author: 0, + guest: 0, + sponsor: 0, + }; + + for (const u of urls) { + const locMatch = u.content.match(/(.*?)<\/loc>/); + assert.ok(locMatch, "Each url block must contain loc"); + const loc = locMatch[1]; + const path = loc.replace("https://codingcat.dev", ""); + + if (path === "" || path === "/") { + counts.root++; + } else if (path === "/search") { + counts.search++; + } else if (path.startsWith("/post/")) { + counts.post++; + } else if (path === "/podcast" || path.startsWith("/podcast/")) { + counts.podcast++; + } else if (path.startsWith("/author/")) { + counts.author++; + } else if (path.startsWith("/guest/")) { + counts.guest++; + } else if (path.startsWith("/sponsor/")) { + counts.sponsor++; + } else { + counts.page++; + } + } + + assert.equal(counts.root, 1, "1 root URL"); + assert.equal(counts.search, 1, "1 search URL"); + assert.equal(counts.post, 69, "69 post URLs"); + assert.equal(counts.podcast, 194, "194 podcast URLs"); + assert.equal(counts.author, 5, "5 author URLs"); + assert.equal(counts.guest, 186, "186 guest URLs"); + assert.equal(counts.sponsor, 9, "9 sponsor URLs"); + assert.equal(counts.page, 7, "7 static page URLs"); + }); + + it("1.3 Priority directives strictly match baseline specification", () => { + for (const u of urls) { + const loc = u.content.match(/(.*?)<\/loc>/)[1]; + const prio = u.content.match(/(.*?)<\/priority>/)[1]; + const path = loc.replace("https://codingcat.dev", ""); + + if (path === "" || path === "/") { + assert.equal(prio, "1", "Root priority must be 1"); + } else if (path === "/search") { + assert.equal(prio, "0.1", "Search priority must be 0.1"); + } else { + assert.equal(prio, "0.5", `Content ${path} priority must be 0.5`); + } + } + }); + + it("1.4 Changefreq directives strictly match baseline specification", () => { + for (const u of urls) { + const loc = u.content.match(/(.*?)<\/loc>/)[1]; + const freq = u.content.match(/(.*?)<\/changefreq>/)[1]; + const path = loc.replace("https://codingcat.dev", ""); + + if (path === "/search") { + assert.equal(freq, "daily", "Search changefreq must be daily"); + } else { + assert.equal( + freq, + "monthly", + `URL ${path} changefreq must be monthly`, + ); + } + } + }); + + it("1.5 Every lastmod timestamp is a valid ISO 8601 UTC date", () => { + const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; + for (const u of urls) { + const lastmod = u.content.match(/(.*?)<\/lastmod>/)[1]; + assert.ok( + isoRegex.test(lastmod), + `lastmod ${lastmod} must be valid ISO 8601 UTC`, + ); + } + }); + + it("1.6 All loc entries conform to origin https://codingcat.dev with no trailing slash or duplicate slashes", () => { + for (const u of urls) { + const loc = u.content.match(/(.*?)<\/loc>/)[1]; + assert.ok( + loc.startsWith("https://codingcat.dev"), + `loc ${loc} must start with canonical origin`, + ); + assert.ok( + !loc.includes("//", 8), + `loc ${loc} must not have double slashes`, + ); + if (loc !== "https://codingcat.dev") { + assert.ok( + !loc.endsWith("/"), + `loc ${loc} must not have trailing slash`, + ); + } + } + }); + }); + + // ── 2. Baseline Podcast Feed Contract ───────────────────────────────── + describe("Baseline Podcast Feed Contract (apps/site/baseline/podcasts_rss.xml)", () => { + const podcastXml = loadBaseline("podcasts_rss.xml"); + const items = extractXmlTags(podcastXml, "item"); + + it("2.1 Baseline podcast feed contains exactly 50 recent episodes", () => { + assert.equal(items.length, 50, "Must contain exactly 50 podcast items"); + }); + + it("2.2 Channel declaration conforms to baseline namespaces and iTunes metadata", () => { + assert.ok( + podcastXml.includes('xmlns:atom="http://www.w3.org/2005/Atom"'), + ); + assert.ok( + podcastXml.includes( + 'xmlns:itunes="http://www.itunes.apple.com/dtds/podcast-1.0.dtd"', + ), + ); + assert.ok( + podcastXml.includes( + 'xmlns:content="http://purl.org/rss/1.0/modules/content/"', + ), + ); + assert.ok( + podcastXml.includes( + 'xmlns:podcast="https://podcastindex.org/namespace/1.0"', + ), + ); + assert.ok(podcastXml.includes("CodingCat.dev Podcast")); + assert.ok( + podcastXml.includes("https://codingcat.dev/podcasts"), + ); + assert.ok( + podcastXml.includes( + '', + ), + ); + assert.ok( + podcastXml.includes("Alex Patterson"), + ); + assert.ok(podcastXml.includes('')); + }); + + it("2.3 Every episode GUID preserves Sanity _id verbatim with isPermaLink=false", () => { + for (const it of items) { + const guidTag = extractXmlTags(it.content, "guid")[0]; + assert.ok(guidTag, "Must have guid tag"); + const attrs = parseXmlAttributes(guidTag.attributes); + assert.equal(attrs.isPermaLink, "false", "Must have isPermaLink=false"); + // Must NOT be a URL + assert.ok( + !guidTag.content.startsWith("http"), + `GUID ${guidTag.content} must not be URL`, + ); + assert.ok(guidTag.content.length >= 10, "GUID must be valid Sanity id"); + } + }); + + it("2.4 Audio enclosures carry valid CloudFront/Anchor MP3 URLs and length attributes", () => { + let enclosureCount = 0; + for (const it of items) { + const enclosureMatch = it.content.match( + //, + ); + if (enclosureMatch) { + enclosureCount++; + const [, url, length, type] = enclosureMatch; + assert.ok( + url.startsWith("https://"), + `Enclosure URL ${url} must be HTTPS`, + ); + assert.ok( + Number(length) > 0, + `Enclosure length ${length} must be positive`, + ); + assert.equal(type, "audio/mpeg", "Enclosure type must be audio/mpeg"); + } + } + assert.ok( + enclosureCount >= 40, + "Majority of baseline episodes have audio enclosures", + ); + }); + + it("2.5 iTunes metadata tags are populated on episodes", () => { + for (const it of items) { + assert.ok( + it.content.includes(""), + "Episode must have itunes:title", + ); + assert.ok( + it.content.includes(""), + "Episode must have itunes:author", + ); + assert.ok( + it.content.includes(""), + "Episode must have itunes:episodeType", + ); + } + }); + + it("2.6 Episode pubDate values conform to RFC 822 format", () => { + const rfc822Regex = + /^[A-Za-z]{3},\s+\d{1,2}\s+[A-Za-z]{3}\s+\d{4}\s+\d{2}:\d{2}:\d{2}\s+GMT$/; + for (const it of items) { + const pubDate = it.content.match(/(.*?)<\/pubDate>/)[1]; + assert.ok( + rfc822Regex.test(pubDate), + `pubDate ${pubDate} must be RFC 822`, + ); + } + }); + }); + + // ── 3. Baseline Blog Feed Contract ──────────────────────────────────── + describe("Baseline Blog Feed Contract (apps/site/baseline/blog_rss.xml)", () => { + const blogXml = loadBaseline("blog_rss.xml"); + const items = extractXmlTags(blogXml, "item"); + + it("3.1 Baseline blog feed contains exactly 50 recent articles", () => { + assert.equal(items.length, 50, "Must contain exactly 50 blog posts"); + }); + + it("3.2 Blog channel metadata matches baseline specification", () => { + assert.ok(blogXml.includes("CodingCat.dev - post feed")); + assert.ok(blogXml.includes("https://codingcat.dev/blog")); + assert.ok( + blogXml.includes( + "CodingCat.dev - post feed", + ), + ); + assert.ok(blogXml.includes("en")); + assert.ok(blogXml.includes("https://codingcat.dev/icon.svg")); + }); + + it("3.3 Blog items contain title, link, guid, pubDate, content:encoded, and author", () => { + for (const it of items) { + assert.ok( + it.content.includes("<![CDATA["), + "Title must be CDATA", + ); + assert.ok( + it.content.includes("<link>https://codingcat.dev/post/"), + "Link must be post URL", + ); + assert.ok( + it.content.includes('<guid isPermaLink="false">'), + "Guid must have isPermaLink=false", + ); + assert.ok(it.content.includes("<pubDate>"), "Must have pubDate"); + assert.ok( + it.content.includes("<content:encoded><![CDATA["), + "Content must be CDATA", + ); + assert.ok(it.content.includes("<author>"), "Must declare author"); + } + }); + + it("3.4 Baseline JSON feeds match item count and schema versions", () => { + const blogJson = loadBaselineJson("blog_rss.json"); + const podcastJson = loadBaselineJson("podcasts_rss.json"); + + assert.equal(blogJson.version, "https://jsonfeed.org/version/1"); + assert.equal(blogJson.items.length, 50); + assert.equal(blogJson.feed_url, "https://codingcat.dev/blog/rss.json"); + + assert.equal(podcastJson.version, "https://jsonfeed.org/version/1"); + assert.equal(podcastJson.items.length, 50); + assert.equal( + podcastJson.feed_url, + "https://codingcat.dev/podcasts/rss.json", + ); + }); + + it("3.5 Parity: XML GUID matches JSON feed ID across all 50 blog items", () => { + const blogJson = loadBaselineJson("blog_rss.json"); + for (let i = 0; i < 50; i++) { + const xmlGuid = extractXmlTags(items[i].content, "guid")[0].content; + const jsonId = blogJson.items[i].id; + assert.equal(xmlGuid, jsonId, `Item ${i} GUID must match JSON feed ID`); + } + }); + }); + + // ── 4. Baseline Robots.txt Contract ─────────────────────────────────── + describe("Baseline Robots Contract (apps/site/baseline/robots.txt)", () => { + const baselineRobots = loadBaseline("robots.txt"); + const robotsModule = loadModule("src/pages/robots.txt.ts"); + + it("4.1 Baseline robots contains User-Agent: * block with /api/ and /dashboard/ disallowed", () => { + assert.ok(baselineRobots.includes("User-Agent: *")); + assert.ok(baselineRobots.includes("Allow: /")); + assert.ok(baselineRobots.includes("Disallow: /api/")); + assert.ok(baselineRobots.includes("Disallow: /dashboard/")); + }); + + it("4.2 Baseline robots declares Host and Sitemap pointing to codingcat.dev", () => { + assert.ok(baselineRobots.includes("Host: https://codingcat.dev")); + assert.ok( + baselineRobots.includes("Sitemap: https://codingcat.dev/sitemap.xml"), + ); + }); + + it("4.3 Generated robots response matches baseline verbatim in production", async () => { + const res = await robotsModule.GET({ + locals: { siteUrl: new URL("https://codingcat.dev") }, + }); + const generated = await res.text(); + assert.equal(generated.trim(), baselineRobots.trim()); + }); + + it("4.4 Non-production environment outputs Disallow: / to prevent indexation", async () => { + const res = await robotsModule.GET({ + locals: { siteUrl: new URL("https://codingcatdev.workers.dev") }, + }); + const generated = await res.text(); + assert.equal(generated.trim(), "User-Agent: *\nDisallow: /"); + }); + + it("4.5 Robots headers include text/plain; charset=utf-8 and max-age=0, s-maxage=3600", async () => { + const res = await robotsModule.GET({ + locals: { siteUrl: new URL("https://codingcat.dev") }, + }); + assert.equal( + res.headers.get("content-type"), + "text/plain; charset=utf-8", + ); + assert.equal( + res.headers.get("cache-control"), + "max-age=0, s-maxage=3600", + ); + }); + }); +}); diff --git a/apps/site/wrangler.jsonc b/apps/site/wrangler.jsonc index 21ff7dbb..ac1d4203 100644 --- a/apps/site/wrangler.jsonc +++ b/apps/site/wrangler.jsonc @@ -1,6 +1,7 @@ { "$schema": "node_modules/wrangler/config-schema.json", - "name": "codingcatdev", + "name": "codingcatdev-dev", + "account_id": "c46bd0f3b9ed47b8692cca098030edbf", "main": "@astrojs/cloudflare/entrypoints/server", "compatibility_date": "2026-08-01", // nodejs_compat: Sanity client + Portable Text pull Node built-ins. @@ -9,13 +10,19 @@ "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"], "observability": { "enabled": true, "head_sampling_rate": 1 }, "workers_dev": true, + "routes": [ + { + "pattern": "dev.codingcat.dev", + "custom_domain": true + } + ], "vars": { "ENVIRONMENT": "dev", "PUBLIC_SANITY_PROJECT_ID": "hfh83o0w", "PUBLIC_SANITY_DATASET": "dev", "PUBLIC_SANITY_API_VERSION": "2025-09-30", "PUBLIC_SANITY_STUDIO_URL": "https://codingcat-dev.sanity.studio/dev", - "SITE_URL": "https://codingcatdev.workers.dev" + "SITE_URL": "https://dev.codingcat.dev" }, // NOTE: bindings are NOT inherited into `env.*`. Every binding added to the @@ -24,8 +31,15 @@ // account is available to confirm/create the auth databases. "env": { "production": { - "name": "codingcatdev-production", + "name": "codingcatdev-prod", + "account_id": "c46bd0f3b9ed47b8692cca098030edbf", "workers_dev": false, + "routes": [ + { + "pattern": "codingcat.dev", + "custom_domain": true + } + ], "observability": { "enabled": true, "head_sampling_rate": 0.1 }, "vars": { "ENVIRONMENT": "production", diff --git a/package.json b/package.json index 683abfbd..ae65ae27 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,10 @@ "deploy:studio": "pnpm --filter @codingcatdev/sanity deploy", "lint": "pnpm --filter @codingcatdev/site lint", "typecheck": "pnpm --filter @codingcatdev/site typecheck", - "typegen": "pnpm --filter @codingcatdev/sanity typegen" + "typegen": "pnpm --filter @codingcatdev/sanity typegen", + "build:production": "pnpm --filter @codingcatdev/site build:production", + "preview": "pnpm --filter @codingcatdev/site preview", + "test:e2e": "pnpm --filter @codingcatdev/site test:e2e" }, "packageManager": "pnpm@10.34.1" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c2706116..68d14461 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,9 +19,18 @@ importers: '@sanity/assist': specifier: ^6.1.19 version: 6.1.19(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sanity@6.9.2(@babel/core@7.29.7)(@babel/runtime@7.29.2)(@noble/hashes@2.0.1)(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.2)(rolldown@1.2.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)))(@sanity/sdk@2.12.0(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))(xstate@5.32.5))(@types/node@25.5.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.1)(jiti@2.7.0)(prismjs@1.30.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rolldown@1.2.4)(styled-components@6.3.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(terser@5.48.0)(typescript@5.9.3))(styled-components@6.3.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + '@sanity/blueprints': + specifier: ^0.28.0 + version: 0.28.0(jiti@2.7.0)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + '@sanity/client': + specifier: ^8.9.0 + version: 8.9.0 '@sanity/code-input': specifier: ^7.3.5 version: 7.3.5(1260ef73d49f53224850f6a5e7cc5735) + '@sanity/functions': + specifier: ^1.8.1 + version: 1.8.1(@aws-lite/client@0.23.5)(@aws-lite/dynamodb@0.3.9)(@aws-lite/lambda@0.1.5)(@aws-lite/sns@0.0.8)(@aws-lite/sqs@0.2.4) '@sanity/icons': specifier: ^5.2.1 version: 5.2.1(react@19.2.4) @@ -755,10 +764,26 @@ packages: resolution: {integrity: sha512-DZ/onVaVvfJfcOcp63WS7/HEjJjy2D6XpU3R8AhwgH6T6NcsuSWr7FbIkVVa33XebYwIsHyWTsPL7h/Nvt/knA==} engines: {node: '>=16'} + '@aws-lite/dynamodb@0.3.9': + resolution: {integrity: sha512-jrMAWwxoAMVJ3z0/mI/GzPM5AfGmH+xzTpNIbjg3+2WdYJRqvIf8025XJdCDtS9/4x8zogdchEp3ZelXnwYyqw==} + engines: {node: '>=16'} + + '@aws-lite/lambda@0.1.5': + resolution: {integrity: sha512-Sa6AxBQwzqGsUr2ifxcpAvFlTkoDwIQIYgcEFVQ+0qKeV9ci+ggsOOSRGBxrGEdRsnQxUsQ2x0ES/29c8crzQw==} + engines: {node: '>=16'} + '@aws-lite/s3@0.1.22': resolution: {integrity: sha512-9OL95fTvHV80JvFTxLx8hhWQ6DgwHUts02KpXITA8syCDnYgua2rNcpwQ5b6GZzpL7yNXU0dud/Y6edThbffig==} engines: {node: '>=16'} + '@aws-lite/sns@0.0.8': + resolution: {integrity: sha512-MIzHe66kLNyzPFY/DX30uN7DlVQsnBiHPYbq/7syNWuoYSG8bkWuUX2CVIuSL7Ji5jaLpQ4lf8/VQ+SiAeoIZA==} + engines: {node: '>=16'} + + '@aws-lite/sqs@0.2.4': + resolution: {integrity: sha512-a1M3HDdkNE/xJfASlfisAaZ8XF6FpvoJbJsH/gr6pogEFWgNQyvmPVNRElnDY7JW3ee82sEOkMukYRdAbjytNQ==} + engines: {node: '>=16'} + '@aws-lite/ssm@0.2.5': resolution: {integrity: sha512-1B8mZ79ySqlTEfSQ87KZ0XkmTOKQFMO3lUYUGUtwNTUncJINr6nhRWEjk128oBWwEQnpJ7NfpDPjdfg1ICe3xw==} engines: {node: '>=16'} @@ -4587,6 +4612,18 @@ packages: vite: optional: true + '@sanity/blueprints@0.28.0': + resolution: {integrity: sha512-Csg/coSR5TV1tQHpdtVLu4JJUTmdsm37Y7zwCGKjwXD4VTkNlWPhj8wjY6Ti0fxLND8MgbwIxdZAjJ4aPbYbzw==} + engines: {node: '>=22'} + peerDependencies: + jiti: ^2 + vite: '>= 8' + peerDependenciesMeta: + jiti: + optional: true + vite: + optional: true + '@sanity/cli-build@5.2.1': resolution: {integrity: sha512-aJbJUufSxv7cPGQ4WBSn8DPorK9fPm4n1gSAko66DFR9CCLuv0FwdeSYSF7Z2jMK3qNhL56ZKwera9gojRqAdQ==} engines: {node: '>=22.12'} @@ -4632,8 +4669,8 @@ packages: resolution: {integrity: sha512-iEkGiHbrxCnT/A30A6pH+vX4otP3lLp935Vwuv0/YrnETxrKCn6PnsZU+TfTm6ZEZ9kZGs8wKP5yBLmZw5S6OQ==} engines: {node: '>=20'} - '@sanity/client@8.1.0': - resolution: {integrity: sha512-GS9q17kSIpF+NSOMJejAmTw0sKJA5oSTxtipkItnuWFvO4njVQnUQ+1OvRYZyg/AK9dwymlM35ROKuA5XuWyFQ==} + '@sanity/client@8.9.0': + resolution: {integrity: sha512-V6kFNH6xvz29ot66zTVVyzZ34dIGzVJHzQJXEV2ljGVZEelHRoMayhErsTDac/ZR7ej8Spz39qEMYCaJWg176g==} engines: {node: '>=22.12.0'} '@sanity/code-input@7.3.5': @@ -4702,6 +4739,16 @@ packages: engines: {node: '>=20.19 <22 || >=22.12'} hasBin: true + '@sanity/functions@1.8.1': + resolution: {integrity: sha512-HZfnnDuzTwfavAzjB8Ita6GGnM0owXnNSuXTyiUMkMyhsHolyFrPtOlCBlqdyhXmOrtPiyZ6UmbIWEw8FNJgKQ==} + engines: {node: '>=22'} + peerDependencies: + '@aws-lite/client': ^0.23.6 + '@aws-lite/dynamodb': ^0.3.9 + '@aws-lite/lambda': ^0.1.5 + '@aws-lite/sns': ^0.0.8 + '@aws-lite/sqs': ^0.2.4 + '@sanity/generate-help-url@0.140.0': resolution: {integrity: sha512-H/G/WA9S22TXcXST52CIiTsHx3S2hH0gvK7LnI5w76vfKS0obnDPh8jrPg4xeNRYGPuV9MHYRlyERGpRGoo4Qw==} @@ -6903,6 +6950,10 @@ packages: resolution: {integrity: sha512-aIpvYxbqTx4KYWw73KpCt1Rh22tEAJ144F1qR76CT3Ex+LNMEPKf5HFrK5Yz9fMP1EVT0GrfA35VZvWQhMPuSQ==} engines: {node: '>=22.12'} + eventsource-parser@4.1.1: + resolution: {integrity: sha512-zaWNqeSLeyzEAAt2Q6aDBRDRk/VnPmsKqvToyGvv6aahvLv71A4Jw7jG93Hckf11zXurbqgsffvpSgKPoeU1Qw==} + engines: {node: '>=22.12'} + eventsource@2.0.2: resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} engines: {node: '>=12.0.0'} @@ -6915,6 +6966,10 @@ packages: resolution: {integrity: sha512-+errgX9LYbcpibagHh7CEjEZJvQbIDZB2yY7SbUAhHGv4Ircri3T9LH33TUtcXHDy/pFYel1xCi4VOcBnr3wmw==} engines: {node: '>=22.12.0'} + eventsource@5.1.2: + resolution: {integrity: sha512-VJnwqqp+fOT3WDkjnUy49Zuyh48+kH0JdJApZkslrRQyx35zIZJeJYJl0+a+LrTzh5TFv6MdmXnBDv71UXFyWQ==} + engines: {node: '>=22.12.0'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -7145,6 +7200,15 @@ packages: vitest: optional: true + get-it@9.6.1: + resolution: {integrity: sha512-uxMgcolbhJTz8f/cOHLcgde3A0gwqv5p6m8OJ2WJqn7NBi79krofZdp95FD0+7N9UUpmAe76vIEX3kDFijRahg==} + engines: {node: '>=22.12.0'} + peerDependencies: + vitest: '>=2.0.0' + peerDependenciesMeta: + vitest: + optional: true + get-latest-version@6.0.1: resolution: {integrity: sha512-6Zub9FhioDbCJzGTZtetVvAkLeA5UnvQEbKfFZUc62hcZm3gO3Txr21oRGOcT6SdiKhjI0vWd/Jxct+wuLJgHA==} engines: {node: '>=20'} @@ -8395,6 +8459,10 @@ packages: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} + obug@3.0.0: + resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} + engines: {node: '>=12.20.0'} + ofetch@1.5.1: resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} @@ -8487,6 +8555,9 @@ packages: parse-css-color@0.2.1: resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==} + parse-duration@2.1.9: + resolution: {integrity: sha512-q73c2djLx0fPIEcnW4Eb9LBZnH+NunMUzKh3/UwHyMz8LKxd4hF0OVq3nap8x0AevX1QEyCojF7QO/vAb6PFiA==} + parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} @@ -9811,6 +9882,10 @@ packages: resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} engines: {node: '>=20.18.1'} + undici@7.30.0: + resolution: {integrity: sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==} + engines: {node: '>=20.18.1'} + undici@8.10.0: resolution: {integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==} engines: {node: '>=22.19.0'} @@ -10930,8 +11005,16 @@ snapshots: dependencies: aws4: 1.13.2 + '@aws-lite/dynamodb@0.3.9': {} + + '@aws-lite/lambda@0.1.5': {} + '@aws-lite/s3@0.1.22': {} + '@aws-lite/sns@0.0.8': {} + + '@aws-lite/sqs@0.2.4': {} + '@aws-lite/ssm@0.2.5': {} '@aws-sdk/client-cloudwatch-logs@3.986.0': @@ -15825,6 +15908,13 @@ snapshots: optionalDependencies: vite: 8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0) + '@sanity/blueprints@0.28.0(jiti@2.7.0)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0))': + dependencies: + parse-duration: 2.1.9 + optionalDependencies: + jiti: 2.7.0 + vite: 8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0) + '@sanity/cli-build@5.2.1(@babel/core@7.29.7)(@babel/runtime@7.29.2)(@noble/hashes@2.0.1)(@types/node@24.12.4)(@types/react@19.2.14)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.1)(jiti@2.7.0)(rolldown@1.2.4)(terser@5.48.0)(tsx@4.23.12)(typescript@5.9.2)(yaml@2.9.0)': dependencies: '@oclif/core': 4.13.5 @@ -16015,7 +16105,7 @@ snapshots: dependencies: '@inquirer/prompts': 8.5.2(@types/node@24.12.4) '@oclif/core': 4.13.5 - '@sanity/client': 8.1.0 + '@sanity/client': 8.9.0 boxen: 8.0.1 empathic: 2.0.1 get-it: 9.5.0 @@ -16054,7 +16144,7 @@ snapshots: dependencies: '@inquirer/prompts': 8.5.2(@types/node@25.5.0) '@oclif/core': 4.13.5 - '@sanity/client': 8.1.0 + '@sanity/client': 8.9.0 boxen: 8.0.1 empathic: 2.0.1 get-it: 9.5.0 @@ -16307,12 +16397,11 @@ snapshots: nanoid: 3.3.18 rxjs: 7.8.2 - '@sanity/client@8.1.0': + '@sanity/client@8.9.0': dependencies: - eventsource: 5.1.0 - get-it: 9.5.0 - nanoid: 6.0.1 - obug: 2.1.4 + eventsource: 5.1.2 + get-it: 9.6.1 + obug: 3.0.0 rxjs: 7.8.2 transitivePeerDependencies: - vitest @@ -16468,6 +16557,14 @@ snapshots: - react-native-b4a - supports-color + '@sanity/functions@1.8.1(@aws-lite/client@0.23.5)(@aws-lite/dynamodb@0.3.9)(@aws-lite/lambda@0.1.5)(@aws-lite/sns@0.0.8)(@aws-lite/sqs@0.2.4)': + dependencies: + '@aws-lite/client': 0.23.5 + '@aws-lite/dynamodb': 0.3.9 + '@aws-lite/lambda': 0.1.5 + '@aws-lite/sns': 0.0.8 + '@aws-lite/sqs': 0.2.4 + '@sanity/generate-help-url@0.140.0': {} '@sanity/generate-help-url@4.0.0': {} @@ -16693,7 +16790,7 @@ snapshots: '@sanity/blueprints-parser': 0.5.0 '@sanity/cli-build': 5.2.1(@babel/core@7.29.7)(@babel/runtime@7.29.2)(@noble/hashes@2.0.1)(@types/node@24.12.4)(@types/react@19.2.14)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.1)(jiti@2.7.0)(rolldown@1.2.4)(terser@5.48.0)(tsx@4.23.12)(typescript@5.9.2)(yaml@2.9.0) '@sanity/cli-core': 3.1.0(@noble/hashes@2.0.1)(@types/node@24.12.4)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.1)(terser@5.48.0)(yaml@2.9.0) - '@sanity/client': 8.1.0 + '@sanity/client': 8.9.0 adm-zip: 0.6.0 array-treeify: 0.2.0 cardinal: 2.1.1 @@ -16752,7 +16849,7 @@ snapshots: '@sanity/blueprints-parser': 0.5.0 '@sanity/cli-build': 5.2.1(@babel/core@7.29.7)(@babel/runtime@7.29.2)(@noble/hashes@2.0.1)(@types/node@25.5.0)(@types/react@19.2.14)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.1)(jiti@2.7.0)(rolldown@1.2.4)(terser@5.48.0)(tsx@4.23.12)(typescript@5.9.3)(yaml@2.9.0) '@sanity/cli-core': 3.1.0(@noble/hashes@2.0.1)(@types/node@25.5.0)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.1)(terser@5.48.0)(yaml@2.9.0) - '@sanity/client': 8.1.0 + '@sanity/client': 8.9.0 adm-zip: 0.6.0 array-treeify: 0.2.0 cardinal: 2.1.1 @@ -19295,6 +19392,8 @@ snapshots: eventsource-parser@4.0.0: {} + eventsource-parser@4.1.1: {} + eventsource@2.0.2: {} eventsource@4.1.0: @@ -19305,6 +19404,10 @@ snapshots: dependencies: eventsource-parser: 4.0.0 + eventsource@5.1.2: + dependencies: + eventsource-parser: 4.1.1 + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -19566,6 +19669,10 @@ snapshots: dependencies: undici: 7.29.0 + get-it@9.6.1: + dependencies: + undici: 7.30.0 + get-latest-version@6.0.1: dependencies: get-it: 8.7.2 @@ -20903,6 +21010,8 @@ snapshots: obug@2.1.4: {} + obug@3.0.0: {} + ofetch@1.5.1: dependencies: destr: 2.0.5 @@ -21015,6 +21124,8 @@ snapshots: color-name: 1.1.4 hex-rgb: 4.3.0 + parse-duration@2.1.9: {} + parse-entities@4.0.2: dependencies: '@types/unist': 2.0.11 @@ -22830,6 +22941,8 @@ snapshots: undici@7.29.0: {} + undici@7.30.0: {} + undici@8.10.0: {} unenv@2.0.0-rc.24: From bed81e7ec2651ffb1bfb8ade522694024bdd1608 Mon Sep 17 00:00:00 2001 From: Alex Patterson <alex@codingcat.dev> Date: Wed, 7 Oct 2026 18:16:28 -0400 Subject: [PATCH 22/26] ci: add cf-typegen step and automatic PR preview comment with Cloudflare preview URL --- .github/workflows/ci.yml | 36 +++++++++++++++++++++++++++- .github/workflows/deploy-workers.yml | 6 +++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c12e33be..393362ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,12 +30,15 @@ jobs: # Generates apps/sanity/extract.json and the site's types.gen.ts, both of # which are derived artifacts and therefore gitignored. Runs before # typecheck because the site imports the generated types. - - name: Typegen + - name: Typegen (Sanity) run: pnpm typegen env: SANITY_STUDIO_PROJECT_ID: hfh83o0w SANITY_STUDIO_DATASET: production + - name: Typegen (Cloudflare) + run: pnpm --filter @codingcatdev/site cf-typegen + - name: Lint (site) run: pnpm --filter @codingcatdev/site lint @@ -54,6 +57,9 @@ jobs: SANITY_STUDIO_PROJECT_ID: hfh83o0w SANITY_STUDIO_DATASET: production + - name: Run E2E Tests + run: pnpm --filter @codingcatdev/site test:e2e + # extract.json is committed and is the single source of truth for typegen. # The Typegen step above regenerates it; if that changed the file, the # commit is stale. This is what stopped three divergent copies before. @@ -91,6 +97,9 @@ jobs: SANITY_STUDIO_PROJECT_ID: hfh83o0w SANITY_STUDIO_DATASET: dev + - name: Generate Cloudflare Types + run: pnpm --filter @codingcatdev/site cf-typegen + - name: Build site (preview) run: pnpm --filter @codingcatdev/site build @@ -99,7 +108,32 @@ jobs: run: | OUTPUT=$(pnpm --filter @codingcatdev/site exec wrangler versions upload --tag "pr-${{ github.event.pull_request.number }}" --message "PR #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}") echo "$OUTPUT" + + PREVIEW_URL=$(echo "$OUTPUT" | grep -i "Version Preview URL:" | awk '{print $NF}' | head -n 1) + VERSION_ID=$(echo "$OUTPUT" | grep -i "Worker Version ID:" | awk '{print $NF}' | head -n 1) + + echo "preview_url=$PREVIEW_URL" >> $GITHUB_OUTPUT + echo "version_id=$VERSION_ID" >> $GITHUB_OUTPUT + + echo "### ⚡ Cloudflare Worker Preview Ready" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Preview URL**: [${PREVIEW_URL}](${PREVIEW_URL})" >> $GITHUB_STEP_SUMMARY + echo "- **Version ID**: \`${VERSION_ID}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Target Worker**: \`codingcatdev-dev\`" >> $GITHUB_STEP_SUMMARY + + if [ -n "$PREVIEW_URL" ]; then + gh pr comment ${{ github.event.pull_request.number }} --body "### ⚡ Cloudflare Worker Preview Ready! + +| Environment | URL | +|---|---| +| **PR Preview** | [${PREVIEW_URL}](${PREVIEW_URL}) | +| **Dev Custom Domain** | [https://dev.codingcat.dev](https://dev.codingcat.dev) | + +- **Worker Version ID**: \`${VERSION_ID}\` +- **Protection**: Restricted to \`@codingcat.dev\` accounts via Cloudflare Zero Trust Access" + fi env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID || 'c46bd0f3b9ed47b8692cca098030edbf' }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/deploy-workers.yml b/.github/workflows/deploy-workers.yml index a0b834cf..19405381 100644 --- a/.github/workflows/deploy-workers.yml +++ b/.github/workflows/deploy-workers.yml @@ -36,6 +36,9 @@ jobs: SANITY_STUDIO_PROJECT_ID: hfh83o0w SANITY_STUDIO_DATASET: dev + - name: Generate Cloudflare Types + run: pnpm --filter @codingcatdev/site cf-typegen + - name: Build site (dev) run: pnpm --filter @codingcatdev/site build @@ -67,6 +70,9 @@ jobs: SANITY_STUDIO_PROJECT_ID: hfh83o0w SANITY_STUDIO_DATASET: production + - name: Generate Cloudflare Types + run: pnpm --filter @codingcatdev/site cf-typegen + - name: Build site (production) run: pnpm --filter @codingcatdev/site build:production From c82bd03f2c187bf0a541de6c7fd09fed9d4110ff Mon Sep 17 00:00:00 2001 From: Alex Patterson <alex@codingcat.dev> Date: Wed, 7 Oct 2026 18:17:36 -0400 Subject: [PATCH 23/26] fix(ci): fix yaml indentation and use echo for pr comment markdown --- .github/workflows/ci.yml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 393362ee..9979ae20 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,15 +122,16 @@ jobs: echo "- **Target Worker**: \`codingcatdev-dev\`" >> $GITHUB_STEP_SUMMARY if [ -n "$PREVIEW_URL" ]; then - gh pr comment ${{ github.event.pull_request.number }} --body "### ⚡ Cloudflare Worker Preview Ready! - -| Environment | URL | -|---|---| -| **PR Preview** | [${PREVIEW_URL}](${PREVIEW_URL}) | -| **Dev Custom Domain** | [https://dev.codingcat.dev](https://dev.codingcat.dev) | - -- **Worker Version ID**: \`${VERSION_ID}\` -- **Protection**: Restricted to \`@codingcat.dev\` accounts via Cloudflare Zero Trust Access" + echo "### ⚡ Cloudflare Worker Preview Ready!" > pr_comment.md + echo "" >> pr_comment.md + echo "| Environment | URL |" >> pr_comment.md + echo "|---|---|" >> pr_comment.md + echo "| **PR Preview** | [${PREVIEW_URL}](${PREVIEW_URL}) |" >> pr_comment.md + echo "| **Dev Custom Domain** | [https://dev.codingcat.dev](https://dev.codingcat.dev) |" >> pr_comment.md + echo "" >> pr_comment.md + echo "- **Worker Version ID**: \`${VERSION_ID}\`" >> pr_comment.md + echo "- **Protection**: Restricted to \`@codingcat.dev\` accounts via Cloudflare Zero Trust Access" >> pr_comment.md + gh pr comment ${{ github.event.pull_request.number }} --body-file pr_comment.md fi env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} From ce71b9c4d9f7913240bb6cb2bcef337c5685fb27 Mon Sep 17 00:00:00 2001 From: Alex Patterson <alex@codingcat.dev> Date: Wed, 7 Oct 2026 18:23:19 -0400 Subject: [PATCH 24/26] fix(ci): fix hardcoded font paths in test mocks and enhance pr preview url reporting --- .github/workflows/ci.yml | 39 +++++++++++++++++++++--------- apps/site/tests/font-bold-mock.mjs | 8 +++--- apps/site/tests/font-reg-mock.mjs | 8 +++--- apps/site/tests/run-e2e.mjs | 13 ++++++++-- 4 files changed, 49 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9979ae20..b70faad7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,7 +106,7 @@ jobs: - name: Upload Cloudflare Worker Preview Version id: upload_preview run: | - OUTPUT=$(pnpm --filter @codingcatdev/site exec wrangler versions upload --tag "pr-${{ github.event.pull_request.number }}" --message "PR #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}") + OUTPUT=$(pnpm --filter @codingcatdev/site exec wrangler versions upload --tag "pr-${PR_NUMBER}" --message "PR #${PR_NUMBER}: ${PR_TITLE}") echo "$OUTPUT" PREVIEW_URL=$(echo "$OUTPUT" | grep -i "Version Preview URL:" | awk '{print $NF}' | head -n 1) @@ -115,6 +115,8 @@ jobs: echo "preview_url=$PREVIEW_URL" >> $GITHUB_OUTPUT echo "version_id=$VERSION_ID" >> $GITHUB_OUTPUT + echo "::notice title=Cloudflare Worker Preview URL::${PREVIEW_URL}" + echo "### ⚡ Cloudflare Worker Preview Ready" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "- **Preview URL**: [${PREVIEW_URL}](${PREVIEW_URL})" >> $GITHUB_STEP_SUMMARY @@ -122,19 +124,34 @@ jobs: echo "- **Target Worker**: \`codingcatdev-dev\`" >> $GITHUB_STEP_SUMMARY if [ -n "$PREVIEW_URL" ]; then - echo "### ⚡ Cloudflare Worker Preview Ready!" > pr_comment.md - echo "" >> pr_comment.md - echo "| Environment | URL |" >> pr_comment.md - echo "|---|---|" >> pr_comment.md - echo "| **PR Preview** | [${PREVIEW_URL}](${PREVIEW_URL}) |" >> pr_comment.md - echo "| **Dev Custom Domain** | [https://dev.codingcat.dev](https://dev.codingcat.dev) |" >> pr_comment.md - echo "" >> pr_comment.md - echo "- **Worker Version ID**: \`${VERSION_ID}\`" >> pr_comment.md - echo "- **Protection**: Restricted to \`@codingcat.dev\` accounts via Cloudflare Zero Trust Access" >> pr_comment.md - gh pr comment ${{ github.event.pull_request.number }} --body-file pr_comment.md + cat << 'EOF' > pr_comment.md +### ⚡ Cloudflare Worker Preview Ready! + +| Environment | URL | +|---|---| +| **PR Preview** | [${PREVIEW_URL}](${PREVIEW_URL}) | +| **Dev Custom Domain** | [https://dev.codingcat.dev](https://dev.codingcat.dev) | + +- **Worker Version ID**: `${VERSION_ID}` +- **Protection**: Restricted to `@codingcat.dev` accounts via Cloudflare Zero Trust Access +EOF + # Replace placeholder variables with actual values in pr_comment.md + sed -i "s|\${PREVIEW_URL}|${PREVIEW_URL}|g" pr_comment.md + sed -i "s|\${VERSION_ID}|${VERSION_ID}|g" pr_comment.md + + # Check if bot has already posted a preview comment on this PR + COMMENT_ID=$(gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" --jq '.[] | select((.user.login=="github-actions[bot]" or .user.login=="app/github-actions") and (.body | contains("Cloudflare Worker Preview Ready"))) | .id' | tail -n 1) + + if [ -n "$COMMENT_ID" ]; then + gh api -X PATCH "repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" -f body="$(cat pr_comment.md)" + else + gh pr comment "$PR_NUMBER" --body-file pr_comment.md + fi fi env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID || 'c46bd0f3b9ed47b8692cca098030edbf' }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} diff --git a/apps/site/tests/font-bold-mock.mjs b/apps/site/tests/font-bold-mock.mjs index af7bb692..73756c98 100644 --- a/apps/site/tests/font-bold-mock.mjs +++ b/apps/site/tests/font-bold-mock.mjs @@ -1,6 +1,8 @@ import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; -const buf = fs.readFileSync( - "/home/alex/codingcat.dev/apps/site/src/assets/fonts/Inter-Bold.ttf", -); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const fontPath = path.resolve(__dirname, "../src/assets/fonts/Inter-Bold.ttf"); +const buf = fs.existsSync(fontPath) ? fs.readFileSync(fontPath) : Buffer.from([]); export default new Uint8Array(buf); diff --git a/apps/site/tests/font-reg-mock.mjs b/apps/site/tests/font-reg-mock.mjs index f5b76655..e457382d 100644 --- a/apps/site/tests/font-reg-mock.mjs +++ b/apps/site/tests/font-reg-mock.mjs @@ -1,6 +1,8 @@ import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; -const buf = fs.readFileSync( - "/home/alex/codingcat.dev/apps/site/src/assets/fonts/Inter-Regular.ttf", -); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const fontPath = path.resolve(__dirname, "../src/assets/fonts/Inter-Regular.ttf"); +const buf = fs.existsSync(fontPath) ? fs.readFileSync(fontPath) : Buffer.from([]); export default new Uint8Array(buf); diff --git a/apps/site/tests/run-e2e.mjs b/apps/site/tests/run-e2e.mjs index c7d9dcf4..4ecf002c 100755 --- a/apps/site/tests/run-e2e.mjs +++ b/apps/site/tests/run-e2e.mjs @@ -98,6 +98,8 @@ async function main() { files: filesToRun, }); + const failureDetails = []; + stream.compose(new spec()).pipe(process.stdout); stream.on("test:pass", () => { @@ -105,9 +107,10 @@ async function main() { totalTests++; }); - stream.on("test:fail", () => { + stream.on("test:fail", (data) => { failedTests++; totalTests++; + failureDetails.push(data); }); await new Promise((resolve) => stream.on("end", resolve)); @@ -142,7 +145,13 @@ async function main() { ); if (failedTests > 0) { - console.error(`\n❌ TEST SUITE FAILED with ${failedTests} failure(s).`); + console.error(`\n❌ TEST SUITE FAILED with ${failedTests} failure(s):`); + for (const f of failureDetails) { + console.error(`\n- Test: ${f.name}`); + if (f.details?.error) { + console.error(f.details.error.stack || f.details.error.message || f.details.error); + } + } process.exit(1); } else { console.log( From 011d01fb627e7836648aae020875e779c3571c00 Mon Sep 17 00:00:00 2001 From: Alex Patterson <alex@codingcat.dev> Date: Wed, 7 Oct 2026 18:25:44 -0400 Subject: [PATCH 25/26] fix(ci): fix yaml indentation and clean up secrets syntax in workflows --- .github/workflows/ci.yml | 30 ++++++++++++---------------- .github/workflows/deploy-workers.yml | 4 ++-- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b70faad7..46c657e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,22 +124,18 @@ jobs: echo "- **Target Worker**: \`codingcatdev-dev\`" >> $GITHUB_STEP_SUMMARY if [ -n "$PREVIEW_URL" ]; then - cat << 'EOF' > pr_comment.md -### ⚡ Cloudflare Worker Preview Ready! - -| Environment | URL | -|---|---| -| **PR Preview** | [${PREVIEW_URL}](${PREVIEW_URL}) | -| **Dev Custom Domain** | [https://dev.codingcat.dev](https://dev.codingcat.dev) | - -- **Worker Version ID**: `${VERSION_ID}` -- **Protection**: Restricted to `@codingcat.dev` accounts via Cloudflare Zero Trust Access -EOF - # Replace placeholder variables with actual values in pr_comment.md - sed -i "s|\${PREVIEW_URL}|${PREVIEW_URL}|g" pr_comment.md - sed -i "s|\${VERSION_ID}|${VERSION_ID}|g" pr_comment.md - - # Check if bot has already posted a preview comment on this PR + { + echo "### ⚡ Cloudflare Worker Preview Ready!" + echo "" + echo "| Environment | URL |" + echo "|---|---|" + echo "| **PR Preview** | [${PREVIEW_URL}](${PREVIEW_URL}) |" + echo "| **Dev Custom Domain** | [https://dev.codingcat.dev](https://dev.codingcat.dev) |" + echo "" + echo "- **Worker Version ID**: \`${VERSION_ID}\`" + echo "- **Protection**: Restricted to \`@codingcat.dev\` accounts via Cloudflare Zero Trust Access" + } > pr_comment.md + COMMENT_ID=$(gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" --jq '.[] | select((.user.login=="github-actions[bot]" or .user.login=="app/github-actions") and (.body | contains("Cloudflare Worker Preview Ready"))) | .id' | tail -n 1) if [ -n "$COMMENT_ID" ]; then @@ -150,7 +146,7 @@ EOF fi env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID || 'c46bd0f3b9ed47b8692cca098030edbf' }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} PR_TITLE: ${{ github.event.pull_request.title }} diff --git a/.github/workflows/deploy-workers.yml b/.github/workflows/deploy-workers.yml index 19405381..0b40510c 100644 --- a/.github/workflows/deploy-workers.yml +++ b/.github/workflows/deploy-workers.yml @@ -46,7 +46,7 @@ jobs: run: pnpm --filter @codingcatdev/site exec wrangler deploy env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID || 'c46bd0f3b9ed47b8692cca098030edbf' }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} deploy-production: if: github.ref == 'refs/heads/main' @@ -80,4 +80,4 @@ jobs: run: pnpm --filter @codingcatdev/site exec wrangler deploy --env production env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID || 'c46bd0f3b9ed47b8692cca098030edbf' }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} From ab8cc23e18e75e16f328e4f5aa137bd0a2a566f5 Mon Sep 17 00:00:00 2001 From: Alex Patterson <alex@codingcat.dev> Date: Wed, 7 Oct 2026 18:28:56 -0400 Subject: [PATCH 26/26] feat(sanity): dedicated studio dataset workspaces and deploy workflow environment passing --- .github/workflows/deploy.yml | 7 +++- apps/sanity/sanity.config.ts | 61 +++++++++++++++++------------- apps/site/tests/font-bold-mock.mjs | 4 +- apps/site/tests/font-reg-mock.mjs | 9 ++++- apps/site/tests/run-e2e.mjs | 4 +- 5 files changed, 54 insertions(+), 31 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 017dd190..af82605d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,6 +1,7 @@ name: Deploy Sanity Studio on: + workflow_dispatch: push: branches: [dev, main] paths: @@ -10,7 +11,7 @@ on: jobs: deploy-dev: - if: github.ref == 'refs/heads/dev' + if: github.ref == 'refs/heads/dev' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest environment: dev @@ -33,6 +34,8 @@ jobs: env: SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }} SANITY_STUDIO_HOSTNAME: ${{ vars.SANITY_STUDIO_HOSTNAME }} + SANITY_STUDIO_DATASET: ${{ vars.SANITY_STUDIO_DATASET }} + SANITY_STUDIO_PROJECT_ID: ${{ vars.SANITY_STUDIO_PROJECT_ID }} deploy-production: if: github.ref == 'refs/heads/main' @@ -58,3 +61,5 @@ jobs: env: SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }} SANITY_STUDIO_HOSTNAME: ${{ vars.SANITY_STUDIO_HOSTNAME }} + SANITY_STUDIO_DATASET: ${{ vars.SANITY_STUDIO_DATASET }} + SANITY_STUDIO_PROJECT_ID: ${{ vars.SANITY_STUDIO_PROJECT_ID }} diff --git a/apps/sanity/sanity.config.ts b/apps/sanity/sanity.config.ts index 9c9a963a..b6db3db2 100644 --- a/apps/sanity/sanity.config.ts +++ b/apps/sanity/sanity.config.ts @@ -279,29 +279,38 @@ function buildPlugins(previewUrl: string): PluginOptions[] { } // ── Workspace definitions ──────────────────────────────────────────── -export default defineConfig([ - { - name: "production", - title: "CodingCat.dev (Production)", - projectId, - dataset: "production", - basePath: "/production", - schema: { types: schemaTypes }, - document: { actions: documentActions }, - plugins: buildPlugins( - isLocal ? localPreviewOrigin : "https://codingcat.dev", - ), - }, - { - name: "dev", - title: "CodingCat.dev (Dev)", - projectId, - dataset: "dev", - basePath: "/dev", - schema: { types: schemaTypes }, - document: { actions: documentActions }, - plugins: buildPlugins( - isLocal ? localPreviewOrigin : "https://dev.codingcat.dev", - ), - }, -]); +const targetDataset = process.env.SANITY_STUDIO_DATASET; + +const devWorkspace = { + name: "dev", + title: "CodingCat.dev (Dev)", + projectId, + dataset: "dev", + basePath: targetDataset === "dev" ? "/" : "/dev", + schema: { types: schemaTypes }, + document: { actions: documentActions }, + plugins: buildPlugins( + isLocal ? localPreviewOrigin : "https://dev.codingcat.dev", + ), +}; + +const prodWorkspace = { + name: "production", + title: "CodingCat.dev (Production)", + projectId, + dataset: "production", + basePath: targetDataset === "production" ? "/" : "/production", + schema: { types: schemaTypes }, + document: { actions: documentActions }, + plugins: buildPlugins( + isLocal ? localPreviewOrigin : "https://codingcat.dev", + ), +}; + +export default defineConfig( + targetDataset === "dev" + ? [devWorkspace] + : targetDataset === "production" + ? [prodWorkspace] + : [prodWorkspace, devWorkspace], +); diff --git a/apps/site/tests/font-bold-mock.mjs b/apps/site/tests/font-bold-mock.mjs index 73756c98..57cb0390 100644 --- a/apps/site/tests/font-bold-mock.mjs +++ b/apps/site/tests/font-bold-mock.mjs @@ -4,5 +4,7 @@ import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const fontPath = path.resolve(__dirname, "../src/assets/fonts/Inter-Bold.ttf"); -const buf = fs.existsSync(fontPath) ? fs.readFileSync(fontPath) : Buffer.from([]); +const buf = fs.existsSync(fontPath) + ? fs.readFileSync(fontPath) + : Buffer.from([]); export default new Uint8Array(buf); diff --git a/apps/site/tests/font-reg-mock.mjs b/apps/site/tests/font-reg-mock.mjs index e457382d..24d133f8 100644 --- a/apps/site/tests/font-reg-mock.mjs +++ b/apps/site/tests/font-reg-mock.mjs @@ -3,6 +3,11 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const fontPath = path.resolve(__dirname, "../src/assets/fonts/Inter-Regular.ttf"); -const buf = fs.existsSync(fontPath) ? fs.readFileSync(fontPath) : Buffer.from([]); +const fontPath = path.resolve( + __dirname, + "../src/assets/fonts/Inter-Regular.ttf", +); +const buf = fs.existsSync(fontPath) + ? fs.readFileSync(fontPath) + : Buffer.from([]); export default new Uint8Array(buf); diff --git a/apps/site/tests/run-e2e.mjs b/apps/site/tests/run-e2e.mjs index 4ecf002c..36b7ea1a 100755 --- a/apps/site/tests/run-e2e.mjs +++ b/apps/site/tests/run-e2e.mjs @@ -149,7 +149,9 @@ async function main() { for (const f of failureDetails) { console.error(`\n- Test: ${f.name}`); if (f.details?.error) { - console.error(f.details.error.stack || f.details.error.message || f.details.error); + console.error( + f.details.error.stack || f.details.error.message || f.details.error, + ); } } process.exit(1);