` or GFM tables).
+- **Code:** Need a `code` type (HTML `` or MD code fences).
diff --git a/.agents/skills/sanity-best-practices/references/nextjs.md b/.agents/skills/sanity-best-practices/references/nextjs.md
new file mode 100644
index 000000000..cad3d239f
--- /dev/null
+++ b/.agents/skills/sanity-best-practices/references/nextjs.md
@@ -0,0 +1,544 @@
+---
+title: Next.js & Sanity Integration Rules
+description: Integration guide for Next.js App Router, Live Content API, and Sanity Studio (Embedded or Standalone).
+---
+
+# Next.js & Sanity Integration Rules
+
+Jump to the section that matches the task instead of reading this guide end-to-end.
+
+## Table of Contents
+
+- Architecture patterns
+- Data fetching (Live Content API)
+- Caching and revalidation
+- Visual Editing and clean data
+- Embedded Studio setup
+- Draft Mode setup
+- Error handling
+- Presentation queries
+- Pagination pattern
+
+## 1. Architecture Patterns
+
+### Option A: Embedded Studio (Recommended)
+**Best for:** Most Next.js projects. Unified deployment, simpler setup.
+
+The Studio lives inside your Next.js app at `/app/studio/[[...tool]]/page.tsx`.
+- **Config:** `sanity.config.ts` lives in the project root.
+- See `project-structure.md` rule for detailed structure.
+
+### Option B: Monorepo (Alternative)
+**Best for:** Separation of concerns, multiple frontends, or strict dependency isolation.
+
+The Studio and Next.js app live in separate folders:
+```
+apps/
+├── studio/ # Sanity Studio (standalone)
+└── web/ # Next.js frontend
+```
+
+- **Config:** Add your Next.js app URL to **CORS Origins** in [Sanity Manage](https://www.sanity.io/manage).
+- See `project-structure.md` rule for detailed structure.
+
+## 2. Data Fetching (Live Content API)
+
+We use `defineLive` (next-sanity v11+) to enable real-time content updates and Visual Editing automatically.
+
+### Setup (`src/sanity/lib/live.ts`)
+
+```typescript
+import { defineLive } from 'next-sanity'
+import { client } from './client'
+
+export const { sanityFetch, SanityLive } = defineLive({
+ client: client.withConfig({
+ apiVersion: '2026-02-01'
+ }),
+ serverToken: process.env.SANITY_API_READ_TOKEN,
+ browserToken: process.env.SANITY_API_READ_TOKEN,
+})
+```
+
+### Rendering (`src/app/layout.tsx`)
+
+You **must** render `` in the root layout to enable real-time updates.
+
+```typescript
+import { SanityLive } from '@/sanity/lib/live'
+import { VisualEditing } from 'next-sanity/visual-editing'
+import { draftMode } from 'next/headers'
+
+export default async function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+ {(await draftMode()).isEnabled && }
+
+
+ )
+}
+```
+
+## 3. Caching & Revalidation
+
+### Prefer Live Content API (Default)
+
+**Use `defineLive` by default.** It handles fetching, caching, and invalidation automatically. Only implement manual caching when you need fine-grained control.
+
+### When to Use Manual Caching
+
+| Scenario | Approach |
+|----------|----------|
+| Real-time updates, Visual Editing | `defineLive` (default) |
+| Static marketing pages, rarely updated | Time-based revalidation |
+| Blog posts, products with frequent edits | Tag-based revalidation |
+| Critical accuracy (stock levels, prices) | Path-based + short revalidation |
+
+### Debugging: Enable Fetch Logging
+
+See every fetch with cache HIT/MISS status:
+
+```typescript
+// next.config.ts
+const nextConfig: NextConfig = {
+ logging: {
+ fetches: {
+ fullUrl: true,
+ },
+ },
+};
+```
+
+Console output shows cache status:
+```text
+GET /posts 200 in 39ms
+ │ GET https://...apicdn.sanity.io/... 200 in 5ms (cache hit)
+```
+
+### Sanity CDN vs API
+
+| Setting | Speed | Freshness | Use When |
+|---------|-------|-----------|----------|
+| `useCdn: true` | Fast | May have brief delay | Default for all runtime fetches |
+| `useCdn: false` | Slower | Guaranteed fresh | `generateStaticParams`, webhooks |
+
+Override per-request:
+```typescript
+// For static generation, use API directly
+export async function generateStaticParams() {
+ const slugs = await client
+ .withConfig({ useCdn: false })
+ .fetch(SLUGS_QUERY);
+ return slugs;
+}
+```
+
+### Manual `sanityFetch` Helper (Advanced)
+
+For manual caching control, create a wrapper:
+
+```typescript
+// src/sanity/lib/client.ts
+export async function sanityFetch({
+ query,
+ params = {},
+ revalidate = 60,
+ tags = [],
+}: {
+ query: QueryString;
+ params?: QueryParams;
+ revalidate?: number | false;
+ tags?: string[];
+}) {
+ return client.fetch(query, params, {
+ next: {
+ revalidate: tags.length ? false : revalidate,
+ tags,
+ },
+ });
+}
+```
+
+### Time-Based Revalidation
+
+Simple and predictable. Good for content that changes infrequently.
+
+```typescript
+const posts = await sanityFetch({
+ query: POSTS_QUERY,
+ revalidate: 3600, // Revalidate every hour
+});
+```
+
+**The "Typo Problem":** With time-based only, content authors may wait up to an hour to see changes. Use webhooks for instant updates.
+
+### Path-Based Revalidation
+
+Surgically revalidate specific routes when documents change.
+
+**1. Create API Route:**
+```typescript
+// src/app/api/revalidate/path/route.ts
+import { revalidatePath } from 'next/cache';
+import { type NextRequest, NextResponse } from 'next/server';
+import { parseBody } from 'next-sanity/webhook';
+
+type WebhookPayload = { path?: string };
+
+export async function POST(req: NextRequest) {
+ try {
+ const { isValidSignature, body } = await parseBody(
+ req,
+ process.env.SANITY_REVALIDATE_SECRET,
+ true // Add delay to allow CDN to update
+ );
+
+ if (!isValidSignature) {
+ return new Response('Invalid signature', { status: 401 });
+ }
+ if (!body?.path) {
+ return new Response('Missing path', { status: 400 });
+ }
+
+ revalidatePath(body.path);
+ return NextResponse.json({ revalidated: body.path });
+ } catch (err) {
+ return new Response((err as Error).message, { status: 500 });
+ }
+}
+```
+
+**2. Create GROQ-Powered Webhook:**
+- URL: `https://yoursite.com/api/revalidate/path`
+- Filter: `_type in ["post"]`
+- Projection: `{ "path": "/posts/" + slug.current }`
+- Add `SANITY_REVALIDATE_SECRET` to webhook and `.env.local`
+
+### Tag-Based Revalidation
+
+"Update once, revalidate everywhere" — best for referenced content.
+
+**1. Tag Your Queries:**
+```typescript
+// Posts index - revalidate when ANY post, author, or category changes
+const posts = await sanityFetch({
+ query: POSTS_QUERY,
+ tags: ['post', 'author', 'category'],
+});
+
+// Individual post - more granular, includes slug-specific tag
+const post = await sanityFetch({
+ query: POST_QUERY,
+ params,
+ tags: [`post:${params.slug}`, 'author', 'category'],
+});
+```
+
+**2. Create API Route:**
+```typescript
+// src/app/api/revalidate/tag/route.ts
+import { revalidateTag } from 'next/cache';
+import { type NextRequest, NextResponse } from 'next/server';
+import { parseBody } from 'next-sanity/webhook';
+
+type WebhookPayload = { tags: string[] };
+
+export async function POST(req: NextRequest) {
+ try {
+ const { isValidSignature, body } = await parseBody(
+ req,
+ process.env.SANITY_REVALIDATE_SECRET,
+ true
+ );
+
+ if (!isValidSignature) {
+ return new Response('Invalid signature', { status: 401 });
+ }
+ if (!Array.isArray(body?.tags) || !body.tags.length) {
+ return new Response('Missing tags', { status: 400 });
+ }
+
+ body.tags.forEach((tag) => revalidateTag(tag));
+ return NextResponse.json({ revalidated: body.tags });
+ } catch (err) {
+ return new Response((err as Error).message, { status: 500 });
+ }
+}
+```
+
+**3. Create GROQ-Powered Webhook:**
+- URL: `https://yoursite.com/api/revalidate/tag`
+- Filter: `_type in ["post", "author", "category"]`
+- Projection: `{ "tags": [_type, _type + ":" + slug.current] }`
+
+### Stale Data After Webhook?
+
+Webhooks fire *before* Sanity CDN updates. If you see stale data:
+
+1. **Add delay** — Pass `true` as third arg to `parseBody`
+2. **Or bypass CDN** — Set `useCdn: false` in client config (use sparingly)
+
+## 4. Visual Editing (Stega) & Clean Data
+
+Visual Editing injects invisible characters into strings to enable click-to-edit.
+
+### A. The Golden Rule of Stega
+
+If a string field controls logic (alignment, colors, IDs), you **must** clean it before comparing.
+
+```typescript
+import { stegaClean } from "@sanity/client/stega";
+
+export function Layout({ align }: { align: string }) {
+ // ❌ Bad: Will fail in Edit Mode due to invisible chars
+ // if (align === 'center') ...
+
+ // ✅ Good: Clean the value first
+ const cleanAlign = stegaClean(align);
+ return
+}
+```
+
+### B. Metadata & SEO (Critical)
+
+**Never** let Stega characters leak into `` tags. Always set `stega: false` for metadata fetching.
+
+```typescript
+export async function generateMetadata({ params }) {
+ const { data } = await sanityFetch({
+ query: SEO_QUERY,
+ params: await params,
+ stega: false // 👈 Critical for SEO
+ })
+ return { title: data?.title }
+}
+```
+
+### C. Static Params
+
+When generating static params, fetch only published content and disable stega.
+
+```typescript
+export async function generateStaticParams() {
+ const { data } = await sanityFetch({
+ query: SLUGS_QUERY,
+ perspective: 'published', // 👈 No drafts
+ stega: false
+ })
+ return data
+}
+```
+
+## 5. Setup: Embedded Studio
+
+Mount the Studio on a Next.js route.
+
+**`src/app/studio/[[...tool]]/page.tsx`:**
+
+```typescript
+import { NextStudio } from 'next-sanity/studio'
+import config from '../../../../sanity.config'
+
+export const dynamic = 'force-static'
+export { metadata, viewport } from 'next-sanity/studio'
+
+export default function StudioPage() {
+ return
+}
+```
+
+## 6. Setup: Draft Mode
+
+Enable Presentation Tool and Visual Editing by setting up a draft mode route.
+
+**`src/app/api/draft-mode/enable/route.ts`:**
+
+```typescript
+import { client } from '@/sanity/lib/client'
+import { defineEnableDraftMode } from 'next-sanity/draft-mode'
+import { token } from '@/sanity/lib/token' // Helper to get token
+
+export const { GET } = defineEnableDraftMode({
+ client: client.withConfig({ token }),
+})
+```
+
+## 7. Error Handling
+
+Use `notFound()` for missing documents. Common errors:
+
+| Error | Cause | Solution |
+|-------|-------|----------|
+| 401 Unauthorized | Invalid/missing token | Check `SANITY_API_READ_TOKEN` |
+| 403 Forbidden | CORS not configured | Add URL to CORS origins |
+| Query syntax error | Invalid GROQ | Test in Vision plugin first |
+| Empty result | Wrong filter/params | Log params, check `_type` spelling |
+
+```typescript
+import { notFound } from 'next/navigation'
+
+export default async function PostPage({ params }: Props) {
+ const { data } = await sanityFetch({ query: POST_QUERY, params: await params })
+ if (!data) notFound()
+ return
+}
+```
+
+## 8. Presentation Queries (`usePresentationQuery`)
+
+For faster live editing in the Presentation Tool, use `usePresentationQuery` to fetch only the specific block being edited, rather than re-rendering the entire page.
+
+### Why Use This
+
+- **Without:** Editing a hero title re-fetches the whole page, re-renders all blocks
+- **With:** Only the hero block re-fetches and re-renders
+
+This is especially valuable for pages with many Page Builder blocks or complex Portable Text.
+
+### Basic Pattern
+
+```typescript
+'use client'
+import { usePresentationQuery } from 'next-sanity/hooks'
+import { HERO_PRESENTATION_QUERY } from '@/sanity/lib/queries'
+
+type HeroProps = {
+ _key: string
+ documentId: string
+ title: string
+ subtitle?: string
+ // ... other initial props from page query
+}
+
+export function Hero({ _key, documentId, title, subtitle, ...rest }: HeroProps) {
+ // Fetch block-specific data for faster updates in Presentation Tool
+ const { data } = usePresentationQuery({
+ query: HERO_PRESENTATION_QUERY,
+ params: { documentId, blockKey: _key },
+ })
+
+ // Use presentation data if available, fallback to initial server props
+ const blockData = data?.heroBlock || { title, subtitle, ...rest }
+
+ return (
+
+ {blockData.title}
+ {blockData.subtitle && {blockData.subtitle}
}
+
+ )
+}
+```
+
+### The Presentation Query
+
+Create a query that targets the specific block by `_key`:
+
+```typescript
+// queries.ts
+export const HERO_PRESENTATION_QUERY = defineQuery(`
+ *[_id == $documentId][0]{
+ _id,
+ _type,
+ "heroBlock": pageBuilder[_key == $blockKey && _type == "hero"][0]{
+ title,
+ subtitle,
+ image,
+ theme,
+ // Include all fields the component needs
+ }
+ }
+`)
+```
+
+### Passing Document Context
+
+Your PageBuilder component needs to pass `documentId` to each block:
+
+```typescript
+export function PageBuilder({ content, documentId }: { content: Block[]; documentId: string }) {
+ return (
+
+ {content.map((block) => {
+ switch (block._type) {
+ case "hero":
+ return
+ // ... other blocks
+ }
+ })}
+
+ )
+}
+```
+
+### For Portable Text Blocks
+
+The same pattern works for custom blocks inside Portable Text:
+
+```typescript
+export const PTE_IMAGE_PRESENTATION_QUERY = defineQuery(`
+ *[_id == $documentId][0]{
+ "pteImageBlock": body[_key == $blockKey && _type == "pteImage"][0]{
+ image,
+ caption,
+ alt
+ }
+ }
+`)
+```
+
+**See also:** `visual-editing.md` for the conceptual overview and `page-builder.md` for full Page Builder patterns.
+
+## 9. Pagination Pattern
+
+For listing pages with many entries, use offset-based pagination with a count query.
+
+### Queries
+```typescript
+// Paginated listing
+export const ARTICLES_QUERY = defineQuery(`
+ *[_type == "article" && defined(slug.current)]
+ | order(date desc) [$start...$end] {
+ _id, title, "slug": slug.current, date
+ }
+`);
+
+// Total count for pagination UI
+export const ARTICLES_COUNT_QUERY = defineQuery(`
+ count(*[_type == "article" && defined(slug.current)])
+`);
+```
+
+### Listing Page
+```typescript
+const ENTRIES_PER_PAGE = 10;
+
+export default async function BlogPage({
+ searchParams
+}: {
+ searchParams: Promise<{ page?: string }>
+}) {
+ const { page: pageParam } = await searchParams;
+ const page = parseInt(pageParam || "1");
+ const start = (page - 1) * ENTRIES_PER_PAGE;
+ const end = start + ENTRIES_PER_PAGE;
+
+ const [{ data: articles }, { data: total }] = await Promise.all([
+ sanityFetch({ query: ARTICLES_QUERY, params: { start, end } }),
+ sanityFetch({ query: ARTICLES_COUNT_QUERY })
+ ]);
+
+ const totalPages = Math.ceil(total / ENTRIES_PER_PAGE);
+
+ return (
+
+ {articles.map(article => (
+
+ ))}
+
+
+ );
+}
+```
diff --git a/.agents/skills/sanity-best-practices/references/nuxt.md b/.agents/skills/sanity-best-practices/references/nuxt.md
new file mode 100644
index 000000000..4169ccff7
--- /dev/null
+++ b/.agents/skills/sanity-best-practices/references/nuxt.md
@@ -0,0 +1,84 @@
+---
+title: Nuxt & Sanity Integration Rules
+description: Integration guide for Nuxt, including @nuxtjs/sanity, visual editing, and data fetching.
+---
+
+# Nuxt & Sanity Integration Rules
+
+## 1. Setup & Configuration
+
+### Configuration (`nuxt.config.ts`)
+Use the official `@nuxtjs/sanity` module.
+
+**Important:** Ensure the `minimal` client is NOT enabled if you want full features.
+
+```typescript
+export default defineNuxtConfig({
+ modules: ["@nuxtjs/sanity"],
+ sanity: {
+ projectId: process.env.NUXT_SANITY_PROJECT_ID,
+ dataset: process.env.NUXT_SANITY_DATASET,
+ apiVersion: "2026-02-01",
+ // Live Visual Editing Configuration
+ visualEditing: {
+ studioUrl: process.env.NUXT_SANITY_STUDIO_URL,
+ token: process.env.NUXT_SANITY_API_READ_TOKEN, // Required for fetching drafts
+ stega: true, // Enable stega for visual editing
+ mode: 'live-visual-editing', // Default: enables live updates
+ },
+ },
+});
+```
+
+## 2. Data Fetching
+
+### `useSanityQuery`
+Use the composable provided by the module for reactive fetching. It automatically handles preview state when configured.
+
+```vue
+
+
+
+
+
+```
+
+## 3. Visual Editing (Live Preview)
+
+### Automatic Setup
+When `visualEditing` is configured in `nuxt.config.ts`, the module handles:
+1. Injecting the Visual Editing overlays.
+2. Refreshing data when content changes in the Studio.
+3. Enabling Stega encoding.
+
+### Handling Stega in Logic
+Just like Next.js, if you use stega-encoded strings in logic (e.g. `v-if="post.layout === 'full'"`), you must clean them.
+
+```typescript
+import { stegaClean } from "@sanity/client/stega";
+
+const layout = computed(() => stegaClean(props.layout));
+```
+
+## 4. Components
+
+### Portable Text
+Use the `` component (if installed via `@portabletext/vue` or provided by the module).
+
+```vue
+
+```
+
+### Images
+Use `@sanity/image-url` helper or a dedicated image component.
+
+```typescript
+import imageUrlBuilder from '@sanity/image-url'
+const builder = imageUrlBuilder(useSanity().client)
+// ... url generation logic
+```
diff --git a/.agents/skills/sanity-best-practices/references/page-builder.md b/.agents/skills/sanity-best-practices/references/page-builder.md
new file mode 100644
index 000000000..c9d64525f
--- /dev/null
+++ b/.agents/skills/sanity-best-practices/references/page-builder.md
@@ -0,0 +1,307 @@
+---
+title: "Sanity Page Builder Patterns"
+description: Patterns for Sanity Page Builder arrays, block components, and live editing.
+---
+
+# Sanity Page Builder Patterns
+
+This guide covers **Page Builder** patterns—arrays of block objects that allow content teams to compose flexible page layouts. For Portable Text (rich text within documents), see `portable-text.md`.
+
+## 1. What is a Page Builder?
+
+A page builder is an **array of objects** (`pageBuilder[]`) that allows content teams to compose pages from reusable blocks without developer intervention.
+
+**When to use:**
+- Flexible layouts needed (marketing pages, landing pages)
+- Content can be reordered
+- Different components on different pages
+
+**When NOT to use:**
+- Rigid, formulaic content (blog posts, product pages)
+- Highly structured data that doesn't change layout
+- Rich text within a document body—use Portable Text instead
+
+## 2. Schema Organization
+
+### Directory Structure
+```
+schemaTypes/
+├── blocks/ # Page builder blocks (objects)
+│ ├── heroType.ts
+│ ├── featuresType.ts
+│ └── faqsType.ts
+├── pageBuilderType.ts # The array definition
+└── pageType.ts # Document using the page builder
+```
+
+### Objects vs References
+
+| Use **Objects** | Use **References** |
+|-----------------|-------------------|
+| Content is unique to this page | Content reused across many pages |
+| Simpler queries | Needs central management |
+| Default choice | FAQs, CTAs, testimonials |
+
+**Rule:** Use references sparingly. Most blocks should be objects.
+
+### Page Builder Array
+```typescript
+// pageBuilderType.ts
+import { defineType, defineArrayMember } from "sanity";
+
+export const pageBuilderType = defineType({
+ name: "pageBuilder",
+ type: "array",
+ of: [
+ defineArrayMember({ type: "hero" }),
+ defineArrayMember({ type: "splitImage" }),
+ defineArrayMember({ type: "features" }),
+ defineArrayMember({ type: "faqs" }),
+ ],
+ options: {
+ insertMenu: {
+ views: [
+ // Optional: Show visual thumbnails in the insert menu grid
+ { name: "grid", previewImageUrl: (type) => `/block-previews/${type}.png` },
+ ],
+ },
+ },
+});
+```
+
+### Block Preview Pattern
+Every block should have consistent previews:
+
+```typescript
+import { defineType } from "sanity";
+import { BlockContentIcon } from "@sanity/icons";
+
+export const splitImageType = defineType({
+ name: "splitImage",
+ type: "object",
+ icon: BlockContentIcon,
+ fields: [/* ... */],
+ preview: {
+ select: { title: "title", media: "image" },
+ prepare({ title, media }) {
+ return {
+ title: title || "Untitled",
+ subtitle: "Split Image", // Block type name
+ media: media ?? BlockContentIcon, // Fallback to icon
+ };
+ },
+ },
+});
+```
+
+## 3. Querying Page Builders
+
+Expand references only for blocks that need them:
+
+```groq
+*[_type == "page" && slug.current == $slug][0]{
+ ...,
+ content[]{
+ ...,
+ _type == "faqs" => {
+ ...,
+ faqs[]-> // Expand only FAQ references
+ }
+ }
+}
+```
+
+## 4. Rendering Page Builders
+
+### TypeScript Typing
+Use `Extract` to type individual blocks from the query result:
+
+```typescript
+import { PAGE_QUERYResult } from "@/sanity/types";
+
+type HeroProps = Extract<
+ NonNullable["content"]>[number],
+ { _type: "hero" }
+>;
+
+export function Hero({ title, image }: HeroProps) {
+ // Fully typed!
+}
+```
+
+### Switch-Based Rendering
+```typescript
+export function PageBuilder({ content }: { content: Block[] }) {
+ if (!Array.isArray(content)) return null;
+
+ return (
+
+ {content.map((block) => {
+ switch (block._type) {
+ case "hero":
+ return ;
+ case "features":
+ return ;
+ case "splitImage":
+ return ;
+ default:
+ return Unknown: {block._type}
;
+ }
+ })}
+
+ );
+}
+```
+
+**Always use `_key` for React keys:**
+```typescript
+// Breaks Visual Editing and causes hydration issues
+{items.map((item, i) => )}
+
+// Always use Sanity's _key
+{items.map((item) => )}
+```
+
+### Cleaning Values for Logic
+Use `stegaClean` when block fields control rendering logic:
+
+```typescript
+import { stegaClean } from "next-sanity";
+
+function SplitImage({ orientation, title, image }) {
+ return (
+
+ );
+}
+```
+
+## 5. Presentation Queries for Live Editing (Next.js)
+
+For faster live updates in the Presentation Tool, use **presentation queries** that fetch only the specific block being edited, rather than re-fetching the entire page.
+
+> **Note:** This pattern uses `usePresentationQuery` from `next-sanity/hooks`. For other frameworks, check your loader package for equivalent functionality.
+
+### The Pattern
+
+1. **Create a block-specific presentation query:**
+
+```typescript
+// queries.ts
+export const HERO_PRESENTATION_QUERY = defineQuery(`
+ *[_id == $documentId][0]{
+ _id,
+ _type,
+ "heroBlock": pageBuilder[_key == $blockKey && _type == "hero"][0]{
+ title,
+ subtitle,
+ image,
+ // ... all fields the component needs
+ }
+ }
+`)
+```
+
+2. **Use `usePresentationQuery` in your component:**
+
+```typescript
+'use client'
+import { usePresentationQuery } from 'next-sanity/hooks'
+import { HERO_PRESENTATION_QUERY } from '@/sanity/lib/queries'
+
+type HeroProps = {
+ _key: string
+ documentId: string
+ // ... initial props from page query
+}
+
+export function Hero({ _key, documentId, ...initialProps }: HeroProps) {
+ // Fetch block-specific data for faster updates
+ const { data } = usePresentationQuery({
+ query: HERO_PRESENTATION_QUERY,
+ params: { documentId, blockKey: _key },
+ })
+
+ // Use presentation data if available, fallback to initial props
+ const blockData = data?.heroBlock || initialProps
+
+ return (
+
+ {blockData.title}
+ {/* ... */}
+
+ )
+}
+```
+
+### Why This Is Faster
+
+- **Without:** Editing a field triggers a full page re-render with all blocks
+- **With:** Only the specific block re-renders with its targeted query
+
+This pattern is especially valuable for pages with many blocks or complex nested data.
+
+**Note:** See `nextjs.md` for more details on `usePresentationQuery` and `visual-editing.md` for the conceptual overview.
+
+## 6. Page Builder Pitfalls
+
+| Pitfall | Solution |
+|---------|----------|
+| Too many block variations | Split into separate blocks if >2 variants |
+| Paradox of choice | Limit blocks per document type |
+| Overusing references | Default to objects; references only for truly shared content |
+| Unused blocks accumulate | Prune regularly; see deprecation patterns |
+| Inconsistent previews | Always set title, subtitle (block name), and media/icon |
+
+## 7. Component Alignment Pattern
+Map Sanity "alignment" fields (usually string/select) to CSS classes using utility functions.
+
+**Schema:**
+```typescript
+defineField({
+ name: 'align',
+ type: 'string',
+ options: { list: ['left', 'center', 'right'], layout: 'radio' }
+})
+```
+
+**Implementation (Utility):**
+```typescript
+import { stegaClean } from "@sanity/client/stega";
+
+export function getTextAlign(align?: string) {
+ // CLEAN the value before switching!
+ switch (stegaClean(align)) {
+ case 'left': return 'text-left';
+ case 'right': return 'text-right';
+ default: return 'text-center';
+ }
+}
+```
+
+## 8. Semantic Heading Levels
+**Rule:** Do NOT store heading levels (h1, h2) in Sanity schema options. Determine them dynamically in the frontend to ensure accessibility.
+
+**Bad Schema:**
+```typescript
+// Don't do this
+{ name: 'level', type: 'string', options: { list: ['h1', 'h2'] } }
+```
+
+**Good Component:**
+Pass a `semanticLevel` prop based on the component's context/nesting.
+
+```typescript
+type Props = {
+ block: HeroBlock;
+ level?: 'h1' | 'h2' | 'h3'; // Default to h2 if undefined
+}
+
+export default function Section({ block, level = 'h2' }: Props) {
+ const Tag = level;
+ return {block.title};
+}
+```
+
+*Note: For Image patterns, see `image.md`. For Portable Text patterns, see `portable-text.md`.*
diff --git a/.agents/skills/sanity-best-practices/references/portable-text.md b/.agents/skills/sanity-best-practices/references/portable-text.md
new file mode 100644
index 000000000..843393837
--- /dev/null
+++ b/.agents/skills/sanity-best-practices/references/portable-text.md
@@ -0,0 +1,365 @@
+---
+title: "Sanity Portable Text Rules"
+description: Portable Text (Rich Text) rendering and custom component creation for React/Next.js.
+---
+
+# Sanity Portable Text Rules
+
+Portable Text is Sanity's rich text format, used for content like article bodies (`body[]`). This guide covers rendering and creating custom PTE components.
+
+**Note:** For page-level layout blocks (`pageBuilder[]`), see `page-builder.md`.
+
+## 1. The Component
+Use the `PortableText` component from `next-sanity` (or `@portabletext/react`).
+
+```typescript
+import { PortableText } from "next-sanity";
+// or import { PortableText } from "@portabletext/react";
+
+export function Content({ value }: { value: any }) {
+ return ;
+}
+```
+
+## 2. Custom Components (`components` prop)
+**Always** define a typed components object to handle custom blocks, marks, and list styles.
+
+```typescript
+import { PortableTextComponents } from "next-sanity";
+
+const components: PortableTextComponents = {
+ // 1. Block styles (paragraphs, headings)
+ block: {
+ h1: ({ children }) => {children}
,
+ h2: ({ children }) => {children}
,
+ blockquote: ({ children }) => {children}
,
+ },
+
+ // 2. Custom types (non-text blocks like images, videos)
+ types: {
+ image: ({ value }) => ,
+ callToAction: ({ value }) => ,
+ },
+
+ // 3. Marks (inline decorators and annotations)
+ marks: {
+ strong: ({ children }) => {children},
+ em: ({ children }) => {children},
+ link: ({ children, value }) => {
+ const rel = !value.href.startsWith("/") ? "noreferrer noopener" : undefined;
+ return {children};
+ },
+ },
+
+ // 4. Lists
+ list: {
+ bullet: ({ children }) => ,
+ number: ({ children }) => {children}
,
+ },
+};
+```
+
+## 3. Component Categories
+
+Portable Text has three types of custom components, each with different patterns:
+
+| Type | Examples | Pattern |
+|------|----------|---------|
+| **Block styles** | h1, h2, blockquote, normal | Text blocks with `children` prop |
+| **Custom types** | image, video, callToAction | Non-text blocks with `value` prop |
+| **Marks** | link, strong, productRef | Inline annotations wrapping text |
+
+## 4. Creating Block Style Components
+
+Block styles are text blocks like headings and paragraphs. For simple styling, inline components work fine:
+
+```typescript
+block: {
+ h2: ({ children }) => {children}
,
+ normal: ({ children }) => {children}
,
+}
+```
+
+### With Visual Editing Support
+
+For live editing in the Presentation Tool, block style components may need **both** a client and server version:
+
+```typescript
+// Heading2.tsx (Server - simple SSR for production)
+export function Heading2({ children }: { children: React.ReactNode }) {
+ return {children}
;
+}
+
+// Heading2Client.tsx (Client - for visual editing context)
+'use client'
+export function Heading2Client({ children, value }: { children: React.ReactNode; value: any }) {
+ // Can access block data via `value` for advanced patterns
+ return {children}
;
+}
+```
+
+Use `useIsPresentationTool` to conditionally render the client version:
+
+```typescript
+import { useIsPresentationTool } from 'next-sanity/hooks'
+
+function Heading2Wrapper(props) {
+ const isPresentationTool = useIsPresentationTool()
+
+ if (isPresentationTool) {
+ return
+ }
+ return
+}
+```
+
+## 5. Creating Custom Type Components
+
+Custom types are non-text blocks like images, videos, or CTAs embedded in rich text.
+
+### Schema Definition
+
+```typescript
+// schemaTypes/blocks/pteImageBlock.ts
+import { defineType, defineField } from 'sanity'
+
+export const pteImageBlock = defineType({
+ name: 'pteImage',
+ title: 'Image',
+ type: 'object',
+ fields: [
+ defineField({ name: 'image', type: 'image', options: { hotspot: true } }),
+ defineField({ name: 'caption', type: 'string' }),
+ defineField({ name: 'alt', type: 'string', validation: (r) => r.required() }),
+ ],
+ preview: {
+ select: { title: 'caption', media: 'image' },
+ },
+})
+```
+
+### Register in Body Schema
+
+```typescript
+defineField({
+ name: 'body',
+ type: 'array',
+ of: [
+ { type: 'block' }, // Standard text
+ { type: 'pteImage' }, // Custom image block
+ { type: 'pteVideo' }, // Custom video block
+ ],
+})
+```
+
+### Frontend Component
+
+```typescript
+// PteImageComponent.tsx
+'use client'
+
+type PteImageProps = {
+ value: {
+ _key: string
+ image: any
+ caption?: string
+ alt: string
+ }
+}
+
+export function PteImageComponent({ value }: PteImageProps) {
+ if (!value.image) return null
+
+ return (
+
+
+ {value.caption && (
+ {value.caption}
+ )}
+
+ )
+}
+
+// Register in components
+const components: PortableTextComponents = {
+ types: {
+ pteImage: PteImageComponent,
+ },
+}
+```
+
+## 6. Creating Mark Components
+
+Marks are inline annotations that wrap text—links, highlights, or custom references.
+
+### Schema Definition (Annotation)
+
+```typescript
+// In your block configuration
+defineField({
+ name: 'body',
+ type: 'array',
+ of: [
+ {
+ type: 'block',
+ marks: {
+ decorators: [
+ { title: 'Strong', value: 'strong' },
+ { title: 'Emphasis', value: 'em' },
+ { title: 'Highlight', value: 'highlight' },
+ ],
+ annotations: [
+ {
+ name: 'link',
+ type: 'object',
+ title: 'Link',
+ fields: [
+ { name: 'href', type: 'url', title: 'URL' },
+ { name: 'openInNewTab', type: 'boolean', title: 'Open in new tab' },
+ ],
+ },
+ {
+ name: 'productRef',
+ type: 'object',
+ title: 'Product Reference',
+ fields: [
+ { name: 'product', type: 'reference', to: [{ type: 'product' }] },
+ ],
+ },
+ ],
+ },
+ },
+ ],
+})
+```
+
+### Frontend Component
+
+```typescript
+// LinkMark.tsx
+type LinkMarkProps = {
+ children: React.ReactNode
+ value: {
+ href: string
+ openInNewTab?: boolean
+ }
+}
+
+export function LinkMark({ children, value }: LinkMarkProps) {
+ const { href, openInNewTab } = value
+ const target = openInNewTab ? '_blank' : undefined
+ const rel = openInNewTab ? 'noopener noreferrer' : undefined
+
+ return (
+
+ {children}
+
+ )
+}
+
+// Register in components
+const components: PortableTextComponents = {
+ marks: {
+ link: LinkMark,
+ highlight: ({ children }) => {children},
+ },
+}
+```
+
+## 7. Presentation Queries for PTE Blocks
+
+For faster live editing of custom PTE blocks, use presentation queries that fetch only the specific block:
+
+```typescript
+// queries.ts
+export const PTE_IMAGE_PRESENTATION_QUERY = defineQuery(`
+ *[_id == $documentId][0]{
+ _id,
+ _type,
+ "pteImageBlock": body[_key == $blockKey && _type == "pteImage"][0]{
+ _key,
+ image,
+ caption,
+ alt
+ }
+ }
+`)
+```
+
+Then in your component:
+
+```typescript
+'use client'
+import { usePresentationQuery } from 'next-sanity/hooks'
+
+export function PteImageComponent({ value, documentId }: { value: any; documentId?: string }) {
+ const { data } = usePresentationQuery({
+ query: PTE_IMAGE_PRESENTATION_QUERY,
+ params: { documentId, blockKey: value._key },
+ })
+
+ const blockData = data?.pteImageBlock || value
+
+ // ... render with blockData
+}
+```
+
+**Note:** You'll need to pass `documentId` through to your PTE components. See `visual-editing.md` for context patterns.
+
+## 8. GROQ Fragment for PTE
+
+When querying documents with Portable Text, expand custom blocks:
+
+```groq
+*[_type == "article" && slug.current == $slug][0]{
+ ...,
+ body[]{
+ ...,
+ _type == "pteImage" => {
+ ...,
+ "imageUrl": image.asset->url
+ },
+ _type == "pteVideo" => {
+ ...,
+ video->{ title, url }
+ }
+ }
+}
+```
+
+## 9. Stega and Visual Editing
+
+When Visual Editing is enabled, text content contains invisible stega characters for click-to-edit functionality.
+
+**For text rendering:** Let stega characters pass through—they enable overlays:
+```typescript
+// Good - stega preserved for click-to-edit
+{children}
+```
+
+**For logic/comparisons:** Clean the values first:
+```typescript
+import { stegaClean } from '@sanity/client/stega'
+
+// Clean before using in logic
+const cleanedStyle = stegaClean(block.style)
+if (cleanedStyle === 'h2') { ... }
+```
+
+## 10. Type Safety
+When using TypeGen, the Portable Text value usually has a complex generated type. You can often use `any` or `PortableTextBlock[]` for the *prop*, but cast specific blocks if needed.
+
+```typescript
+import { PortableTextBlock } from "next-sanity";
+
+type Props = {
+ value: PortableTextBlock[];
+};
+```
+
+## 11. Best Practices
+
+- **Tailwind Typography:** For simple blogs, wrap `` in a `` (from `@tailwindcss/typography`) instead of manually styling every block.
+- **Handling Nulls:** Always check if `value` exists and is an array before rendering.
+- **Keys:** The `PortableText` component handles React keys automatically using the `_key` from Sanity. Do not add keys manually.
+- **Separate from Page Builder:** PTE blocks live in `body[]` (rich text fields), not `pageBuilder[]` (page layout). Keep these patterns separate.
diff --git a/.agents/skills/sanity-best-practices/references/project-structure.md b/.agents/skills/sanity-best-practices/references/project-structure.md
new file mode 100644
index 000000000..03a6e5bcf
--- /dev/null
+++ b/.agents/skills/sanity-best-practices/references/project-structure.md
@@ -0,0 +1,116 @@
+---
+title: Sanity Project Structure
+description: Project structure patterns for Sanity projects including monorepo and embedded Studio setups.
+---
+
+# Sanity Project Structure
+
+## Standalone Studio
+
+Best for content-only projects, API-first architectures, or when frontend is managed separately.
+
+```
+your-project/
+├── schemaTypes/
+│ ├── index.ts
+│ ├── documents/
+│ ├── objects/
+│ └── blocks/
+├── sanity.config.ts
+├── sanity.cli.ts
+└── package.json
+```
+
+**Use cases:**
+- Content modeling with MCP/AI tools (no frontend needed)
+- Headless CMS with external consumers
+- Prototyping and content design
+
+## Embedded Studio (Recommended for Next.js)
+
+Best for most Next.js projects. Unified deployment, simpler setup.
+
+```
+your-project/
+├── src/
+│ ├── app/ # Next.js App Router
+│ │ └── studio/[[...tool]]/ # Embedded Studio route
+│ └── sanity/
+│ ├── lib/
+│ │ ├── client.ts
+│ │ ├── live.ts # defineLive setup
+│ │ └── queries.ts
+│ └── schemaTypes/
+│ ├── index.ts
+│ ├── documents/
+│ ├── objects/
+│ └── blocks/
+├── sanity.config.ts
+├── sanity.cli.ts # CLI + TypeGen configuration
+└── sanity.types.ts # Generated types (from TypeGen)
+```
+
+## Monorepo
+
+Best when you need separation of concerns, multiple frontends, or strict dependency isolation.
+
+```
+your-project/
+├── apps/
+│ ├── studio/ # Sanity Studio (standalone)
+│ │ ├── src/
+│ │ │ └── schemaTypes/
+│ │ │ ├── index.ts
+│ │ │ ├── documents/
+│ │ │ ├── objects/
+│ │ │ └── blocks/
+│ │ ├── sanity.config.ts
+│ │ ├── sanity.cli.ts
+│ │ └── package.json
+│ └── web/ # Next.js (or other framework)
+│ ├── src/
+│ │ ├── app/
+│ │ └── sanity/
+│ │ ├── client.ts
+│ │ ├── live.ts
+│ │ └── queries.ts
+│ └── package.json
+├── pnpm-workspace.yaml
+└── package.json
+```
+
+**Setup:**
+1. Add web app URL to CORS origins in [Sanity Manage](https://www.sanity.io/manage)
+2. Configure `typegen` in `sanity.cli.ts` to read schema from `apps/studio` and output types to `apps/web`
+
+## File Naming Conventions
+
+- **kebab-case** for all files: `user-profile.ts`, `hero-block.ts`
+- `.ts` for schemas/utilities, `.tsx` for React components
+- Each schema exports a named const matching filename
+
+## Schema Directory Structure
+
+```
+schemaTypes/
+├── index.ts # Exports all types
+├── documents/ # Standalone content types
+│ ├── post.ts
+│ └── author.ts
+├── objects/ # Embeddable/reusable types
+│ ├── seo.ts
+│ └── link.ts
+├── blocks/ # Portable Text blocks
+│ ├── hero.ts
+│ └── callout.ts
+└── shared/ # Shared field definitions
+ └── seoFields.ts
+```
+
+## Key Files
+
+| File | Purpose |
+|------|---------|
+| `sanity.config.ts` | Studio configuration (plugins, schema, structure) |
+| `sanity.cli.ts` | CLI configuration (project ID, dataset, TypeGen config) |
+| `structure.ts` | Custom desk structure |
diff --git a/.agents/skills/sanity-best-practices/references/remix.md b/.agents/skills/sanity-best-practices/references/remix.md
new file mode 100644
index 000000000..5ee232a52
--- /dev/null
+++ b/.agents/skills/sanity-best-practices/references/remix.md
@@ -0,0 +1,134 @@
+---
+title: React Router (Remix) & Sanity Integration Rules
+description: Integration guide for React Router (formerly Remix) with Sanity, including Loaders and Visual Editing.
+---
+
+# React Router (Remix) & Sanity Integration Rules
+
+## Version Note
+
+This guide covers both:
+- **Remix v2** (`@remix-run/*` packages)
+- **React Router v7** (the successor to Remix, `react-router` package)
+
+The Sanity integration pattern is the same for both. Import paths differ slightly:
+
+| Remix v2 | React Router v7 |
+|----------|-----------------|
+| `@remix-run/node` | `react-router` |
+| `@remix-run/react` | `react-router` |
+| `remix.config.js` | `react-router.config.ts` |
+
+The examples below use Remix v2 imports. Adjust if using React Router v7.
+
+## 1. Setup & Client Pattern
+
+To support both server-side fetching and client-side live previews, use the **Split Loader Pattern**.
+
+### A. Shared Loader (`app/sanity/loader.ts`)
+Defines the store config (SSR enabled, client deferred).
+
+```typescript
+import { createQueryStore } from '@sanity/react-loader'
+
+export const {
+ loadQuery,
+ setServerClient,
+ useQuery,
+ useLiveMode,
+} = createQueryStore({ client: false, ssr: true })
+```
+
+### B. Server Loader (`app/sanity/loader.server.ts`)
+Initializes the server client.
+
+```typescript
+import { createClient } from '@sanity/client'
+import { loadQuery, setServerClient } from './loader'
+
+const client = createClient({
+ projectId: process.env.SANITY_PROJECT_ID,
+ dataset: process.env.SANITY_DATASET,
+ useCdn: true,
+ apiVersion: '2026-02-01',
+ stega: {
+ enabled: true,
+ studioUrl: 'https://my-studio-url.com',
+ },
+})
+
+setServerClient(client)
+
+export { loadQuery }
+```
+
+## 2. Data Fetching (Loaders)
+
+Use `loadQuery` from your **server** file in route loaders.
+
+```typescript
+import type { LoaderFunctionArgs } from "@remix-run/node";
+import { useLoaderData } from "@remix-run/react";
+import { loadQuery } from "~/sanity/loader.server";
+import { POSTS_QUERY } from "~/sanity/queries";
+
+export async function loader({ params }: LoaderFunctionArgs) {
+ const initial = await loadQuery(POSTS_QUERY, params);
+ return { initial, query: POSTS_QUERY, params };
+}
+
+export default function Index() {
+ const { initial, query, params } = useLoaderData
();
+ // ... pass to component
+}
+```
+
+## 3. Real-time Preview & Visual Editing
+
+### A. Use `useQuery` in Components
+Import `useQuery` from your **shared** loader file.
+
+```typescript
+import { useQuery } from "~/sanity/loader";
+
+export default function Page() {
+ const { initial, query, params } = useLoaderData();
+
+ const { data, encodeDataAttribute } = useQuery(query, params, {
+ initial
+ });
+
+ return (
+
+ {data?.title}
+
+ );
+}
+```
+
+### B. Enable Live Mode (`VisualEditing.tsx`)
+Create a component to handle the connection.
+
+```typescript
+import { enableVisualEditing } from '@sanity/visual-editing'
+import { useLiveMode } from '~/sanity/loader'
+import { client } from '~/sanity/client' // Your browser-safe client
+import { useEffect } from 'react'
+
+export default function VisualEditing() {
+ useEffect(() => enableVisualEditing(), [])
+ useLiveMode({ client })
+ return null
+}
+```
+
+Render this component in `root.tsx` only when valid (e.g., check env vars or user session).
+
+## 4. Stega Cleaning
+When using data for logic (routing, classNames), use `stegaClean`.
+
+```typescript
+import { stegaClean } from "@sanity/client/stega"
+// ...
+if (stegaClean(slug) === 'home') { ... }
+```
diff --git a/.agents/skills/sanity-best-practices/references/schema.md b/.agents/skills/sanity-best-practices/references/schema.md
new file mode 100644
index 000000000..33d650d3f
--- /dev/null
+++ b/.agents/skills/sanity-best-practices/references/schema.md
@@ -0,0 +1,378 @@
+---
+title: Sanity Schema Best Practices
+description: Rules for defining Sanity Content Models (Schemas), including field definitions, strict typing, and validation patterns.
+---
+
+# Sanity Schema Best Practices
+
+Use this contents list to jump to the schema design decision you are making.
+
+## Table of Contents
+
+- Core philosophy: data over presentation
+- Strict definition syntax
+- Shared fields pattern
+- Field patterns
+- References vs nested objects
+- Safe schema updates
+- Validation patterns
+
+## 1. Core Philosophy: Data > Presentation
+Model **what things are**, not **what they look like**.
+- ❌ **Bad:** `bigHeroText`, `redButton`, `threeColumnRow`, `color`, `fontSize`
+- ✅ **Good:** `heroStatement`, `callToAction`, `featuresSection`, `status`, `role`
+
+**The test:** "If we redesigned the site, would this field name still make sense?"
+- `threeColumnLayout` → ❌ Fails (what if we go to 2 columns?)
+- `features` → ✅ Passes (features are features regardless of layout)
+
+## 2. Strict Definition Syntax
+Always use the helper functions from `sanity` for type safety and autocompletion.
+
+- **ALWAYS** use `defineType` for the root export.
+- **ALWAYS** use `defineField` for fields.
+- **ALWAYS** use `defineArrayMember` for items inside arrays.
+
+```typescript
+import { defineType, defineField, defineArrayMember } from 'sanity'
+import { TagIcon } from '@sanity/icons'
+
+export const article = defineType({
+ name: 'article',
+ title: 'Article',
+ type: 'document',
+ icon: TagIcon,
+ fields: [
+ defineField({
+ name: 'title',
+ type: 'string',
+ validation: (rule) => rule.required(),
+ }),
+ defineField({
+ name: 'tags',
+ type: 'array',
+ of: [
+ // ALWAYS use defineArrayMember for array items
+ defineArrayMember({ type: 'reference', to: [{ type: 'tag' }] })
+ ]
+ })
+ ]
+})
+```
+
+## 3. Shared Fields Pattern
+Export arrays of fields to reuse common patterns (e.g., SEO, standard page headers).
+
+```typescript
+// src/schemaTypes/shared/seoFields.ts
+export const seoFields = [
+ defineField({ name: 'seoTitle', type: 'string', title: 'SEO Title' }),
+ defineField({ name: 'seoDesc', type: 'text', title: 'SEO Description' })
+]
+
+// Usage
+defineType({
+ name: 'page',
+ fields: [
+ defineField({ name: 'title', type: 'string' }),
+ ...seoFields // Spread shared fields
+ ]
+})
+```
+
+## 4. Field Patterns
+
+### A. Array Keys (`_key`)
+Every item in a Sanity array automatically gets a `_key` property. This is **critical** for:
+- React reconciliation (use as `key` prop)
+- Visual Editing overlays (click-to-edit)
+- Portable Text rendering
+
+**Schema:** Sanity auto-generates `_key` for array items. You don't define it.
+
+**Frontend:** Always use `_key` as React's `key`:
+```typescript
+// ✅ Correct
+{items.map((item) => )}
+
+// ❌ Wrong - index keys break Visual Editing
+{items.map((item, i) => )}
+```
+
+**Querying:** Always include `_key` in array projections:
+```groq
+*[_type == "page"][0]{
+ pageBuilder[]{
+ _key, // Always include _key in queries
+ _type,
+ ...
+ }
+}
+```
+
+### B. Icons
+Always assign an icon from `@sanity/icons` to documents and objects. This improves the Studio UX significantly. Browse all icons at [icons.sanity.build](https://icons.sanity.build/all).
+
+| Content Type | Icon |
+|--------------|------|
+| Article, Post | `DocumentTextIcon` |
+| Author, Person | `UserIcon` |
+| Category, Tag | `TagIcon` |
+| Settings | `CogIcon` |
+| Page | `DocumentIcon` |
+| Image block | `ImageIcon` |
+| Video block | `PlayIcon` |
+| FAQ | `HelpCircleIcon` |
+| Link | `LinkIcon` |
+
+### C. Boolean vs. List
+Avoid boolean fields for binary states that might expand later.
+- **Prefer:** `options.list` with "radio" layout.
+
+```typescript
+defineField({
+ name: 'status',
+ type: 'string',
+ options: {
+ list: [
+ { title: 'Draft', value: 'draft' },
+ { title: 'Published', value: 'published' }
+ ],
+ layout: 'radio'
+ }
+})
+```
+
+### D. The "Toggle" Pattern (Conditional Fields)
+Use a radio/boolean field to toggle visibility of other fields (often grouped in fieldsets).
+
+```typescript
+defineField({
+ name: 'linkType',
+ type: 'string',
+ options: { list: ['internal', 'external'], layout: 'radio' }
+}),
+defineField({
+ name: 'internalLink',
+ type: 'reference',
+ hidden: ({ parent }) => parent?.linkType !== 'internal'
+}),
+defineField({
+ name: 'externalUrl',
+ type: 'url',
+ hidden: ({ parent }) => parent?.linkType !== 'external'
+})
+```
+
+## 5. References vs Nested Objects
+
+A **critical modeling decision**: when to use `reference` vs embedding an `object`.
+
+### Use References When:
+- Content is **reusable** across documents (authors, categories, products)
+- Content needs its **own editing interface** in Studio
+- You need to query/filter by the related content independently
+- Multiple documents should share the **same instance** (update once, reflect everywhere)
+
+```typescript
+// ✅ Author is reusable and independently editable
+defineField({
+ name: 'author',
+ type: 'reference',
+ to: [{ type: 'author' }]
+})
+```
+
+### Use Nested Objects When:
+- Content is **specific to this document** (not shared)
+- Content doesn't make sense on its own (address, SEO metadata)
+- You want **simpler editing** (all fields in one place)
+- You need the data to be **copied** not linked
+
+```typescript
+// ✅ SEO is document-specific, not shared
+defineField({
+ name: 'seo',
+ type: 'object',
+ fields: [
+ defineField({ name: 'title', type: 'string' }),
+ defineField({ name: 'description', type: 'text' })
+ ]
+})
+```
+
+### Quick Decision Matrix
+
+| Scenario | Use |
+|----------|-----|
+| Blog post author | `reference` (reusable) |
+| Product category | `reference` (shared taxonomy) |
+| Page SEO fields | `object` (page-specific) |
+| Hero section content | `object` (page-specific) |
+| Team member on About page | `reference` (might be used elsewhere) |
+| Call-to-action button | `object` (usually page-specific) |
+
+### Querying Differences
+```groq
+// Reference requires expansion
+*[_type == "post"]{ author->{ name, bio } }
+
+// Object is already inline
+*[_type == "post"]{ seo { title, description } }
+```
+
+## 6. Safe Schema Updates (The Deprecation Pattern)
+
+**NEVER** delete a field that contains production data. It will cause data loss or Studio crashes. Instead, follow the **ReadOnly -> Hidden -> Deprecated** lifecycle.
+
+### The Pattern
+1. **`deprecated`**: Adds a visual warning and reason.
+2. **`readOnly: true`**: Prevents new edits but keeps data visible.
+3. **`hidden`**: Hides it from *new* documents (where value is undefined).
+4. **`initialValue: undefined`**: Ensures new documents don't get this field.
+
+```typescript
+defineField({
+ name: 'oldTitle', // The field you want to remove
+ title: 'Article Title (Deprecated)',
+ type: 'string',
+ deprecated: {
+ reason: 'Use the new "seoTitle" field instead. This will be removed in v2.'
+ },
+ readOnly: true,
+ hidden: ({ value }) => value === undefined,
+ initialValue: undefined
+})
+```
+
+### Migration Workflow
+
+**Phase 1: Deprecate** — Apply the deprecation pattern above. Deploy.
+
+**Phase 2: Migrate** — Update frontend to use new fields (with `coalesce()` fallbacks). Create a migration:
+
+```typescript
+// migrations/rename-oldTitle-to-newTitle/index.ts
+import {defineMigration, at, setIfMissing, unset} from 'sanity/migrate'
+
+export default defineMigration({
+ title: 'Rename oldTitle to newTitle',
+ documentTypes: ['article'],
+ filter: 'defined(oldTitle) && !defined(newTitle)',
+ migrate: {
+ document(doc) {
+ if (!doc.oldTitle || doc.newTitle) return
+ return [
+ at('newTitle', setIfMissing(doc.oldTitle)),
+ at('oldTitle', unset())
+ ]
+ }
+ }
+})
+```
+
+```bash
+# Dry run first (default)
+sanity migration run rename-oldTitle-to-newTitle
+
+# Execute when ready
+sanity migration run rename-oldTitle-to-newTitle --no-dry-run
+```
+
+**Phase 3: Remove** — Once `oldTitle` is undefined for all documents, delete the field definition.
+
+## 7. Validation Patterns
+
+Beyond `rule.required()`, Sanity offers powerful validation options.
+
+### Common Patterns
+
+```typescript
+// Email validation
+defineField({
+ name: 'email',
+ type: 'string',
+ validation: (rule) => rule.email().required()
+})
+
+// URL validation (with custom message)
+defineField({
+ name: 'website',
+ type: 'url',
+ validation: (rule) => rule.uri({
+ scheme: ['http', 'https']
+ }).error('Must be a valid URL starting with http:// or https://')
+})
+
+// Length constraints
+defineField({
+ name: 'excerpt',
+ type: 'text',
+ validation: (rule) => rule.max(200).warning('Keep it under 200 characters for best SEO')
+})
+
+// Regex pattern
+defineField({
+ name: 'slug',
+ type: 'slug',
+ validation: (rule) => rule.required().custom((slug) => {
+ if (!slug?.current) return 'Required'
+ if (!/^[a-z0-9-]+$/.test(slug.current)) {
+ return 'Slug must be lowercase with hyphens only'
+ }
+ return true
+ })
+})
+```
+
+### Cross-Field Validation
+
+```typescript
+defineField({
+ name: 'endDate',
+ type: 'datetime',
+ validation: (rule) => rule.custom((endDate, context) => {
+ const startDate = context.document?.startDate
+ if (startDate && endDate && new Date(endDate) < new Date(startDate)) {
+ return 'End date must be after start date'
+ }
+ return true
+ })
+})
+```
+
+### Array Validation
+
+```typescript
+defineField({
+ name: 'tags',
+ type: 'array',
+ of: [{ type: 'string' }],
+ validation: (rule) => rule
+ .min(1).error('Add at least one tag')
+ .max(10).warning('Too many tags may hurt SEO')
+ .unique()
+})
+```
+
+### Async Validation (Uniqueness Check)
+
+```typescript
+defineField({
+ name: 'slug',
+ type: 'slug',
+ validation: (rule) => rule.required().custom(async (slug, context) => {
+ if (!slug?.current) return true
+
+ const client = context.getClient({ apiVersion: '2026-02-01' })
+ const id = context.document?._id?.replace(/^drafts\./, '')
+
+ const existing = await client.fetch(
+ `count(*[_type == "post" && slug.current == $slug && _id != $id])`,
+ { slug: slug.current, id }
+ )
+
+ return existing === 0 || 'Slug already exists'
+ })
+})
+```
diff --git a/.agents/skills/sanity-best-practices/references/seo.md b/.agents/skills/sanity-best-practices/references/seo.md
new file mode 100644
index 000000000..503127530
--- /dev/null
+++ b/.agents/skills/sanity-best-practices/references/seo.md
@@ -0,0 +1,331 @@
+---
+title: Sanity SEO Best Practices
+description: SEO best practices for Sanity with Next.js, including metadata, Open Graph, sitemaps, redirects, and JSON-LD structured data.
+---
+
+# Sanity SEO Best Practices
+
+## 1. Core Philosophy
+
+SEO doesn't require complex configurations. A few core principles, applied consistently:
+
+- **Smart defaults with optional overrides** — Don't require SEO fields; use existing content as fallback
+- **Use GROQ for fallback logic** — Move conditional logic into queries, not components
+- **Leverage Next.js APIs** — Use `generateMetadata`, `sitemap.ts`, not manual `` tags
+- **Structured content = structured data** — Your content model is already SEO-ready
+
+## 2. SEO Schema Type (Reusable)
+
+Create a reusable SEO object type for consistent metadata across document types.
+
+```typescript
+// schemaTypes/seoType.ts
+import { defineField, defineType } from "sanity";
+
+export const seoType = defineType({
+ name: "seo",
+ title: "SEO",
+ type: "object",
+ fields: [
+ defineField({
+ name: "title",
+ description: "Overrides the page title if provided",
+ type: "string",
+ }),
+ defineField({
+ name: "description",
+ type: "text",
+ rows: 3,
+ }),
+ defineField({
+ name: "image",
+ description: "Image for social sharing (1200x630 recommended)",
+ type: "image",
+ options: { hotspot: true },
+ }),
+ defineField({
+ name: "noIndex",
+ description: "Hide this page from search engines",
+ type: "boolean",
+ initialValue: false,
+ }),
+ ],
+});
+```
+
+**Usage in document types:**
+```typescript
+defineField({
+ name: "seo",
+ type: "seo",
+})
+```
+
+## 3. GROQ Queries with Fallbacks
+
+Use `coalesce()` to provide fallback values. This keeps frontend logic clean.
+
+```groq
+*[_type == "page" && slug.current == $slug][0]{
+ ...,
+ "seo": {
+ // Use SEO field if provided, otherwise fall back to main title
+ "title": coalesce(seo.title, title, ""),
+ "description": coalesce(seo.description, ""),
+ "image": seo.image,
+ "noIndex": seo.noIndex == true
+ }
+}
+```
+
+**Key principle:** `seo.title` will never be `null` — it contains either the SEO override, the page title, or empty string.
+
+## 4. Next.js Metadata (The Right Way)
+
+Use `generateMetadata` — never render `` or `` tags directly in components.
+
+```typescript
+// app/(frontend)/[slug]/page.tsx
+import type { Metadata } from "next";
+import { urlFor } from "@/sanity/lib/image";
+
+type RouteProps = {
+ params: Promise<{ slug: string }>;
+};
+
+// Extract fetch to reuse in both functions
+const getPage = async (params: RouteProps["params"]) =>
+ sanityFetch({
+ query: PAGE_QUERY,
+ params: await params,
+ stega: false, // Critical for SEO!
+ });
+
+export async function generateMetadata({ params }: RouteProps): Promise {
+ const { data: page } = await getPage(params);
+
+ if (!page) return {};
+
+ const metadata: Metadata = {
+ title: page.seo.title,
+ description: page.seo.description,
+ };
+
+ // Open Graph image
+ if (page.seo.image) {
+ metadata.openGraph = {
+ images: {
+ url: urlFor(page.seo.image).width(1200).height(630).url(),
+ width: 1200,
+ height: 630,
+ },
+ };
+ }
+
+ // noIndex robots directive
+ if (page.seo.noIndex) {
+ metadata.robots = "noindex";
+ }
+
+ return metadata;
+}
+
+export default async function Page({ params }: RouteProps) {
+ const { data: page } = await getPage(params);
+ // ... render page
+}
+```
+
+**Critical:** Always set `stega: false` when fetching for metadata. Stega characters in `` destroy SEO.
+
+## 5. Dynamic Sitemap
+
+Use Next.js `sitemap.ts` convention to auto-generate from Sanity content.
+
+### GROQ Query
+```groq
+*[_type in ["page", "post"] && defined(slug.current) && seo.noIndex != true] {
+ "href": select(
+ _type == "page" => "/" + slug.current,
+ _type == "post" => "/posts/" + slug.current,
+ slug.current
+ ),
+ _updatedAt
+}
+```
+
+### Route Implementation
+```typescript
+// app/sitemap.ts
+import { MetadataRoute } from "next";
+import { client } from "@/sanity/lib/client";
+import { SITEMAP_QUERY } from "@/sanity/lib/queries";
+
+export default async function sitemap(): Promise {
+ const baseUrl = process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}`
+ : "http://localhost:3000";
+
+ try {
+ const paths = await client.fetch(SITEMAP_QUERY);
+ if (!paths) return [];
+
+ return paths.map((path) => ({
+ url: new URL(path.href!, baseUrl).toString(),
+ lastModified: new Date(path._updatedAt),
+ changeFrequency: "weekly",
+ priority: 1,
+ }));
+ } catch (error) {
+ console.error("Sitemap generation failed:", error);
+ return [];
+ }
+}
+```
+
+**Note:** Sitemap limit is 50,000 URLs per file. For larger sites, use sitemap index.
+
+## 6. Redirects (Managed in Sanity)
+
+Create a redirect document type for content team management.
+
+### Schema
+```typescript
+// schemaTypes/redirectType.ts
+import { defineField, defineType, SanityDocumentLike } from "sanity";
+import { LinkIcon } from "@sanity/icons";
+
+function isValidPath(value: string | undefined) {
+ if (!value) return "Required";
+ if (!value.startsWith("/")) return "Must start with /";
+ if (/[^a-zA-Z0-9\-_/:]/.test(value)) return "Invalid characters";
+ return true;
+}
+
+export const redirectType = defineType({
+ name: "redirect",
+ title: "Redirect",
+ type: "document",
+ icon: LinkIcon,
+ validation: (Rule) =>
+ Rule.custom((doc: SanityDocumentLike | undefined) => {
+ if (doc?.source === doc?.destination) {
+ return "Source and destination cannot be the same";
+ }
+ return true;
+ }),
+ fields: [
+ defineField({
+ name: "source",
+ type: "string",
+ validation: (Rule) => Rule.required().custom(isValidPath),
+ }),
+ defineField({
+ name: "destination",
+ type: "string",
+ validation: (Rule) => Rule.required(),
+ }),
+ defineField({
+ name: "permanent",
+ description: "301 (permanent) or 302 (temporary)",
+ type: "boolean",
+ initialValue: true,
+ }),
+ defineField({
+ name: "isEnabled",
+ type: "boolean",
+ initialValue: true,
+ }),
+ ],
+});
+```
+
+### Next.js Config
+```typescript
+// next.config.ts
+import { fetchRedirects } from "@/sanity/lib/fetchRedirects";
+
+const nextConfig: NextConfig = {
+ async redirects() {
+ return await fetchRedirects();
+ },
+};
+```
+
+**Limits:** Vercel allows max 1,024 redirects in `next.config`. For more, use middleware.
+
+## 7. Dynamic Open Graph Images
+
+Generate OG images on-the-fly using Next.js Edge Runtime at `/api/og`.
+
+```typescript
+// app/api/og/route.tsx
+import { ImageResponse } from "next/og";
+export const runtime = "edge";
+
+export async function GET(request: Request) {
+ const id = new URL(request.url).searchParams.get("id");
+ if (!id) return new Response("Missing id", { status: 400 });
+
+ const data = await client.fetch(`*[_id == $id][0]{ title }`, { id });
+
+ return new ImageResponse(
+
+
{data?.title || "Untitled"}
+ ,
+ { width: 1200, height: 630 }
+ );
+}
+```
+
+Use as fallback in metadata: `url: page.seo.image ? urlFor(page.seo.image).url() : \`/api/og?id=\${page._id}\``
+
+## 8. JSON-LD Structured Data
+
+Use `schema-dts` for type-safe structured data.
+
+```bash
+npm install schema-dts
+```
+
+### FAQ Example
+```typescript
+import { FAQPage, WithContext } from "schema-dts";
+
+const generateFaqData = (faqs: FAQ[]): WithContext => ({
+ "@context": "https://schema.org",
+ "@type": "FAQPage",
+ mainEntity: faqs.map((faq) => ({
+ "@type": "Question",
+ name: faq.title,
+ acceptedAnswer: {
+ "@type": "Answer",
+ text: faq.text, // Use pt::text() in GROQ to get plain text
+ },
+ })),
+});
+
+// In component
+
+```
+
+### GROQ for Plain Text
+```groq
+faqs[]->{
+ _id,
+ title,
+ body,
+ "text": pt::text(body) // Convert Portable Text to plain string
+}
+```
+
+## 9. Testing Tools
+
+- **Open Graph:** [opengraph.ing](https://opengraph.ing/)
+- **Facebook:** [Sharing Debugger](https://developers.facebook.com/tools/debug/)
+- **Twitter:** [Card Validator](https://cards-dev.twitter.com/validator)
+- **LinkedIn:** [Post Inspector](https://www.linkedin.com/post-inspector/)
+- **Sitemap:** [XML Sitemaps Validator](https://www.xml-sitemaps.com/validate-xml-sitemap.html)
diff --git a/.agents/skills/sanity-best-practices/references/studio-structure.md b/.agents/skills/sanity-best-practices/references/studio-structure.md
new file mode 100644
index 000000000..51bcd6d2b
--- /dev/null
+++ b/.agents/skills/sanity-best-practices/references/studio-structure.md
@@ -0,0 +1,136 @@
+---
+title: "Sanity Studio Structure Rules"
+description: Rules for customizing the Sanity Studio Structure (S.structure).
+---
+
+# Sanity Studio Structure Rules
+
+## 1. Setup
+Custom structure is defined in `sanity.config.ts` using the `structureTool`.
+
+```typescript
+import { structureTool } from 'sanity/structure'
+import { structure } from './src/structure'
+
+export default defineConfig({
+ // ...
+ plugins: [
+ structureTool({ structure })
+ ]
+})
+```
+
+## 2. Structure Definition
+**Location:** `src/structure/index.ts`
+
+Use a function that receives `S` (StructureBuilder).
+
+```typescript
+import type { StructureResolver } from 'sanity/structure'
+
+export const structure: StructureResolver = (S) =>
+ S.list()
+ .title('Content')
+ .items([
+ // ... items
+ ])
+```
+
+## 3. Organization Principles
+1. **Singletons First:** Place critical site-wide settings (Global Settings, Homepage) at the top.
+2. **Dividers:** Use `S.divider()` to visually separate logical groups.
+3. **Filtered Lists:** Always exclude Singleton documents from generic `documentTypeList` items to avoid duplication.
+
+## 4. Singleton Pattern (Critical)
+
+**Singletons are enforced via Structure, NOT schema options.** There is no `singleton: true` schema option.
+
+### How Singletons Work
+1. Use `S.document().documentId('fixed-id')` to lock the document to a specific ID.
+2. Filter the type from generic lists to prevent duplicate entries.
+
+### Singleton Helper Function
+```typescript
+// Helper to create singleton list items
+function createSingleton(S: StructureBuilder, typeName: string, title: string, icon?: ComponentType) {
+ return S.listItem()
+ .title(title)
+ .icon(icon)
+ .child(
+ S.document()
+ .schemaType(typeName)
+ .documentId(typeName) // Fixed ID = singleton
+ .title(title)
+ )
+}
+
+// Usage
+createSingleton(S, 'settings', 'Site Settings', CogIcon)
+```
+
+### Querying Singletons
+```groq
+// By fixed ID (most efficient)
+*[_id == "settings"][0]
+
+// By type (works but slower)
+*[_type == "settings"][0]
+```
+
+**For localized singletons** (e.g., homepage per language), see `localization.md` Section 6.
+
+## 5. Implementation Pattern
+
+```typescript
+// Define singleton types to exclude from generic lists
+const SINGLETONS = ['settings', 'homePage']
+
+export const structure: StructureResolver = (S) =>
+ S.list()
+ .title('Website Content')
+ .items([
+ // 1. Singletons
+ S.listItem()
+ .title('Site Settings')
+ .icon(CogIcon)
+ .child(S.document().schemaType('settings').documentId('settings')),
+
+ S.divider(),
+
+ // 2. Content Verticals
+ S.listItem()
+ .title('Blog')
+ .child(
+ S.list()
+ .title('Blog Content')
+ .items([
+ S.documentTypeListItem('post').title('Posts'),
+ S.documentTypeListItem('author').title('Authors'),
+ ])
+ ),
+
+ S.divider(),
+
+ // 3. Remaining Documents (Filtered)
+ ...S.documentTypeListItems().filter(
+ (listItem) => !SINGLETONS.includes(listItem.getId() as string)
+ )
+ ])
+```
+
+## 6. Views (Split Pane)
+Add "Web Preview" or other views to documents.
+
+```typescript
+export const defaultDocumentNode: DefaultDocumentNodeResolver = (S, { schemaType }) => {
+ switch (schemaType) {
+ case `post`:
+ return S.document().views([
+ S.view.form(), // Default form
+ S.view.component(PreviewComponent).title('Preview') // Custom view
+ ])
+ default:
+ return S.document().views([S.view.form()])
+ }
+}
+```
diff --git a/.agents/skills/sanity-best-practices/references/svelte.md b/.agents/skills/sanity-best-practices/references/svelte.md
new file mode 100644
index 000000000..aeca87d46
--- /dev/null
+++ b/.agents/skills/sanity-best-practices/references/svelte.md
@@ -0,0 +1,155 @@
+---
+title: "SvelteKit & Sanity Integration Rules"
+description: Integration guide for SvelteKit with Sanity, including @sanity/svelte-loader, Visual Editing, and Preview Mode.
+---
+
+# SvelteKit & Sanity Integration Rules
+
+## 1. Setup & Configuration
+
+### Installation
+```bash
+npm install @sanity/svelte-loader @sanity/client @sanity/visual-editing
+```
+
+### Client Configuration (`src/lib/sanity.ts`)
+Define the client with `stega` enabled for the studio URL.
+
+```typescript
+import { createClient } from '@sanity/client'
+import { PUBLIC_SANITY_PROJECT_ID, PUBLIC_SANITY_DATASET, PUBLIC_SANITY_API_VERSION, PUBLIC_SANITY_STUDIO_URL } from '$env/static/public'
+
+export const client = createClient({
+ projectId: PUBLIC_SANITY_PROJECT_ID,
+ dataset: PUBLIC_SANITY_DATASET,
+ apiVersion: PUBLIC_SANITY_API_VERSION,
+ useCdn: true,
+ stega: {
+ studioUrl: PUBLIC_SANITY_STUDIO_URL,
+ },
+})
+```
+
+### Server Client (`src/lib/server/sanity.ts`)
+Use the read token for fetching preview content.
+
+```typescript
+import { SANITY_API_READ_TOKEN } from '$env/static/private'
+import { client } from '$lib/sanity'
+
+export const serverClient = client.withConfig({
+ token: SANITY_API_READ_TOKEN,
+ stega: true, // Optional: enable stega on server too if needed
+})
+```
+
+## 2. Hooks & Request Handler (Critical)
+
+You **must** configure `createRequestHandler` in `src/hooks.server.ts` to handle preview sessions and inject `loadQuery` into locals.
+
+```typescript
+// src/hooks.server.ts
+import { createRequestHandler, setServerClient } from '@sanity/svelte-loader'
+import { serverClient } from '$lib/server/sanity'
+
+setServerClient(serverClient)
+
+export const handle = createRequestHandler()
+```
+
+**Update `app.d.ts` types:**
+```typescript
+import type { LoaderLocals } from '@sanity/svelte-loader'
+
+declare global {
+ namespace App {
+ interface Locals extends LoaderLocals {}
+ }
+}
+```
+
+## 3. Preview State Propagation
+
+Pass the preview state from the server to the client via the root layout.
+
+**Server Layout (`src/routes/+layout.server.ts`):**
+```typescript
+import type { LayoutServerLoad } from './$types'
+
+export const load: LayoutServerLoad = ({ locals: { preview } }) => {
+ return { preview }
+}
+```
+
+**Client Layout (`src/routes/+layout.ts`):**
+```typescript
+import { setPreviewing } from '@sanity/svelte-loader'
+import type { LayoutLoad } from './$types'
+
+export const load: LayoutLoad = ({ data: { preview } }) => {
+ setPreviewing(preview)
+}
+```
+
+## 4. Data Fetching (Loaders)
+
+Use `locals.loadQuery` in your page server loaders.
+
+```typescript
+// src/routes/[slug]/+page.server.ts
+import type { PageServerLoad } from './$types'
+
+export const load: PageServerLoad = async ({ locals: { loadQuery }, params }) => {
+ const initial = await loadQuery(QUERY, params)
+ return { initial }
+}
+```
+
+## 5. Real-time Preview & Visual Editing
+
+### Component Usage (`useQuery`)
+Use `useQuery` in your Svelte component to handle real-time updates.
+
+```svelte
+
+
+
+{#if !loading && post}
+
+
+ {post.title}
+
+{/if}
+```
+
+### Enable Visual Editing (`+layout.svelte`)
+Enable Visual Editing and Live Mode in your root layout.
+
+```svelte
+
+
+
+```
diff --git a/.agents/skills/sanity-best-practices/references/typegen.md b/.agents/skills/sanity-best-practices/references/typegen.md
new file mode 100644
index 000000000..8635b7545
--- /dev/null
+++ b/.agents/skills/sanity-best-practices/references/typegen.md
@@ -0,0 +1,215 @@
+---
+title: Sanity TypeGen Rules
+description: Workflow for generating TypeScript types from Sanity Schema and GROQ queries.
+---
+
+# Sanity TypeGen Rules
+
+## 1. The Workflow
+Sanity TypeGen generates TypeScript types from your schema and GROQ queries. Types can be generated automatically or manually.
+
+### Automatic (Recommended)
+Enable in `sanity.cli.ts` — types regenerate during `sanity dev` and `sanity build`:
+
+```typescript
+// sanity.cli.ts
+import { defineCliConfig } from 'sanity/cli'
+
+export default defineCliConfig({
+ typegen: {
+ enabled: true,
+ },
+})
+```
+
+### Manual
+Run the extract + generate cycle whenever schema or queries change:
+
+1. **Extract:** Converts your Schema (TS/JS) into a static JSON representation.
+2. **Generate:** Scans your codebase for GROQ queries and generates TypeScript types.
+
+```bash
+npx sanity schema extract && npx sanity typegen generate
+```
+
+### Watch Mode (for separate frontends)
+If your frontend is in a separate repo from the Studio, use watch mode:
+
+```bash
+npx sanity typegen generate --watch
+```
+
+## 2. The "Update Types" Pattern
+For manual workflows, implement a single script:
+
+**package.json:**
+```json
+"scripts": {
+ "typegen": "sanity schema extract && sanity typegen generate"
+}
+```
+
+### Git Strategy for Generated Files
+
+**Option A: Commit generated types (Recommended for most teams)**
+- Types available immediately after `git pull`
+- CI/CD doesn't need to run typegen
+- Can cause merge conflicts
+
+**Option B: Generate in CI (Recommended for larger teams)**
+Add to `.gitignore`:
+```gitignore
+# Sanity TypeGen (generated)
+sanity.types.ts
+schema.json
+```
+
+Then ensure CI runs typegen before build:
+```yaml
+# Example GitHub Actions
+- run: npm run typegen
+- run: npm run build
+```
+
+## 3. Configuration (`sanity.cli.ts`)
+
+> **Note:** `sanity-typegen.json` is deprecated. Move your configuration to `sanity.cli.ts`.
+
+```typescript
+// sanity.cli.ts
+import { defineCliConfig } from 'sanity/cli'
+
+export default defineCliConfig({
+ typegen: {
+ enabled: true, // Auto-generate during sanity dev/build
+ path: "./src/**/*.{ts,tsx,js,jsx,astro,svelte,vue}", // Glob to find queries
+ schema: "schema.json", // Schema file from extract
+ generates: "./sanity.types.ts", // Output file
+ overloadClientMethods: true, // Auto-type client.fetch() calls
+ },
+})
+```
+
+### Project Structure Examples
+
+**Single Repo / Embedded Studio (most common):**
+Use defaults — no extra config needed.
+
+**Monorepo** (Studio in `apps/studio`, Frontend in `apps/web`):
+```typescript
+export default defineCliConfig({
+ typegen: {
+ path: "../web/src/**/*.{ts,tsx,js,jsx}",
+ schema: "schema.json",
+ generates: "../web/sanity.types.ts",
+ },
+})
+```
+
+**Separate Repos:**
+Use `--watch` mode in your frontend: `sanity typegen generate --watch`
+
+## 4. Usage in Code
+
+### Automatic Type Inference (Recommended)
+With `overloadClientMethods: true` (default), `client.fetch()` automatically returns typed results when you use `defineQuery`:
+
+```typescript
+import { defineQuery } from "groq";
+import { createClient } from "@sanity/client";
+
+const client = createClient({...});
+
+const POSTS_QUERY = defineQuery(`*[_type == "post"]{ title, slug }`);
+
+// Return type is automatically inferred — no manual type import needed!
+const posts = await client.fetch(POSTS_QUERY);
+```
+
+### Manual Type Import (Alternative)
+You can also import generated types directly:
+
+```typescript
+import { defineQuery } from "groq";
+// Next.js re-exports defineQuery for convenience:
+// import { defineQuery } from "next-sanity";
+
+const AUTHOR_QUERY = defineQuery(`*[_type == "author" && slug.current == $slug][0]{ name, bio }`);
+
+import type { AUTHOR_QUERYResult } from "@/sanity.types";
+
+export default function Author({ data }: { data: AUTHOR_QUERYResult }) {
+ return {data.name}
+}
+```
+
+### Required Fields
+Use `--enforce-required-fields` during extraction to translate `validation: rule => rule.required()` into non-optional types:
+
+```bash
+npx sanity schema extract --enforce-required-fields
+npx sanity typegen generate
+```
+
+> **Warning:** If you use draft previews, fields may still be `undefined` even with required validation, since drafts can be in an invalid state.
+
+### Type Utilities
+TypeGen provides utilities for working with complex types:
+
+```typescript
+import type { Get, FilterByType } from 'sanity'
+import type { Page, PageBuilder } from './sanity.types'
+
+// Extract deeply nested type (up to 20 levels)
+type HeroSection = Get
+
+// Filter specific types from unions using _type discriminator
+type HeroBlock = FilterByType
+```
+
+### Unique Query Names
+All queries must have unique variable names. Duplicate names across files will cause TypeGen to silently overwrite types. Use descriptive, scoped names:
+
+```typescript
+// Unique names
+const POSTS_INDEX_QUERY = defineQuery(`*[_type == "post"]{ title }`)
+const POST_DETAIL_QUERY = defineQuery(`*[_type == "post" && slug.current == $slug][0]`)
+
+// Duplicate names will conflict
+const QUERY = defineQuery(`*[_type == "post"]`) // file-a.ts
+const QUERY = defineQuery(`*[_type == "author"]`) // file-b.ts — overwrites!
+```
+
+### Supported Query Formats
+Queries must be assigned to a variable using `groq` or `defineQuery`:
+
+```typescript
+// Works — groq template tag
+const query = groq`*[_type == "post"]`
+
+// Works — defineQuery
+const query = defineQuery(`*[_type == "post"]`)
+
+// Won't work — inline query
+await client.fetch(groq`*[_type == "post"]`)
+```
+
+### Supported File Types
+TypeGen parses queries from: `.ts`, `.tsx`, `.js`, `.jsx`, `.astro`, `.svelte`, `.vue`
+
+### tsconfig Requirements
+Ensure `sanity.types.ts` is included in your `tsconfig.json`'s `include` array. If your config restricts includes (e.g., `["src/**/*"]`) and the types file is at the project root, TypeScript won't pick up the generated types:
+
+```json
+{
+ "include": ["src/**/*", "sanity.types.ts"]
+}
+```
+
+### Skipping Individual Queries
+Add `@sanity-typegen-ignore` in a comment before a query to skip type generation:
+
+```typescript
+// @sanity-typegen-ignore
+const debugQuery = groq`*[_type == "debug"]`
+```
diff --git a/.agents/skills/sanity-best-practices/references/visual-editing.md b/.agents/skills/sanity-best-practices/references/visual-editing.md
new file mode 100644
index 000000000..1f7a9678e
--- /dev/null
+++ b/.agents/skills/sanity-best-practices/references/visual-editing.md
@@ -0,0 +1,263 @@
+---
+title: "Sanity Visual Editing Rules"
+description: Comprehensive guide for Sanity Visual Editing, including Presentation Tool, Stega (Content Source Maps), and Overlays.
+---
+
+# Sanity Visual Editing Rules
+
+## 1. Concepts
+
+### Presentation Tool
+The Studio plugin (`sanity/presentation`) that renders your front-end application inside an iframe in the Studio. It enables the "Edit" overlay and bidirectional navigation.
+
+### Content Source Maps (Stega)
+Invisible characters embedded in strings that tell the Presentation Tool which field in which document the content comes from.
+- **Mechanism:** Sanity encodes document ID, field path, and dataset info into string values.
+- **Result:** Click-to-edit functionality in the preview.
+
+### Loaders
+Framework-agnostic or specific libraries that handle:
+1. Fetching data (production vs. preview).
+2. Subscribing to real-time updates (Live Content API).
+3. Encoding Stega strings (if not handled by the Content Lake automatically).
+
+## 2. The Golden Rule of Stega (Clean Data)
+
+When Visual Editing is enabled, string fields will contain invisible characters. You **MUST** clean them before using the value for logic.
+
+| Scenario | Clean? | Why |
+|----------|--------|-----|
+| Comparing strings (`if (x === 'y')`) | ✅ Yes | Stega breaks equality |
+| Using as object keys | ✅ Yes | Keys won't match |
+| Using as HTML IDs | ✅ Yes | Invalid characters |
+| Passing to third-party libraries | ✅ Yes | May validate input |
+| Rendering text (`{title}
`) | ❌ No | Breaks click-to-edit |
+| Passing to `` | ❌ No | Handles internally |
+| Passing to image helpers | ❌ No | Handles internally |
+
+```typescript
+import { stegaClean } from "@sanity/client/stega";
+
+export function Layout({ align }: { align: string }) {
+ // Good: Clean before comparison
+ const cleanAlign = stegaClean(align);
+ return
+}
+```
+
+## 3. Token Handling (Security)
+
+Store your read token in a dedicated file that throws if missing:
+
+```typescript
+// src/sanity/lib/token.ts
+export const token = process.env.SANITY_API_READ_TOKEN
+
+if (!token) {
+ throw new Error('Missing SANITY_API_READ_TOKEN')
+}
+```
+
+**Never** expose tokens in client bundles. Pass to `defineLive` for server/browser use only when Draft Mode is enabled.
+
+## 4. Setup: Presentation Tool
+
+**File:** `sanity.config.ts`
+
+```typescript
+import { defineConfig } from 'sanity'
+import { presentationTool } from 'sanity/presentation'
+import { resolve } from '@/sanity/presentation/resolve'
+
+export default defineConfig({
+ // ...
+ plugins: [
+ presentationTool({
+ resolve, // Document locations (see below)
+ previewUrl: {
+ previewMode: {
+ enable: '/api/draft-mode/enable',
+ },
+ },
+ }),
+ ],
+})
+```
+
+### Document Locations
+
+Show where documents appear in the front-end — enables quick navigation between Structure and Presentation tools.
+
+```typescript
+// src/sanity/presentation/resolve.ts
+import { defineLocations, PresentationPluginOptions } from 'sanity/presentation'
+
+export const resolve: PresentationPluginOptions['resolve'] = {
+ locations: {
+ post: defineLocations({
+ select: { title: 'title', slug: 'slug.current' },
+ resolve: (doc) => ({
+ locations: [
+ { title: doc?.title || 'Untitled', href: `/posts/${doc?.slug}` },
+ { title: 'Posts index', href: `/posts` },
+ ],
+ }),
+ }),
+ // Add more document types as needed
+ },
+}
+```
+
+## 5. Visual Editing Overlays
+
+Render `` in Draft Mode for click-to-edit overlays.
+
+**Next.js (App Router):**
+```typescript
+// layout.tsx
+import { VisualEditing } from 'next-sanity/visual-editing'
+import { draftMode } from 'next/headers'
+import { DisableDraftMode } from '@/components/disable-draft-mode'
+
+export default async function RootLayout({ children }) {
+ return (
+
+
+ {children}
+ {(await draftMode()).isEnabled && (
+ <>
+
+
+ >
+ )}
+
+
+ )
+}
+```
+
+### Disable Draft Mode Button
+
+Useful for content authors to exit preview and see published content:
+
+```typescript
+// src/components/disable-draft-mode.tsx
+'use client'
+import { useDraftModeEnvironment } from 'next-sanity/hooks'
+
+export function DisableDraftMode() {
+ const environment = useDraftModeEnvironment()
+ // Only show outside of Presentation Tool
+ if (environment !== 'live' && environment !== 'unknown') return null
+
+ return (
+
+ Disable Draft Mode
+
+ )
+}
+```
+
+**Remix/Svelte:** See framework-specific rules for `useLiveMode` and `enableVisualEditing` patterns.
+
+## 6. SEO & Metadata (Critical)
+
+**NEVER** allow Stega strings in `` tags (Title, Description, Canonical URLs). It destroys SEO rankings and looks broken in search results.
+
+- **Next.js:** Set `stega: false` in `generateMetadata`.
+- **General:** Explicitly clean fields used in `` or ``.
+
+```typescript
+// Next.js Example — disable stega at fetch level
+export async function generateMetadata({ params }) {
+ const { data } = await sanityFetch({
+ query: SEO_QUERY,
+ stega: false // Critical
+ })
+ return { title: data.title }
+}
+```
+
+**Alternative:** If you can't disable stega at the fetch level, clean explicitly:
+
+```typescript
+import { stegaClean } from "@sanity/client/stega";
+
+export async function generateMetadata({ params }) {
+ const { data } = await sanityFetch({ query: PAGE_QUERY })
+ return {
+ title: stegaClean(data.title),
+ description: stegaClean(data.description),
+ openGraph: { url: stegaClean(data.canonicalUrl) }
+ }
+}
+```
+
+## 7. Drag-and-Drop Reordering (Advanced)
+
+For arrays (e.g., "Related Posts"), enable drag-and-drop in the preview using `data-sanity` attributes and `useOptimistic`:
+
+```typescript
+import { createDataAttribute } from 'next-sanity'
+import { useOptimistic } from 'next-sanity/hooks'
+
+// Add data-sanity to array container
+
+ {items.map((item) => (
+ -
+ {item.title}
+
+ ))}
+
+```
+
+**Key requirements:**
+- Query must include `_key` for array items
+- Use `useOptimistic` hook for instant UI updates during mutations
+
+## 8. Optimistic Updates for Faster Editing
+
+By default, editing a field in the Presentation Tool triggers a full page re-render. For pages with many components, this can feel sluggish. **Presentation queries** solve this by fetching only the specific block being edited.
+
+### The Concept
+
+Instead of:
+1. User edits a field -> Full page query re-runs -> All components re-render
+
+You get:
+1. User edits a field -> Block-specific query runs -> Only that component re-renders
+
+### How It Works
+
+1. **Create a targeted query** that fetches just the block data using `_key`:
+
+```groq
+*[_id == $documentId][0]{
+ "heroBlock": pageBuilder[_key == $blockKey && _type == "hero"][0]{
+ title, subtitle, image
+ }
+}
+```
+
+2. **Use a presentation query hook** in your component (e.g., `usePresentationQuery` in Next.js)
+
+3. **Fall back to initial props** when not in presentation mode
+
+This pattern works for both Page Builder blocks (`pageBuilder[]`) and Portable Text blocks (`body[]`).
+
+**See framework-specific rules for implementation:**
+- Next.js: `nextjs.md` (Section 9)
+- Page Builder: `page-builder.md` (Section 5)
+- Portable Text: `portable-text.md` (Section 7)
+
+## 9. Framework Specifics
+
+| Framework | Loader Package | Key Components |
+| :--- | :--- | :--- |
+| **Next.js** | `next-sanity` | ``, `defineLive`, `usePresentationQuery` |
+| **Remix** | `@sanity/react-loader` | `createQueryStore`, `useLiveMode`, `enableVisualEditing` |
+| **Svelte** | `@sanity/svelte-loader` | `createRequestHandler`, `useLiveMode`, `enableVisualEditing` |
+| **Nuxt** | `@nuxtjs/sanity` | Automatic via module config (`visualEditing: {}`) |
+| **Astro** | `@sanity/astro` | `sanity({ useCdn: false, stega: true })` |
diff --git a/.agents/skills/sanity-live-cache-components/SKILL.md b/.agents/skills/sanity-live-cache-components/SKILL.md
new file mode 100644
index 000000000..c6075b783
--- /dev/null
+++ b/.agents/skills/sanity-live-cache-components/SKILL.md
@@ -0,0 +1,168 @@
+---
+name: sanity-live-cache-components
+description: Integrates Sanity Live with Next.js Cache Components in next-sanity v13+ apps. Sets up sanityFetch, , Visual Editing, Presentation Tool, draft mode handling, and the three-layer (Page/Dynamic/Cached) component pattern with explicit perspective/stega prop-drilling. Use when configuring or migrating a Next.js app to cacheComponents with Sanity, when adding sanityFetch, when wiring /, or when refactoring components that hardcode perspective/stega.
+---
+
+# Sanity Live + Cache Components
+
+Wires `next-sanity` into a Next.js 16+ app with `cacheComponents: true`. Data is fetched with `sanityFetch` (which calls `cacheTag`/`cacheLife` internally), and `` in the root layout revalidates cached content over an EventSource connection to Sanity Content Lake. Visual Editing and Presentation Tool are fully supported when draft mode is enabled.
+
+Read the relevant guide in `node_modules/next/dist/docs/` (when available) before writing code. If a guide conflicts with this skill, follow this skill.
+
+This skill assumes familiarity with the `next-cache-components` skill — it covers `'use cache'`, `cacheLife`, `cacheTag`, and the cookies/headers/params rule. The only Sanity-relevant exception: `await draftMode()` is allowed inside `'use cache'` (Next.js bypasses caching when draft mode is enabled — see [the `use cache` reference](https://nextjs.org/docs/app/api-reference/directives/use-cache#draft-mode)).
+
+## Prerequisites
+
+- Next.js 16.2+ installed in the project (check `package.json` or run `pnpm list next` / `npm ls next` — don't use `pnpm view next version`, that reports the registry's latest, not what's installed).
+- `AGENTS.md` exists, or [follow the guide](https://nextjs.org/docs/app/guides/ai-agents#existing-projects).
+- These environment variables are set:
+ - `NEXT_PUBLIC_SANITY_PROJECT_ID`
+ - `NEXT_PUBLIC_SANITY_DATASET`
+ - `SANITY_API_READ_TOKEN`
+- Embedded Sanity Studio configuration (`sanity.config.ts`, `sanity.cli.ts`, anything under `sanity/`) needs no changes — this skill only touches the Next.js app surface.
+
+## Reference files
+
+| File | When to read |
+| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
+| [reference/live-helpers.md](reference/live-helpers.md) | Full `client.ts` / `live.ts`, `sanityFetch*` and `getDynamicFetchOptions` details |
+| [reference/three-layer-pattern.md](reference/three-layer-pattern.md) | The Page → Dynamic → Cached pattern for `page.tsx`, including the `searchParams` variant |
+| [reference/layouts.md](reference/layouts.md) | Non-blocking data fetching inside `layout.tsx` with a shared `'use cache'` helper |
+| [reference/dynamic-segments.md](reference/dynamic-segments.md) | High-performance `[slug]` routes: `loading.tsx` + partial `generateStaticParams`, or non-blocking dynamic `params` in a layout |
+
+---
+
+## 1. Install `next-sanity@^13`
+
+```bash
+npm install next-sanity@^13 --save-exact
+```
+
+### Migrating an existing Sanity Live setup
+
+If the app is already using `defineLive`, this skill is a refactor, not a rewrite. The 5-step sequence below still applies, but watch for these specific differences:
+
+- **Don't overwrite `client.ts` or `live.ts`** if they exist. Append missing options. Preserve any existing `token` and `stega.*` settings — see [reference/live-helpers.md](reference/live-helpers.md).
+- **Search the codebase for hardcoded `perspective: 'published'` and `stega: false`** in `sanityFetch` callsites and refactor them to source `perspective`/`stega` via `getDynamicFetchOptions` and the three-layer pattern.
+- **Search for `sanityFetch` calls inside `generateStaticParams`** → swap for `sanityFetchStaticParams`.
+- **Search for `sanityFetch` calls inside `generateMetadata` / `sitemap.ts` / `opengraph-image.tsx` / etc.** → swap for `sanityFetchMetadata`.
+- **Search for `sanityFetch` calls directly inside a `'use server'` function** → split into a separate `'use cache'` helper.
+- **Verify there is exactly one `` and one `` in the tree.** Multiple renders are undefined behavior.
+
+The "Anti-patterns to grep for" section at the bottom of this file lists the search patterns.
+
+---
+
+## 2. Configure `next.config.ts`
+
+Enable `cacheComponents` and set `cacheLife.default` to `sanity` so default revalidation is 1 year (instead of 15 minutes). `sanityFetch` is optimized for on-demand revalidation and doesn't need time-based revalidation.
+
+```ts
+// next.config.ts
+import type {NextConfig} from 'next'
+import {sanity} from 'next-sanity/live/cache-life'
+
+const nextConfig: NextConfig = {
+ cacheComponents: true,
+ cacheLife: {default: sanity},
+}
+
+export default nextConfig
+```
+
+---
+
+## 3. Configure `defineLive` and export helpers
+
+Create `src/sanity/lib/client.ts` and `src/sanity/lib/live.ts`. The minimal `defineLive` call:
+
+```ts
+// src/sanity/lib/live.ts (excerpt)
+export const {SanityLive, sanityFetch} = defineLive({
+ client,
+ serverToken: token,
+ browserToken: token,
+ strict: true,
+})
+```
+
+Full file contents (including `client.ts`, `getDynamicFetchOptions`, `sanityFetchMetadata`, `sanityFetchStaticParams`) and per-helper guidance: [reference/live-helpers.md](reference/live-helpers.md).
+
+The helpers exported from `live.ts`:
+
+| Helper | Used in |
+| ------------------------- | ---------------------------------------------------------------------------------------------- |
+| `sanityFetch` | `'use cache'` components rendered from `page.tsx` / `layout.tsx` |
+| `sanityFetchMetadata` | `generateMetadata`, `generateViewport`, `sitemap.ts`, `robots.ts`, `opengraph-image.tsx`, etc. |
+| `sanityFetchStaticParams` | `generateStaticParams` only |
+| `getDynamicFetchOptions` | Resolving `perspective`/`stega` outside any `'use cache'` boundary |
+| `SanityLive` | Rendered once in a root layout |
+
+---
+
+## 4. Render `` in a root layout
+
+`` and `` both belong in a `layout.tsx`, never a `page.tsx`. Both must be rendered at most once across the whole tree — duplicate renders are undefined behavior.
+
+- `includeDrafts` is **required** when `defineLive` is configured with `strict: true` (the recommended setup). TypeScript will surface the error if it's missing; pass `includeDrafts={isDraftMode}` so live revalidation includes drafts only in draft mode.
+- Preserve any existing optional callback props on `` when migrating: `onError`, `onWelcome`, `onReconnect`. They are commonly wired to a toast/notification helper and silently dropping them regresses UX.
+
+```tsx
+// src/app/layout.tsx
+import {SanityLive} from '@/sanity/lib/live'
+import {VisualEditing} from 'next-sanity/visual-editing'
+import {draftMode} from 'next/headers'
+
+export default async function RootLayout({children}: LayoutProps<'/'>) {
+ const {isEnabled: isDraftMode} = await draftMode()
+ return (
+
+
+ {children}
+
+ {isDraftMode && }
+
+
+ )
+}
+```
+
+### With an embedded Sanity Studio
+
+If a route mounts `NextStudio` from `next-sanity/studio` (e.g. `app/studio/[[...index]]/page.tsx`), `` must live in a layout the embedded studio doesn't share. Use [route groups](https://nextjs.org/docs/app/api-reference/file-conventions/route-groups): put `` in `src/app/(website)/layout.tsx` and keep the rest of the app under `src/app/(website)`.
+
+---
+
+## 5. Apply the three-layer pattern to pages and layouts
+
+Every route that should be statically prerendered uses the same shape:
+
+```text
+Page/Layout (Layer 1: draftMode branch)
+ ├── NOT draft mode → (no Suspense)
+ └── draft mode →
+ (Layer 2: awaits dynamic APIs)
+ └── (Layer 3: 'use cache')
+```
+
+**Critical rule**: Only Layer 3 carries `'use cache'`. The top-level `Page` / `Layout` must **not** have `'use cache'` — it awaits `params`, `searchParams`, or `cookies()` (via `getDynamicFetchOptions`), and those dynamic APIs are forbidden inside `'use cache'`. Layer 3 carrying `'use cache'` is enough for the whole route to prerender into the static shell. Adding `'use cache'` to the top-level function is the most common failure mode — TypeScript and the runtime will both complain.
+
+Pick the right reference for the file you're editing:
+
+- **`page.tsx`** with static or `generateStaticParams`-backed params → [reference/three-layer-pattern.md](reference/three-layer-pattern.md).
+- **`page.tsx`** that uses `searchParams` or other dynamic APIs → the `searchParams` variant in [reference/three-layer-pattern.md](reference/three-layer-pattern.md).
+- **`layout.tsx`** that fetches its own data → [reference/layouts.md](reference/layouts.md).
+- **Dynamic `[slug]` route** that needs the `loading.tsx` + partial `generateStaticParams` optimization, or a layout that needs non-blocking `params` → [reference/dynamic-segments.md](reference/dynamic-segments.md).
+
+---
+
+## Anti-patterns to grep for
+
+When auditing an app, search for these and refactor:
+
+- `perspective: 'published'` and `stega: false` hardcoded together in a `sanityFetch` call → use the three-layer pattern, source `perspective`/`stega` via `getDynamicFetchOptions`.
+- `sanityFetch(` directly inside a function whose body begins with `'use server'` → split into a separate `'use cache'` helper.
+- `sanityFetch(` inside `generateStaticParams` → swap for `sanityFetchStaticParams`.
+- `sanityFetch(` inside `generateMetadata` / `generateViewport` / `sitemap.ts` / `robots.ts` / `opengraph-image.tsx` etc. → swap for `sanityFetchMetadata` and resolve `perspective` via `getDynamicFetchOptions`.
+- `await draftMode()` immediately followed by `await getDynamicFetchOptions()` at the top of a `page.tsx` or `layout.tsx` without a sibling `loading.tsx` → move those dynamic-API calls into a child component wrapped in `` so the static shell can prerender.
+- More than one `` or `` rendered in the tree → consolidate to a single render in the right layout.
diff --git a/.agents/skills/sanity-live-cache-components/reference/dynamic-segments.md b/.agents/skills/sanity-live-cache-components/reference/dynamic-segments.md
new file mode 100644
index 000000000..09ab6aa02
--- /dev/null
+++ b/.agents/skills/sanity-live-cache-components/reference/dynamic-segments.md
@@ -0,0 +1,114 @@
+# High-performance dynamic segments
+
+[Dynamic routes](https://nextjs.org/docs/app/api-reference/file-conventions/dynamic-routes) should always implement `generateStaticParams`, even if only a subset of pages — see [the Cache Components note on dynamic routes](https://nextjs.org/docs/app/api-reference/file-conventions/dynamic-routes#with-cache-components). Whether to use `loading.tsx` or `` for fallback UI depends on the use case — see [the streaming guide](https://nextjs.org/docs/app/guides/streaming#when-to-use-loadingjs-vs-suspense).
+
+## Contents
+
+- [Case 1: `page.tsx` with `loading.tsx` + partial `generateStaticParams`](#case-1-pagetsx-with-loadingtsx--partial-generatestaticparams)
+- [Case 2: `layout.tsx` with non-blocking dynamic `params`](#case-2-layouttsx-with-non-blocking-dynamic-params)
+
+## Case 1: `page.tsx` with `loading.tsx` + partial `generateStaticParams`
+
+`generateStaticParams` returns only the 100 most recently updated pages. A sibling `loading.tsx` renders fallback UI, so `page.tsx` itself can skip the `` wrapper. The same fallback UI is reused in draft mode.
+
+This scales to thousands of pages without ballooning `next build` and without compromising UX in production:
+
+- Prerendered pages load instantly.
+- Pages not prerendered start rendering on `` hover (or when scrolled into view), so on click:
+ - If prerendering finished in time → serves instantly, no loading state.
+ - If not → instantly shows the cached `loading.tsx` fallback.
+
+Add a sibling `src/app/[slug]/loading.tsx` that renders the same skeleton you would otherwise pass to ``. Keep it cheap and free of layout shift:
+
+```tsx
+// src/app/[slug]/loading.tsx
+export default function Loading() {
+ return (
+
+ Loading…
+
+ )
+}
+```
+
+```tsx
+// src/app/[slug]/page.tsx
+import {
+ getDynamicFetchOptions,
+ sanityFetch,
+ sanityFetchStaticParams,
+ type DynamicFetchOptions,
+} from '@/sanity/lib/live'
+import {defineQuery} from 'next-sanity'
+
+export async function generateStaticParams() {
+ const pageSlugsQuery = defineQuery(
+ `*[_type == "page" && defined(slug.current)] | order(_updatedAt desc) [0...100]{"slug": slug.current}`,
+ )
+ const {data} = await sanityFetchStaticParams({query: pageSlugsQuery})
+ return data
+}
+
+// With sibling `loading.tsx`, skip the `` + `DynamicPage` indirection: await `params`
+// and `getDynamicFetchOptions` directly inside `Page`.
+export default async function Page({params}: PageProps<'/[slug]'>) {
+ const [{slug}, {perspective, stega}] = await Promise.all([params, getDynamicFetchOptions()])
+ return
+}
+async function CachedPage({
+ slug,
+ perspective,
+ stega,
+}: Awaited['params']> & DynamicFetchOptions) {
+ 'use cache'
+ const pageQuery = defineQuery(`*[_type == "page" && slug.current == $slug][0]`)
+ const {data} = await sanityFetch({
+ query: pageQuery,
+ params: {slug},
+ perspective,
+ stega,
+ })
+ return {/* use `data` to render stuff */}
+}
+```
+
+## Case 2: `layout.tsx` with non-blocking dynamic `params`
+
+A `layout.tsx` can't use `loading.tsx` for fallback UI — [it's one level higher in the hierarchy](https://nextjs.org/docs/app/getting-started/project-structure#component-hierarchy). To fetch data that depends on dynamic `params` without blocking `children` from streaming, pass the unawaited `params` promise into a `` boundary and await it inside.
+
+```tsx
+// src/app/(website)/[slug]/layout.tsx
+
+import {getDynamicFetchOptions, sanityFetch, type DynamicFetchOptions} from '@/sanity/lib/live'
+import {defineQuery} from 'next-sanity'
+import {Suspense} from 'react'
+
+export default function WebsiteLayout({children, params}: LayoutProps<'/[slug]'>) {
+ return (
+ <>
+ {children}
+ {/* The footer renders below the fold, no fallback needed */}
+
+
+
+ >
+ )
+}
+async function DynamicFooter({params}: Pick, 'params'>) {
+ const [{slug}, {perspective, stega}] = await Promise.all([params, getDynamicFetchOptions()])
+ return
+}
+async function Footer({
+ slug,
+ perspective,
+ stega,
+}: Awaited['params']> & DynamicFetchOptions) {
+ 'use cache'
+ const footerQuery = defineQuery(`*[_type == "footer" && slug.current == $slug][0]`)
+ const {data} = await sanityFetch({query: footerQuery, params: {slug}, perspective, stega})
+ return
+}
+```
diff --git a/.agents/skills/sanity-live-cache-components/reference/layouts.md b/.agents/skills/sanity-live-cache-components/reference/layouts.md
new file mode 100644
index 000000000..ff1549520
--- /dev/null
+++ b/.agents/skills/sanity-live-cache-components/reference/layouts.md
@@ -0,0 +1,163 @@
+# Non-blocking layout patterns
+
+When `sanityFetch` runs inside a `layout.tsx`, the goal is to keep `children` streaming and keep the static shell as large as possible.
+
+## Contents
+
+- [Rules](#rules)
+- [Pattern: shared `'use cache'` helper per draft/published branch](#pattern-shared-use-cache-helper-per-draftpublished-branch)
+- [Anti-pattern: wrapping `children` in a single cached layout](#anti-pattern-wrapping-children-in-a-single-cached-layout)
+- [Simpler example: a single `
+ ) : (
+
+ )}
+ {children}
+ {isDraftMode ? (
+
+
+
+ ) : (
+
+ )}
+ >
+ )
+}
+
+async function DynamicNavbar() {
+ const {perspective, stega} = await getDynamicFetchOptions()
+ return
+}
+async function CachedNavbar({perspective, stega}: DynamicFetchOptions) {
+ 'use cache'
+ const data = await fetchSettings({perspective, stega})
+ return
+}
+
+async function DynamicFooter() {
+ const {perspective, stega} = await getDynamicFetchOptions()
+ return
+}
+async function CachedFooter({perspective, stega}: DynamicFetchOptions) {
+ 'use cache'
+ const data = await fetchSettings({perspective, stega})
+ return
+}
+```
+
+## Anti-pattern: wrapping `children` in a single cached layout
+
+This blocks `children` on the layout's data fetch and prevents the page itself from streaming in independently.
+
+```tsx
+// src/app/(website)/layout.tsx
+export default async function WebsiteLayout({children}: LayoutProps<'/'>) {
+ const {isEnabled: isDraftMode} = await draftMode()
+ if (isDraftMode) {
+ return (
+
+ {children}
+
+ )
+ }
+ return (
+
+ {children}
+
+ )
+}
+async function CachedWebsiteLayout({
+ children,
+ perspective,
+ stega,
+}: {children: ReactNode} & DynamicFetchOptions) {
+ 'use cache'
+ const settingsQuery = defineQuery(`*[_type == "settings"][0]`)
+ const {data} = await sanityFetch({query: settingsQuery, perspective, stega})
+
+ return (
+ <>
+
+ {children}
+
+ >
+ )
+}
+```
+
+## Simpler example: a single `
+ ) : (
+
+ )}
+ >
+ )
+}
+async function DynamicFooter() {
+ const {perspective, stega} = await getDynamicFetchOptions()
+ return
+}
+async function Footer({perspective, stega}: DynamicFetchOptions) {
+ 'use cache'
+ const footerQuery = defineQuery(`*[_type == "footer"][0]`)
+ const {data} = await sanityFetch({query: footerQuery, perspective, stega})
+ return
+}
+function FooterFallback() {
+ return (
+
+ )
+}
+```
+
+The non-draft `` is part of the static shell, so the whole layout is cached and revalidates only when content used by `sanityFetch` changes. In draft mode the layout still renders immediately from its static shell while `` streams in.
diff --git a/.agents/skills/sanity-live-cache-components/reference/live-helpers.md b/.agents/skills/sanity-live-cache-components/reference/live-helpers.md
new file mode 100644
index 000000000..4f7d63307
--- /dev/null
+++ b/.agents/skills/sanity-live-cache-components/reference/live-helpers.md
@@ -0,0 +1,220 @@
+# Live helpers: `client.ts` and `live.ts`
+
+## Contents
+
+- [`client.ts`](#clientts)
+- [`live.ts`](#livets)
+- [`sanityFetch`](#sanityfetch)
+- [`sanityFetchMetadata`](#sanityfetchmetadata)
+- [`getDynamicFetchOptions`](#getdynamicfetchoptions)
+- [`sanityFetchStaticParams`](#sanityfetchstaticparams)
+- [Anti-patterns to grep for](#anti-patterns-to-grep-for)
+
+## `client.ts`
+
+Projects typically have a `src/sanity/lib/client.ts` that exports a `createClient` instance.
+
+**If no `client.ts` exists yet**, use this shape as a starting point:
+
+```ts
+// src/sanity/lib/client.ts
+import {createClient} from 'next-sanity'
+
+export const client = createClient({
+ projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
+ dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
+ useCdn: true,
+ apiVersion: '2026-05-19',
+ perspective: 'published',
+ stega: {studioUrl: process.env.NEXT_PUBLIC_SANITY_STUDIO_URL || 'http://localhost:3333'},
+})
+```
+
+**If `client.ts` already exists**, leave its structure alone. Templates often centralize env-var reads in a separate `sanity/lib/api.ts` with an `assertValue` helper — keep that. Append only what's missing.
+
+- Use a modern `apiVersion` (e.g. today's date as a hardcoded string).
+- `stega.studioUrl` enables stega encoding. It can be a relative string when an embedded Studio is mounted via `NextStudio` from `next-sanity/studio`, otherwise an absolute URL (typically env-driven).
+- Changing `apiVersion` or removing existing `stega.*` options can break callers.
+- Never remove an existing `token` from `createClient`. Private datasets require a client token even for published-content fetches.
+
+## `live.ts`
+
+Create `src/sanity/lib/live.ts` alongside `client.ts`. If it already exists, append only what's missing.
+
+`SANITY_API_READ_TOKEN` must never reach the client bundle. If the project already keeps it in a dedicated server-only module (commonly `src/sanity/lib/token.ts` with `import 'server-only'` at the top), import the token from there instead of inlining the `process.env` read. The example below inlines it for brevity — swap in the existing module if there is one.
+
+```ts
+// src/sanity/lib/live.ts
+import {type QueryParams} from 'next-sanity'
+import {defineLive, resolvePerspectiveFromCookies, type LivePerspective} from 'next-sanity/live'
+import {cookies, draftMode} from 'next/headers'
+import {client} from './client'
+
+const token = process.env.SANITY_API_READ_TOKEN
+if (!token) {
+ throw new Error('Missing SANITY_API_READ_TOKEN')
+}
+
+export const {SanityLive, sanityFetch} = defineLive({
+ client,
+ serverToken: token,
+ browserToken: token,
+ strict: true,
+})
+
+export interface DynamicFetchOptions {
+ perspective: LivePerspective
+ stega: boolean
+}
+export async function getDynamicFetchOptions(): Promise {
+ const {isEnabled: isDraftMode} = await draftMode()
+ if (!isDraftMode) {
+ return {perspective: 'published', stega: false}
+ }
+
+ const jar = await cookies()
+ const perspective = await resolvePerspectiveFromCookies({cookies: jar})
+ return {perspective: perspective ?? 'drafts', stega: true}
+}
+
+// For usage within `generateStaticParams`
+export async function sanityFetchStaticParams({
+ query,
+ params = {},
+}: {
+ query: QueryString
+ params?: QueryParams
+}) {
+ 'use cache'
+ const {data} = await sanityFetch({query, params, perspective: 'published', stega: false})
+ return {data}
+}
+
+// For usage within `generateMetadata` and `generateViewport`
+export async function sanityFetchMetadata({
+ query,
+ params = {},
+ perspective,
+}: {
+ query: QueryString
+ params?: QueryParams
+ perspective: LivePerspective
+}) {
+ 'use cache'
+ const {data} = await sanityFetch({query, params, perspective, stega: false})
+ return {data}
+}
+```
+
+## `sanityFetch`
+
+For fetching data in React Server Components that have a `'use cache'` directive and are rendered (directly or transitively) from a `page.tsx` or `layout.tsx`.
+
+- `perspective` switches between published, drafts, and specific Sanity Content Releases.
+- `stega: true` (combined with `stega.studioUrl` in `createClient` and `` in the root layout) renders click-to-edit overlays.
+- `getDynamicFetchOptions` resolves `perspective` from the `sanity-preview-perspective` cookie, which `` manages when the app is rendered inside Presentation Tool's preview iframe.
+
+The async function that calls `sanityFetch` must carry `'use cache'` or `'use cache: remote'`, and must take `perspective` and `stega` as props. Never hardcode them.
+
+Pattern:
+
+```tsx
+import {sanityFetch, type DynamicFetchOptions} from '@/sanity/lib/live'
+import {defineQuery} from 'next-sanity'
+
+async function CachedComponent({slug, perspective, stega}: {slug: string} & DynamicFetchOptions) {
+ 'use cache'
+ const pageQuery = defineQuery(`*[_type == "page" && slug.current == $slug][0]`)
+ const {data} = await sanityFetch({query: pageQuery, params: {slug}, perspective, stega})
+}
+```
+
+Anti-pattern (hardcoded options break Visual Editing and content-release previewing):
+
+```tsx
+async function CachedComponent({slug}: {slug: string}) {
+ 'use cache'
+ const {data} = await sanityFetch({
+ query: pageQuery,
+ params: {slug},
+ perspective: 'published', // hardcoded
+ stega: false, // hardcoded
+ })
+}
+```
+
+### `sanityFetch` inside server actions
+
+`'use server'` boundaries cannot accept `perspective`/`stega` as props (server action inputs are untrusted). Resolve them inside the `'use server'` function and forward them to a separate `'use cache'` boundary:
+
+```tsx
+import {getDynamicFetchOptions, sanityFetch, type DynamicFetchOptions} from '@/sanity/lib/live'
+import {defineQuery} from 'next-sanity'
+
+async function fetchMore({page, perspective, stega}: {page: string} & DynamicFetchOptions) {
+ 'use cache'
+ const pagesQuery = defineQuery(`*[_type == "page"][0...$page]`)
+ const {data} = await sanityFetch({query: pagesQuery, params: {page}, perspective, stega})
+ return data
+}
+async function renderMore({page}: {page: string}) {
+ 'use server'
+ const {perspective, stega} = await getDynamicFetchOptions()
+ const data = await fetchMore({page, perspective, stega})
+}
+```
+
+Anti-patterns:
+
+- Hardcoding `perspective`/`stega` in the `'use cache'` helper.
+- Calling `sanityFetch` directly inside `'use server'` — it bypasses caching entirely.
+
+### `sanityFetch` inside `route.ts`
+
+Hardcode `stega: false` and resolve only `perspective`. Route handlers don't render a DOM next to ``, so stega encoding only inflates the payload (and can cause downstream errors).
+
+## `sanityFetchMetadata`
+
+For fetching data inside `generateMetadata`, `generateSitemaps`, `generateViewport`, `generateImageMetadata`, and the file-based metadata routes (`icon.tsx`, `apple-icon.tsx`, `manifest.ts`, `opengraph-image.tsx`, `twitter-image.tsx`, `robots.ts`, `sitemap.ts`).
+
+It's `sanityFetch` without `stega` (never wanted in these contexts) and without requiring `'use cache'` at the callsite — the helper already provides it.
+
+Presentation Tool can open an app in a standalone preview window, so the correct content release must still be reflected in `` and friends. Always resolve `perspective`:
+
+```ts
+import {getDynamicFetchOptions, sanityFetchMetadata} from '@/sanity/lib/live'
+import {defineQuery} from 'next-sanity'
+
+export async function generateMetadata({params}: PageProps<'/[slug]'>) {
+ const [{slug}, {perspective}] = await Promise.all([params, getDynamicFetchOptions()])
+ const pageQuery = defineQuery(`*[_type == "page" && slug.current == $slug][0]`)
+ const {data} = await sanityFetchMetadata({query: pageQuery, params: {slug}, perspective})
+}
+```
+
+Anti-pattern: hardcoding `perspective: 'published'` — content-release previewing won't work.
+
+## `getDynamicFetchOptions`
+
+Resolves `perspective` and `stega` outside the `'use cache'` boundary so they can be passed in as plain props. Calls `cookies()`, which is a dynamic API, so the call must live inside a `` boundary (or a route with a sibling `loading.tsx`) so it doesn't block the static shell from streaming.
+
+Avoid calling `getDynamicFetchOptions` in the top-level body of a `layout.tsx` or `page.tsx` that should remain part of the static shell. The exception is routes that intentionally use a sibling `loading.tsx` for fallback UI (see [dynamic-segments.md](dynamic-segments.md)) — there the page can await `getDynamicFetchOptions` directly because `loading.tsx` provides the streaming fallback.
+
+When Cache Components are enabled, `` boundaries determine the static shell. For fully prerendered routes, render the Suspense tree only when in draft mode — see [three-layer-pattern.md](three-layer-pattern.md).
+
+## `sanityFetchStaticParams`
+
+Used inside `generateStaticParams`. `stega` is never wanted (the data feeds route params), and `perspective` cookies aren't available at build time anyway, so both are hardcoded.
+
+- Never call `sanityFetch` inside `generateStaticParams` — always use `sanityFetchStaticParams`.
+- Never call `sanityFetchStaticParams` outside `generateStaticParams`.
+
+## Anti-patterns to grep for
+
+When migrating an existing app, these are the strings to search for and refactor:
+
+- `perspective: 'published'` and `stega: false` hardcoded together in a `sanityFetch` call → replace with `perspective` and `stega` props sourced from `getDynamicFetchOptions` via the three-layer pattern.
+- `sanityFetch(` directly inside a function whose body starts with `'use server'` → split into a separate `'use cache'` helper and forward `perspective`/`stega` as props.
+- `sanityFetch(` inside `generateStaticParams` → swap for `sanityFetchStaticParams`.
+- `sanityFetch(` inside `generateMetadata` / `generateViewport` / `sitemap.ts` / `robots.ts` / `opengraph-image.tsx` etc. → swap for `sanityFetchMetadata` and resolve `perspective` via `getDynamicFetchOptions`.
+- `await draftMode()` immediately followed by `await getDynamicFetchOptions()` at the top of a `page.tsx` or `layout.tsx` without a sibling `loading.tsx` → move the dynamic-API calls into a child component wrapped in `` so the static shell can prerender.
diff --git a/.agents/skills/sanity-live-cache-components/reference/three-layer-pattern.md b/.agents/skills/sanity-live-cache-components/reference/three-layer-pattern.md
new file mode 100644
index 000000000..8f5b34205
--- /dev/null
+++ b/.agents/skills/sanity-live-cache-components/reference/three-layer-pattern.md
@@ -0,0 +1,146 @@
+# Three-layer component pattern
+
+The core architecture for every route that can be fully statically prerendered and cached.
+
+## Contents
+
+- [Structure](#structure)
+- [`generateStaticParams` for dynamic routes](#generatestaticparams-for-dynamic-routes)
+- [Layer 1: Page component](#layer-1-page-component)
+- [Layer 2: Dynamic component](#layer-2-dynamic-component)
+- [Layer 3: Cached component](#layer-3-cached-component)
+- [`searchParams` and other dynamic APIs](#searchparams-and-other-dynamic-apis)
+
+## Structure
+
+```text
+Page/Layout (Layer 1)
+ ├── NOT draft mode → (no Suspense)
+ └── draft mode →
+ (Layer 2)
+ └── (Layer 3)
+```
+
+## `generateStaticParams` for dynamic routes
+
+The examples below use `/[slug]/page.tsx`, which needs:
+
+```tsx
+// src/app/[slug]/page.tsx
+import {sanityFetchStaticParams} from '@/sanity/lib/live'
+import {defineQuery} from 'next-sanity'
+
+export async function generateStaticParams() {
+ const pageSlugsQuery = defineQuery(
+ `*[_type == "page" && defined(slug.current)]{"slug": slug.current}`,
+ )
+ const {data} = await sanityFetchStaticParams({query: pageSlugsQuery})
+ return data
+}
+```
+
+For `/layout.tsx` or `/page.tsx` (no params), skip the `params` handling.
+
+## Layer 1: Page component
+
+Calls `draftMode()` and branches:
+
+```tsx
+// src/app/[slug]/page.tsx (continued)
+import {draftMode} from 'next/headers'
+import {Suspense} from 'react'
+
+export default async function Page({params}: PageProps<'/[slug]'>) {
+ const {isEnabled: isDraftMode} = await draftMode()
+ if (isDraftMode) {
+ return (
+ }>
+ ` so the Suspense boundary works
+ params={params}
+ />
+
+ )
+ }
+ const {slug} = await params
+ return
+}
+```
+
+Notes:
+
+- `Page` does **not** have a `'use cache'` directive. `draftMode()` is allowed inside `'use cache'`, but `Page` also `awaits` `params` (and may call `getDynamicFetchOptions()`, which reads `cookies()`), and those dynamic APIs are not allowed inside `'use cache'`. It's enough for `` (Layer 3) to carry `'use cache'` for `Page` to be prerendered as part of the static shell.
+- Requires `generateStaticParams` if `params` is used as input to `sanityFetch`.
+- Not in draft mode → no `` boundary, maximizes the static shell.
+- In draft mode → `` inside `` will suspend twice:
+ 1. when `` awaits `getDynamicFetchOptions()`
+ 2. when `` awaits `sanityFetch` with the resolved `perspective`/`stega`
+
+ A good fallback skeleton that doesn't cause layout shift is highly recommended.
+
+## Layer 2: Dynamic component
+
+Resolves `params`, `cookies()`, and `headers()` outside the cache boundary and passes plain props in:
+
+```tsx
+// src/app/[slug]/page.tsx (continued)
+import {getDynamicFetchOptions} from '@/sanity/lib/live'
+
+async function DynamicPage({params}: Pick, 'params'>) {
+ const [{slug}, {perspective, stega}] = await Promise.all([params, getDynamicFetchOptions()])
+
+ return
+}
+```
+
+`draftMode()` is the only dynamic API allowed inside `'use cache'`, but in this pattern it isn't needed in Layer 3 because `perspective` and `stega` already encode the draft state.
+
+## Layer 3: Cached component
+
+Has `'use cache'` and only receives plain, serializable props:
+
+```tsx
+// src/app/[slug]/page.tsx (continued)
+import {sanityFetch, type DynamicFetchOptions} from '@/sanity/lib/live'
+import {defineQuery} from 'next-sanity'
+
+async function CachedPage({
+ slug,
+ perspective,
+ stega,
+}: Awaited['params']> & DynamicFetchOptions) {
+ 'use cache'
+ const pageQuery = defineQuery(`*[_type == "page" && slug.current == $slug][0]`)
+ const {data} = await sanityFetch({
+ query: pageQuery,
+ params: {slug},
+ perspective,
+ stega,
+ })
+ return {/* use `data` to render stuff */}
+}
+```
+
+## `searchParams` and other dynamic APIs
+
+If `searchParams` or other dynamic APIs are inputs to `sanityFetch` (or `params` is used without `generateStaticParams` or a `loading.tsx`), always render the `` tree and **stop branching on `draftMode`**. See [the streaming guide](https://nextjs.org/docs/app/guides/streaming#when-to-use-loadingjs-vs-suspense) for picking between `loading.tsx` and ``.
+
+```tsx
+// src/app/[slug]/page.tsx (continued)
+import {Suspense} from 'react'
+
+// Do not export an async function here, to avoid accidentally blocking render while awaiting a dynamic API
+export default function Page({params}: PageProps<'/[slug]'>) {
+ return (
+ }
+ >
+ ` so the Suspense boundary works
+ params={params}
+ />
+
+ )
+}
+```
diff --git a/.agents/skills/seo-aeo-best-practices/SKILL.md b/.agents/skills/seo-aeo-best-practices/SKILL.md
new file mode 100644
index 000000000..ade2bb51e
--- /dev/null
+++ b/.agents/skills/seo-aeo-best-practices/SKILL.md
@@ -0,0 +1,37 @@
+---
+name: seo-aeo-best-practices
+description: SEO and AEO best practices for metadata, Open Graph, sitemaps, robots.txt, hreflang, JSON-LD structured data, EEAT, and content optimized for search engines and AI answer surfaces. Use this skill when implementing page SEO, technical SEO, schema markup, international SEO, AI-overview readiness, or improving content for Google, ChatGPT, Perplexity, and similar assistants.
+---
+
+# SEO & AEO Best Practices
+
+Principles for optimizing content for both traditional search engines (SEO) and AI-powered answer engines (AEO). Includes Google's EEAT guidelines and structured data implementation.
+
+## When to Apply
+
+Reference these guidelines when:
+- Implementing metadata and Open Graph tags
+- Creating sitemaps and robots.txt
+- Adding JSON-LD structured data
+- Optimizing content for featured snippets
+- Preparing content for AI assistants (ChatGPT, Perplexity, etc.)
+- Evaluating content quality using EEAT principles
+
+## Core Concepts
+
+### SEO (Search Engine Optimization)
+Optimizing content to rank well in traditional search results (Google, Bing).
+
+### AEO (Answer Engine Optimization)
+Optimizing content to be selected as authoritative answers by AI systems.
+
+### EEAT (Experience, Expertise, Authoritativeness, Trustworthiness)
+Google's framework for evaluating content quality.
+
+## References
+
+Start with the one reference that matches the task, such as technical SEO, structured data, EEAT, or AI-answer readiness. See `references/` for detailed guidance:
+- `references/eeat-principles.md` — EEAT implementation and author schema
+- `references/structured-data.md` — JSON-LD patterns (Article, FAQ, Breadcrumb, Product)
+- `references/technical-seo.md` — Technical SEO checklist (metadata, sitemaps, hreflang, robots.txt)
+- `references/aeo-considerations.md` — AI/AEO considerations (AI Overviews, crawler management)
diff --git a/.agents/skills/seo-aeo-best-practices/references/aeo-considerations.md b/.agents/skills/seo-aeo-best-practices/references/aeo-considerations.md
new file mode 100644
index 000000000..227d79e8e
--- /dev/null
+++ b/.agents/skills/seo-aeo-best-practices/references/aeo-considerations.md
@@ -0,0 +1,159 @@
+# AI/AEO Considerations
+
+Answer Engine Optimization (AEO) prepares content to be selected as authoritative answers by AI systems like ChatGPT, Perplexity, Google AI Overviews, and Bing Copilot.
+
+## How AI Selects Answers
+
+AI systems evaluate content based on:
+
+1. **Clarity:** Is the answer direct and easy to extract?
+2. **Authority:** Is the source trustworthy?
+3. **Comprehensiveness:** Does it fully address the question?
+4. **Recency:** Is the information up to date?
+5. **Structure:** Can the AI parse and understand it?
+
+## Content Structure for AI
+
+### Direct Answers First
+Lead with the answer, then explain.
+
+**Bad:**
+> The history of JavaScript dates back to 1995 when Brendan Eich... [500 words later] ...JavaScript runs in the browser.
+
+**Good:**
+> JavaScript is a programming language that runs in web browsers. It was created in 1995 by Brendan Eich...
+
+### Clear Headings
+Use descriptive H2/H3 headings that match user questions.
+
+**Bad:** "Overview" → "Details" → "More Information"
+**Good:** "What is X?" → "How does X work?" → "When should you use X?"
+
+### Lists and Tables
+AI extracts structured information more easily than prose.
+
+```markdown
+## Benefits of Structured Content
+
+- **Reusability:** Use content across channels
+- **Flexibility:** Change presentation without changing content
+- **Scalability:** Manage large content volumes
+```
+
+### FAQ Format
+Question-answer pairs are ideal for AI extraction.
+
+```typescript
+// Schema for AI-friendly FAQs
+defineType({
+ name: 'faq',
+ type: 'document',
+ fields: [
+ defineField({ name: 'question', type: 'string' }),
+ defineField({ name: 'answer', type: 'text' }),
+ defineField({ name: 'category', type: 'reference', to: [{ type: 'faqCategory' }] }),
+ ]
+})
+```
+
+## Technical Implementation
+
+### Structured Data (Critical)
+JSON-LD helps AI understand content type and relationships.
+
+```typescript
+// FAQ structured data
+const faqSchema = {
+ "@context": "https://schema.org",
+ "@type": "FAQPage",
+ mainEntity: faqs.map(faq => ({
+ "@type": "Question",
+ name: faq.question,
+ acceptedAnswer: {
+ "@type": "Answer",
+ text: faq.answer
+ }
+ }))
+}
+```
+
+### Canonical Content
+Ensure AI finds your authoritative version, not copies.
+
+- Set canonical URLs
+- Avoid duplicate content across pages
+- Use `rel="canonical"` for syndicated content
+
+### Freshness Signals
+AI systems prefer current information.
+
+- Display publish and update dates prominently
+- Update content regularly with substantive changes (superficial updates like changing dates without meaningful edits can be counterproductive)
+- Use `dateModified` in structured data
+
+## Content Quality Signals
+
+### Author Credentials
+AI systems increasingly check author authority.
+
+- Display author name and credentials
+- Link to author profiles
+- Include author structured data
+
+### Citations and Sources
+Linking to authoritative sources increases trust.
+
+- Cite primary sources
+- Link to studies, documentation, official sources
+- Avoid circular citations (sites citing each other)
+
+### Comprehensive Coverage
+AI prefers content that fully answers questions.
+
+- Cover related questions users might have
+- Include definitions for technical terms
+- Address common misconceptions
+
+## Google AI Overviews
+
+Google's AI Overviews (formerly SGE) now appear in many search results. To optimize:
+
+- **Be the cited source:** AI Overviews cite specific pages. Concise, authoritative answers increase citation likelihood.
+- **Structure for extraction:** Use clear headings, direct answers, and lists that AI can easily parse.
+- **Cover follow-up questions:** AI Overviews often address related queries. Anticipate and answer them on the same page or link to dedicated pages.
+- **Monitor in Search Console:** Google Search Console provides data on AI Overview impressions and clicks.
+
+## AI Crawler Management
+
+Make conscious decisions about which AI systems can crawl your content:
+
+- **robots.txt directives:** Use `User-agent: GPTBot`, `ClaudeBot`, `PerplexityBot`, `Google-Extended` to control access.
+- **Allowing crawlers** increases chances of being cited as a source in AI responses.
+- **Blocking crawlers** prevents content from being used in AI training (but may reduce AI citations).
+- Review your policy regularly — this is one of the most actively evolving areas of SEO.
+
+## Measuring AEO Success
+
+### Monitor AI Mentions
+Track when AI assistants cite your content:
+- Use Google Search Console's AI Overview data for impression and click tracking
+- Monitor referral traffic from AI platforms (Perplexity, ChatGPT, Bing Copilot)
+- Search for your brand + "according to" in AI assistants
+- Consider third-party AEO tracking tools for comprehensive monitoring
+
+### Track Zero-Click Queries
+If AI answers questions directly, traditional rankings matter less.
+
+### Featured Snippet Capture
+Featured snippets often become AI answers. Track which you own.
+
+## AEO vs SEO Balance
+
+AEO and SEO largely align—quality content serves both. Key differences:
+
+| Aspect | SEO Focus | AEO Focus |
+|--------|-----------|-----------|
+| Goal | Rank on page 1 | Be THE answer |
+| Format | Varies | Direct, structured |
+| Length | Often longer | Concise + comprehensive |
+| Links | Link building | Source citations |
diff --git a/.agents/skills/seo-aeo-best-practices/references/eeat-principles.md b/.agents/skills/seo-aeo-best-practices/references/eeat-principles.md
new file mode 100644
index 000000000..899a12039
--- /dev/null
+++ b/.agents/skills/seo-aeo-best-practices/references/eeat-principles.md
@@ -0,0 +1,127 @@
+# EEAT Principles
+
+Google's EEAT framework (Experience, Expertise, Authoritativeness, Trustworthiness) guides how content quality is evaluated. This applies to both SEO rankings and AI answer selection.
+
+## The Four Pillars
+
+### Experience
+First-hand or life experience with the topic.
+
+**Signals:**
+- Personal anecdotes and case studies
+- "I tested this" content
+- Real-world results and screenshots
+- User-generated reviews
+
+**Implementation:**
+- Include author bios with relevant experience
+- Add "About the Author" sections
+- Feature customer testimonials
+- Show real examples, not just theory
+
+### Expertise
+Knowledge and skill in the subject area.
+
+**Signals:**
+- Credentials and qualifications
+- Depth of content coverage
+- Technical accuracy
+- Citations to authoritative sources
+
+**Implementation:**
+- Display author credentials
+- Link to primary sources
+- Cover topics comprehensively
+- Keep content technically accurate and updated
+
+### Authoritativeness
+Recognition as a go-to source in the field.
+
+**Signals:**
+- Backlinks from respected sites
+- Mentions in industry publications
+- Social proof and follower counts
+- Brand recognition
+
+**Implementation:**
+- Build thought leadership content
+- Contribute to industry publications
+- Maintain consistent publishing
+- Develop recognizable brand voice
+
+### Trustworthiness
+Accuracy, transparency, and legitimacy.
+
+**Signals:**
+- Clear authorship and contact info
+- Accurate, fact-checked content
+- Secure website (HTTPS)
+- Privacy policy and terms
+
+**Implementation:**
+- Display clear author attribution
+- Include publication and update dates
+- Provide contact information
+- Use HTTPS and maintain security
+
+## Sanity Implementation
+
+```typescript
+// Author schema with EEAT signals
+defineType({
+ name: 'author',
+ type: 'document',
+ fields: [
+ defineField({ name: 'name', type: 'string' }),
+ defineField({ name: 'role', type: 'string' }),
+ defineField({ name: 'bio', type: 'text' }),
+ defineField({ name: 'credentials', type: 'array', of: [{ type: 'string' }] }),
+ defineField({ name: 'image', type: 'image' }),
+ // sameAs: used for schema.org Person structured data output
+ defineField({ name: 'sameAs', type: 'array', of: [{ type: 'url' }],
+ description: 'Canonical profile URLs (LinkedIn, Twitter, etc.) for schema.org Person'
+ }),
+ // socialLinks: used for display purposes (platform icons, labels)
+ defineField({
+ name: 'socialLinks',
+ type: 'array',
+ of: [{ type: 'object', fields: [
+ defineField({ name: 'platform', type: 'string' }),
+ defineField({ name: 'url', type: 'url' })
+ ]}],
+ description: 'Social links for display in the UI. Use sameAs for structured data output.'
+ }),
+ ]
+})
+
+// Content with EEAT metadata
+defineType({
+ name: 'post',
+ fields: [
+ defineField({ name: 'author', type: 'reference', to: [{ type: 'author' }] }),
+ defineField({ name: 'publishedAt', type: 'datetime' }),
+ defineField({ name: 'updatedAt', type: 'datetime' }),
+ defineField({
+ name: 'reviewedBy',
+ type: 'reference',
+ to: [{ type: 'author' }],
+ description: 'Expert reviewer for fact-checking'
+ }),
+ defineField({
+ name: 'sources',
+ type: 'array',
+ of: [{ type: 'url' }],
+ description: 'Citations and references'
+ }),
+ ]
+})
+```
+
+## YMYL Considerations
+
+"Your Money or Your Life" topics (health, finance, legal, safety) require extra EEAT rigor:
+
+- Medical content reviewed by healthcare professionals
+- Financial advice from certified experts
+- Legal content reviewed by attorneys
+- Clear disclaimers where appropriate
diff --git a/.agents/skills/seo-aeo-best-practices/references/structured-data.md b/.agents/skills/seo-aeo-best-practices/references/structured-data.md
new file mode 100644
index 000000000..84292dc32
--- /dev/null
+++ b/.agents/skills/seo-aeo-best-practices/references/structured-data.md
@@ -0,0 +1,183 @@
+# Structured Data (JSON-LD)
+
+Structured data helps search engines and AI understand your content. JSON-LD is the recommended format.
+
+## Why Structured Data Matters
+
+- **Rich snippets:** Enhanced search result appearance
+- **Knowledge panels:** Featured information boxes
+- **AI training:** Better content understanding
+- **Voice search:** Answer selection for voice queries
+
+## Common Schema Types
+
+### Article / Blog Post
+
+```typescript
+import { Article, WithContext } from 'schema-dts'
+
+const articleSchema: WithContext = {
+ "@context": "https://schema.org",
+ "@type": "Article",
+ headline: post.title,
+ description: post.excerpt,
+ image: post.image?.url,
+ datePublished: post.publishedAt,
+ dateModified: post.updatedAt,
+ author: {
+ "@type": "Person",
+ name: post.author.name,
+ url: post.author.url
+ },
+ publisher: {
+ "@type": "Organization",
+ name: "Your Company",
+ logo: {
+ "@type": "ImageObject",
+ url: "https://example.com/logo.png"
+ }
+ }
+}
+```
+
+### FAQ Page
+
+```typescript
+import { FAQPage, WithContext } from 'schema-dts'
+
+const faqSchema: WithContext = {
+ "@context": "https://schema.org",
+ "@type": "FAQPage",
+ mainEntity: faqs.map(faq => ({
+ "@type": "Question",
+ name: faq.question,
+ acceptedAnswer: {
+ "@type": "Answer",
+ text: faq.answer // Plain text, use pt::text() in GROQ
+ }
+ }))
+}
+```
+
+### Organization
+
+```typescript
+import { Organization, WithContext } from 'schema-dts'
+
+const orgSchema: WithContext = {
+ "@context": "https://schema.org",
+ "@type": "Organization",
+ name: "Your Company",
+ url: "https://example.com",
+ logo: "https://example.com/logo.png",
+ sameAs: [
+ "https://twitter.com/company",
+ "https://linkedin.com/company/company"
+ ],
+ contactPoint: {
+ "@type": "ContactPoint",
+ telephone: "+1-555-555-5555",
+ contactType: "customer service"
+ }
+}
+```
+
+### Product
+
+```typescript
+import { Product, WithContext } from 'schema-dts'
+
+const productSchema: WithContext = {
+ "@context": "https://schema.org",
+ "@type": "Product",
+ name: product.name,
+ description: product.description,
+ image: product.images,
+ offers: {
+ "@type": "Offer",
+ price: product.price,
+ priceCurrency: "USD",
+ availability: "https://schema.org/InStock"
+ },
+ aggregateRating: product.rating ? {
+ "@type": "AggregateRating",
+ ratingValue: product.rating.average,
+ reviewCount: product.rating.count
+ } : undefined
+}
+```
+
+### Breadcrumb
+
+```typescript
+import { BreadcrumbList, WithContext } from 'schema-dts'
+
+const breadcrumbSchema: WithContext = {
+ "@context": "https://schema.org",
+ "@type": "BreadcrumbList",
+ itemListElement: breadcrumbs.map((crumb, index) => ({
+ "@type": "ListItem",
+ position: index + 1, // schema.org positions are 1-based
+ name: crumb.title,
+ item: `https://example.com${crumb.path}`
+ }))
+}
+```
+
+## Combining Multiple Schemas (@graph)
+
+Real-world pages often need multiple schema types. Use `@graph` to combine them. The `@context` is defined once at the top level — omit it from individual schema generators when used inside `@graph`:
+
+```typescript
+const pageSchema = {
+ "@context": "https://schema.org",
+ "@graph": [
+ generateArticleSchema(post), // No @context needed here
+ generateBreadcrumbSchema(breadcrumbs),
+ generateOrganizationSchema(),
+ ]
+}
+```
+
+## Implementation in Next.js
+
+```typescript
+// Component to render JSON-LD
+// Ensure data comes from trusted sources (your CMS).
+// If data could contain user-generated content, strip HTML tags
+// and escape special characters before passing to JSON.stringify.
+function JsonLd({ data }: { data: WithContext }) {
+ return (
+
+ )
+}
+
+// Usage in page
+export default function PostPage({ post }) {
+ return (
+ <>
+
+ ...
+ >
+ )
+}
+```
+
+## GROQ for Plain Text
+
+Structured data often needs plain text, not rich text:
+
+```groq
+*[_type == "faq"]{
+ question,
+ "answer": pt::text(answerRichText) // Convert Portable Text to plain string
+}
+```
+
+## Testing Tools
+
+- [Google Rich Results Test](https://search.google.com/test/rich-results)
+- [Schema.org Validator](https://validator.schema.org/)
diff --git a/.agents/skills/seo-aeo-best-practices/references/technical-seo.md b/.agents/skills/seo-aeo-best-practices/references/technical-seo.md
new file mode 100644
index 000000000..a0ba6e668
--- /dev/null
+++ b/.agents/skills/seo-aeo-best-practices/references/technical-seo.md
@@ -0,0 +1,188 @@
+# Technical SEO Checklist
+
+Essential technical SEO elements for modern web applications.
+
+## Table of Contents
+
+- Metadata
+- Sitemaps
+- Canonical URLs
+- Redirects
+- Performance
+- Robots.txt
+- International SEO
+
+## Metadata
+
+### Title Tags
+- Unique per page
+- 50-60 characters
+- Primary keyword near the beginning
+- Brand name at the end (optional)
+
+### Meta Descriptions
+- Unique per page
+- 150-160 characters
+- Include call-to-action
+- Contain relevant keywords
+
+### Open Graph
+```html
+
+
+
+
+
+```
+
+### Sanity + Next.js Implementation
+
+```typescript
+export async function generateMetadata({ params }): Promise {
+ const { data } = await sanityFetch({
+ query: PAGE_QUERY,
+ stega: false, // Critical: no stega in metadata
+ })
+
+ return {
+ title: data.seo?.title || data.title,
+ description: data.seo?.description,
+ openGraph: {
+ images: data.seo?.image ? [{
+ url: urlFor(data.seo.image).width(1200).height(630).url(),
+ width: 1200,
+ height: 630,
+ }] : [],
+ },
+ robots: data.seo?.noIndex ? 'noindex' : undefined,
+ }
+}
+```
+
+## Sitemaps
+
+Dynamic sitemap from CMS content:
+
+```typescript
+// app/sitemap.ts
+import { MetadataRoute } from 'next'
+
+export default async function sitemap(): Promise {
+ const pages = await client.fetch(`
+ *[_type in ["page", "post"] && defined(slug.current) && seo.noIndex != true]{
+ "url": select(
+ _type == "page" => "/" + slug.current,
+ _type == "post" => "/blog/" + slug.current
+ ),
+ _updatedAt
+ }
+ `)
+
+ return pages.map(page => ({
+ url: `https://example.com${page.url}`,
+ lastModified: new Date(page._updatedAt),
+ // Note: changeFrequency and priority are largely ignored by Google
+ // but may be used by other search engines
+ }))
+}
+```
+
+## Canonical URLs
+
+Prevent duplicate content issues:
+
+```typescript
+export async function generateMetadata({ params }): Promise {
+ return {
+ alternates: {
+ canonical: `https://example.com/${params.slug}`,
+ },
+ }
+}
+```
+
+## Redirects
+
+CMS-managed redirects:
+
+```typescript
+// next.config.ts
+async redirects() {
+ const redirects = await client.fetch(`
+ *[_type == "redirect" && isEnabled == true]{
+ source,
+ destination,
+ permanent
+ }
+ `)
+ return redirects
+}
+```
+
+## Performance
+
+[Core Web Vitals](https://web.dev/articles/defining-core-web-vitals-thresholds) impact rankings:
+
+- **LCP (Largest Contentful Paint):** < 2.5s
+- **INP (Interaction to Next Paint):** < 200ms
+- **CLS (Cumulative Layout Shift):** < 0.1
+
+### Image Optimization (Next.js example)
+- Use `next/image` with Sanity URL builder
+- Serve WebP/AVIF formats
+- Implement LQIP blur placeholders
+- Set explicit dimensions
+
+### Font Loading (Next.js example)
+```typescript
+// Prevent layout shift
+import { Inter } from 'next/font/google'
+const inter = Inter({ subsets: ['latin'], display: 'swap' })
+```
+
+## Robots.txt
+
+```
+# public/robots.txt
+User-agent: *
+Allow: /
+Disallow: /api/
+Disallow: /studio/
+
+# AI crawlers — allow or block based on your content strategy
+# Uncomment to block specific AI crawlers:
+# User-agent: GPTBot
+# Disallow: /
+# User-agent: ClaudeBot
+# Disallow: /
+# User-agent: PerplexityBot
+# Disallow: /
+# User-agent: Google-Extended
+# Disallow: /
+
+Sitemap: https://example.com/sitemap.xml
+```
+
+**AI crawler considerations:** Decide whether AI training crawlers should access your content. Blocking `Google-Extended` prevents AI training use while still allowing Google Search indexing. Review your policy regularly as this landscape evolves.
+
+## International SEO (hreflang)
+
+For multi-language sites, implement hreflang tags to indicate language/region variants:
+
+```typescript
+export async function generateMetadata({ params }: { params: Promise<{ lang: string; slug: string }> }): Promise {
+ const { lang, slug } = await params
+ return {
+ alternates: {
+ canonical: `https://example.com/${lang}/${slug}`,
+ languages: {
+ 'en': `https://example.com/en/${slug}`,
+ 'de': `https://example.com/de/${slug}`,
+ 'x-default': `https://example.com/en/${slug}`,
+ },
+ },
+ }
+}
+```
+
+Include all language variants in sitemaps with `hreflang` annotations for proper indexing.
diff --git a/.github/.branch-cleanup b/.github/.branch-cleanup
new file mode 100644
index 000000000..94467fc44
--- /dev/null
+++ b/.github/.branch-cleanup
@@ -0,0 +1 @@
+cleanup
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 000000000..46c657e91
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,153 @@
+name: CI
+
+on:
+ pull_request:
+ push:
+ branches: [dev, main]
+
+concurrency:
+ group: ci-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ verify:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: pnpm/action-setup@v4
+
+ # node-version-file (not a bare "22") is what actually enforces the
+ # >=22.12 floor required by sanity 6 / @sanity/ui 4.
+ - uses: actions/setup-node@v4
+ with:
+ node-version-file: ".nvmrc"
+ cache: "pnpm"
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ # 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 (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
+
+ - name: Typecheck (site)
+ run: pnpm --filter @codingcatdev/site typecheck
+
+ - name: Typecheck (studio)
+ run: pnpm --filter @codingcatdev/sanity exec tsc --noEmit
+
+ - name: Build site
+ run: pnpm --filter @codingcatdev/site build
+
+ - name: Build studio
+ run: pnpm --filter @codingcatdev/sanity build
+ env:
+ 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.
+ - name: Verify extract.json is current
+ run: |
+ if ! git diff --quiet -- apps/sanity/extract.json; then
+ echo "::error::apps/sanity/extract.json is stale. Run 'pnpm typegen' and commit the result."
+ git diff --stat -- apps/sanity/extract.json
+ exit 1
+ fi
+
+ preview:
+ needs: verify
+ if: github.event_name == 'pull_request' && github.base_ref == 'dev'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: pnpm/action-setup@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version-file: ".nvmrc"
+ cache: "pnpm"
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Generate Sanity Types
+ run: pnpm typegen
+ env:
+ 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
+
+ - name: Upload Cloudflare Worker Preview Version
+ id: upload_preview
+ run: |
+ 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)
+ 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 "::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
+ echo "- **Version ID**: \`${VERSION_ID}\`" >> $GITHUB_STEP_SUMMARY
+ echo "- **Target Worker**: \`codingcatdev-dev\`" >> $GITHUB_STEP_SUMMARY
+
+ if [ -n "$PREVIEW_URL" ]; then
+ {
+ 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
+ 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 }}
+ 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
new file mode 100644
index 000000000..0b40510c9
--- /dev/null
+++ b/.github/workflows/deploy-workers.yml
@@ -0,0 +1,83 @@
+name: Deploy Site to Cloudflare Workers
+
+on:
+ push:
+ branches: [dev, main]
+ paths:
+ - "apps/site/**"
+ - "packages/**"
+ - "pnpm-lock.yaml"
+ - ".github/workflows/deploy-workers.yml"
+
+concurrency:
+ group: deploy-site-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ deploy-dev:
+ if: github.ref == 'refs/heads/dev'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: pnpm/action-setup@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version-file: ".nvmrc"
+ cache: "pnpm"
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Generate Sanity Types
+ run: pnpm typegen
+ env:
+ 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
+
+ - name: Deploy to Cloudflare Workers (dev.codingcat.dev)
+ run: pnpm --filter @codingcatdev/site exec wrangler deploy
+ env:
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+
+ deploy-production:
+ if: github.ref == 'refs/heads/main'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: pnpm/action-setup@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version-file: ".nvmrc"
+ cache: "pnpm"
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Generate Sanity Types
+ run: pnpm typegen
+ env:
+ 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
+
+ - name: Deploy to Cloudflare Workers (codingcat.dev)
+ 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 }}
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index c937ba600..af82605d8 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -1,12 +1,17 @@
-name: Deploy CodingCat.dev
+name: Deploy Sanity Studio
on:
+ workflow_dispatch:
push:
branches: [dev, main]
+ paths:
+ - "apps/sanity/**"
+ - "pnpm-lock.yaml"
+ - ".github/workflows/deploy.yml"
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
@@ -17,35 +22,20 @@ jobs:
- uses: actions/setup-node@v4
with:
- node-version: "22"
+ node-version-file: ".nvmrc"
cache: "pnpm"
- 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
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'
@@ -59,31 +49,17 @@ jobs:
- uses: actions/setup-node@v4
with:
- node-version: "22"
+ node-version-file: ".nvmrc"
cache: "pnpm"
- 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
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/.gitignore b/.gitignore
index 2059b78a4..746942e8e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@ node_modules/
# build output
dist/
+.next/
# astro
.astro/
@@ -37,3 +38,6 @@ yarn-error.log*
!.vscode/mcp.json
# pnpm workspace — root pnpm-lock.yaml is the lockfile
package-lock.json
+
+# agents teamwork
+.agents/
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 000000000..c94711948
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+22.23.2
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 000000000..2a485f4c7
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,88 @@
+# codingcat.dev
+
+pnpm monorepo. Node >= 22.12 (see `.nvmrc`); pnpm 10.34.1.
+
+## Packages
+
+| Path | Package | What it is |
+|---|---|---|
+| `apps/site` | `@codingcatdev/site` | **Astro 7 SSR on Cloudflare Workers** — the public site. Active development. |
+| `apps/web` | `@codingcatdev/web` | Next.js 16 app. Being retired: its public half is replaced by `apps/site`; the dashboard + sponsor portal move to `admin.codingcat.dev`. |
+| `apps/sanity` | `@codingcatdev/sanity` | Sanity Studio v6, deployed to Sanity hosting. |
+
+Root scripts target `apps/site`. Use `pnpm dev:dashboard` / `build:dashboard` for the Next app, `pnpm dev:studio` for the Studio.
+
+## In-flight migration
+
+Moving from Next.js on Vercel to Astro 7 SSR on Cloudflare Workers, with Sanity
+upgraded to v6 and the video/AI pipeline (Remotion, Gemini, ElevenLabs,
+NotebookLM, GCS, YouTube upload) dropped. Auth moves from Supabase to
+better-auth on Cloudflare D1.
+
+Branch note: `dev` still holds the **pre-Next Astro app** and `main` holds the
+Next app — `main` is ahead. Harvest old Astro code with `git show dev:`,
+never by checking `dev` out over the working tree.
+
+## Conventions that bite
+
+- **`output: "server"` — there is no `getStaticPaths` anywhere.** Pagination is a
+ runtime bounds check on `Astro.params`, not a build-time param list.
+- **GROQ queries must live in `.ts` files.** Sanity TypeGen cannot parse `.astro`
+ frontmatter, so `defineQuery` calls in `.astro` files are silently skipped.
+- **Never read secrets from `import.meta.env`.** Vite inlines non-`PUBLIC_` vars
+ into the server bundle, baking them into the deployed Worker script. Read them
+ from `import { env } from "cloudflare:workers"` — `@astrojs/cloudflare` v14
+ reduced `Runtime` to `{ cfContext }` and deprecated `locals.runtime`.
+- **Cloudflare bindings are not inherited into `env.production`.** Anything added
+ to the top level of `wrangler.jsonc` must be repeated there.
+- **The environment is chosen at BUILD time, via `CLOUDFLARE_ENV`.** The adapter
+ flattens `wrangler.jsonc` into `dist/server/wrangler.json` and writes
+ `.wrangler/deploy/config.json`, which redirects wrangler away from the source
+ config — so `wrangler deploy --env production` on a default build silently
+ ships the *dev* dataset under the *dev* worker name. Use
+ `pnpm --filter @codingcatdev/site build:production`.
+- **`Astro.site` is baked in at build time**, so it cannot carry a per-env
+ origin. Canonicals, `og:url`, feeds and the sitemap read `Astro.locals.siteUrl`,
+ resolved per request from the `SITE_URL` var in middleware.
+- **`stegaClean` anything bound to an attribute**, ``, JSON-LD, or XML.
+ Stega's zero-width characters are harmless in text nodes and corrupting in
+ `href`, `src`, and feeds.
+- **`@sanity/icons` v5 has no root-entry icon exports.** Import from subpaths:
+ `import {UserIcon} from "@sanity/icons/User"`.
+- **`astro-portabletext` uses singular `type` / `block` / `mark` keys**, and mark
+ components read the mark definition from `node.markDef`, not `node`. Both fail
+ silently — the component simply never runs.
+- **A self-closing `` in an `.astro` file breaks props inference**, so
+ `Astro.props` degrades to `Record`. Use paired tags.
+- `apps/sanity/plugins/podcast-rss/` is vendored, not an npm dep — see its README.
+
+## Agent skills
+
+Sanity guidance is vendored in `.agents/skills/` (pinned by `skills-lock.json`).
+The Astro-relevant ones for this migration:
+
+- `.agents/skills/portable-text-serialization/rules/astro.md` — `astro-portabletext`
+ usage. Note it uses **singular** `type` / `block` / `mark` keys, unlike
+ `@portabletext/react`'s plural `types` / `marks`.
+- `.agents/skills/sanity-best-practices/references/astro.md`
+- `.agents/skills/content-modeling-best-practices/`, `seo-aeo-best-practices/`
+
+`.agents/skills/sanity-live-cache-components/` documents the Next.js
+`cacheComponents` + `next-sanity` pattern being removed. It applies only to
+`apps/web` and should be deleted with it.
+
+Cloudflare guidance comes from the Claude Code cloudflare plugin — invoke the
+`cloudflare`, `workers-best-practices`, and `wrangler` skills rather than
+guessing Workers APIs.
+
+## Verifying
+
+`pnpm lint`, `pnpm typecheck`, `pnpm build` cover `apps/site`; CI also builds the
+Studio and fails if `apps/sanity/extract.json` has drifted from the schema.
+
+Bindings only resolve under `wrangler dev`, not `astro dev` — verify anything
+touching `locals.runtime.env`, D1, or OG image generation against a real Worker.
+
+`apps/site/baseline/` holds pre-migration production `sitemap.xml` and RSS
+snapshots (472 URLs, 50 items per feed). Diff against them before cutover; a
+changed podcast GUID re-publishes every episode to Apple/Spotify.
diff --git a/apps/sanity/components/CodeSandboxPreview.tsx b/apps/sanity/components/CodeSandboxPreview.tsx
index cce81e46f..ed1af52ba 100644
--- a/apps/sanity/components/CodeSandboxPreview.tsx
+++ b/apps/sanity/components/CodeSandboxPreview.tsx
@@ -1,5 +1,4 @@
import React from "react";
-import { RenderPreviewCallbackProps } from "sanity";
// TODO: Add stronger typing
const CodePenPreview = (props: any) => {
diff --git a/apps/sanity/components/YouTubePreview.tsx b/apps/sanity/components/YouTubePreview.tsx
index 8ae51ab8f..3fba10c2e 100644
--- a/apps/sanity/components/YouTubePreview.tsx
+++ b/apps/sanity/components/YouTubePreview.tsx
@@ -105,7 +105,7 @@ export function YoutubeInputComponent(props: ObjectInputProps) {
);
return (
-
+
{" "}
{!!props.value && (
diff --git a/apps/sanity/components/documentActions/SharePreviewActionButton.tsx b/apps/sanity/components/documentActions/SharePreviewActionButton.tsx
index 7c94a4d78..7b496c076 100644
--- a/apps/sanity/components/documentActions/SharePreviewActionButton.tsx
+++ b/apps/sanity/components/documentActions/SharePreviewActionButton.tsx
@@ -1,9 +1,7 @@
import React, { useEffect, useState } from "react";
-import { useClient } from "sanity";
-import { CopyIcon } from "@sanity/icons";
+import { CopyIcon } from "@sanity/icons/Copy";
import { Button, Dialog, Text, Stack, Card } from "@sanity/ui";
import { RecycleIcon } from "lucide-react";
-import { apiVersion, dataset, projectId, studioUrl } from "@/sanity/lib/api";
interface SharePreviewActionButtonProps {
id: string;
@@ -60,7 +58,7 @@ const SharePreviewActionButton: React.FC = ({
onClose={onClose}
>
-
+
{loading && Generating link...}
{!loading && shareUrl && (
<>
diff --git a/apps/sanity/components/documentActions/sharePreviewAction.tsx b/apps/sanity/components/documentActions/sharePreviewAction.tsx
index c1eea61c0..9a7f1cc53 100644
--- a/apps/sanity/components/documentActions/sharePreviewAction.tsx
+++ b/apps/sanity/components/documentActions/sharePreviewAction.tsx
@@ -1,7 +1,7 @@
import type { DocumentActionComponent, DocumentActionProps } from "sanity";
import React, { useState } from "react";
import SharePreviewActionButton from "./SharePreviewActionButton";
-import { ShareIcon } from "@sanity/icons";
+import { ShareIcon } from "@sanity/icons/Share";
export const sharePreviewAction: DocumentActionComponent = (
props: DocumentActionProps,
diff --git a/apps/sanity/extract.json b/apps/sanity/extract.json
new file mode 100644
index 000000000..71fddaddc
--- /dev/null
+++ b/apps/sanity/extract.json
@@ -0,0 +1,15097 @@
+[
+ {
+ "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"
+ }
+ },
+ {
+ "name": "syndication",
+ "type": "document",
+ "attributes": {
+ "_id": {
+ "type": "objectAttribute",
+ "value": {
+ "type": "string"
+ }
+ },
+ "_type": {
+ "type": "objectAttribute",
+ "value": {
+ "type": "string",
+ "value": "syndication"
+ }
+ },
+ "_createdAt": {
+ "type": "objectAttribute",
+ "value": {
+ "type": "string"
+ }
+ },
+ "_updatedAt": {
+ "type": "objectAttribute",
+ "value": {
+ "type": "string"
+ }
+ },
+ "_rev": {
+ "type": "objectAttribute",
+ "value": {
+ "type": "string"
+ }
+ },
+ "target": {
+ "type": "objectAttribute",
+ "value": {
+ "type": "union",
+ "of": [
+ {
+ "type": "inline",
+ "name": "post.reference"
+ },
+ {
+ "type": "inline",
+ "name": "podcast.reference"
+ }
+ ]
+ },
+ "optional": true
+ },
+ "platform": {
+ "type": "objectAttribute",
+ "value": {
+ "type": "union",
+ "of": [
+ {
+ "type": "string",
+ "value": "devto"
+ },
+ {
+ "type": "string",
+ "value": "hashnode"
+ }
+ ]
+ },
+ "optional": true
+ },
+ "status": {
+ "type": "objectAttribute",
+ "value": {
+ "type": "union",
+ "of": [
+ {
+ "type": "string",
+ "value": "pending"
+ },
+ {
+ "type": "string",
+ "value": "published"
+ },
+ {
+ "type": "string",
+ "value": "simulated_complete"
+ },
+ {
+ "type": "string",
+ "value": "failed"
+ }
+ ]
+ },
+ "optional": true
+ },
+ "externalUrl": {
+ "type": "objectAttribute",
+ "value": {
+ "type": "string"
+ },
+ "optional": true
+ },
+ "externalId": {
+ "type": "objectAttribute",
+ "value": {
+ "type": "string"
+ },
+ "optional": true
+ },
+ "canonicalUrl": {
+ "type": "objectAttribute",
+ "value": {
+ "type": "string"
+ },
+ "optional": true
+ },
+ "syndicatedAt": {
+ "type": "objectAttribute",
+ "value": {
+ "type": "string"
+ },
+ "optional": true
+ },
+ "error": {
+ "type": "objectAttribute",
+ "value": {
+ "type": "string"
+ },
+ "optional": true
+ }
+ }
+ },
+ {
+ "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
+ }
+ }
+ }
+ },
+ {
+ "type": "type",
+ "name": "media.folder.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": "media.folder"
+ }
+ },
+ {
+ "name": "media.folder",
+ "type": "document",
+ "attributes": {
+ "_id": {
+ "type": "objectAttribute",
+ "value": {
+ "type": "string"
+ }
+ },
+ "_type": {
+ "type": "objectAttribute",
+ "value": {
+ "type": "string",
+ "value": "media.folder"
+ }
+ },
+ "_createdAt": {
+ "type": "objectAttribute",
+ "value": {
+ "type": "string"
+ }
+ },
+ "_updatedAt": {
+ "type": "objectAttribute",
+ "value": {
+ "type": "string"
+ }
+ },
+ "_rev": {
+ "type": "objectAttribute",
+ "value": {
+ "type": "string"
+ }
+ },
+ "name": {
+ "type": "objectAttribute",
+ "value": {
+ "type": "string"
+ },
+ "optional": true
+ },
+ "parent": {
+ "type": "objectAttribute",
+ "value": {
+ "type": "inline",
+ "name": "media.folder.reference"
+ },
+ "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/functions/syndicate-content/index.ts b/apps/sanity/functions/syndicate-content/index.ts
new file mode 100644
index 000000000..0c0f1bd3c
--- /dev/null
+++ b/apps/sanity/functions/syndicate-content/index.ts
@@ -0,0 +1,238 @@
+import { documentEventHandler } from "@sanity/functions";
+import { createClient } from "@sanity/client";
+
+interface ContentDocument {
+ _id: string;
+ _type: "post" | "podcast";
+ title?: string;
+ slug?: { current?: string };
+ excerpt?: string;
+ youtube?: string;
+ coverImage?: any;
+ content?: any[];
+ season?: number;
+}
+
+export const handler = documentEventHandler(
+ async ({ context, event }) => {
+ const { data } = event;
+ if (!data?._id || !data?._type) {
+ console.log("No valid document data in event, skipping syndication.");
+ return;
+ }
+
+ const dataset = context.clientOptions?.dataset || "dev";
+ const isProduction = dataset === "production";
+ const slug = data.slug?.current || data._id;
+ const canonicalUrl = isProduction
+ ? `https://codingcat.dev/${data._type}/${slug}`
+ : `https://dev.codingcat.dev/${data._type}/${slug}`;
+
+ const client = createClient({
+ ...context.clientOptions,
+ apiVersion: "2025-09-30",
+ });
+
+ const platforms: Array<"devto" | "hashnode"> = ["devto", "hashnode"];
+
+ console.log(
+ `[Syndicate Content] Processing ${data._type} (${data._id}) on dataset: ${dataset}. Production mode: ${isProduction}`,
+ );
+
+ for (const platform of platforms) {
+ const syndicationDocId = `syndication.${data._id}.${platform}`;
+
+ if (!isProduction) {
+ // Non-production (dev / preview): simulate and mark complete without outbound API calls
+ console.log(
+ `[Syndicate Content] Simulating syndication for ${platform} on ${data._id}`,
+ );
+
+ try {
+ await client.createOrReplace({
+ _id: syndicationDocId,
+ _type: "syndication",
+ target: {
+ _type: "reference",
+ _ref: data._id,
+ },
+ platform,
+ status: "simulated_complete",
+ canonicalUrl,
+ externalUrl: `https://${platform === "devto" ? "dev.to" : "hashnode.com"}/preview-simulated/${slug}`,
+ externalId: `simulated-${data._id}-${platform}`,
+ syndicatedAt: new Date().toISOString(),
+ error: undefined,
+ });
+
+ console.log(
+ `[Syndicate Content] Created simulated syndication document: ${syndicationDocId}`,
+ );
+ } catch (err) {
+ console.error(
+ `[Syndicate Content] Failed to create simulated syndication document for ${platform}:`,
+ err,
+ );
+ }
+ continue;
+ }
+
+ // Production mode: live outbound publishing
+ try {
+ if (platform === "devto") {
+ const apiKey = process.env.DEVTO_API_KEY || process.env.PRIVATE_DEVTO;
+ if (!apiKey) {
+ console.warn(
+ "[Syndicate Content] DEVTO_API_KEY not configured. Skipping Dev.to live dispatch.",
+ );
+ await client.createOrReplace({
+ _id: syndicationDocId,
+ _type: "syndication",
+ target: { _type: "reference", _ref: data._id },
+ platform: "devto",
+ status: "failed",
+ canonicalUrl,
+ error: "DEVTO_API_KEY environment variable is missing",
+ });
+ continue;
+ }
+
+ const devtoPayload = {
+ article: {
+ title: data.title || "Untitled",
+ published: true,
+ tags: ["webdev", "javascript", "beginners"],
+ canonical_url: canonicalUrl,
+ description: data.excerpt || "",
+ organization_id: "1009",
+ body_markdown: `Original: ${canonicalUrl}\n\n${data.excerpt || ""}`,
+ },
+ };
+
+ const res = await fetch("https://dev.to/api/articles", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "api-key": apiKey,
+ },
+ body: JSON.stringify(devtoPayload),
+ });
+
+ if (!res.ok) {
+ const errText = await res.text();
+ throw new Error(`Dev.to API error (${res.status}): ${errText}`);
+ }
+
+ const resJson = (await res.json()) as { url?: string; id?: number };
+
+ await client.createOrReplace({
+ _id: syndicationDocId,
+ _type: "syndication",
+ target: { _type: "reference", _ref: data._id },
+ platform: "devto",
+ status: "published",
+ canonicalUrl,
+ externalUrl: resJson.url,
+ externalId: String(resJson.id ?? ""),
+ syndicatedAt: new Date().toISOString(),
+ });
+ console.log(`[Syndicate Content] Successfully published to Dev.to: ${resJson.url}`);
+ }
+
+ if (platform === "hashnode") {
+ const token =
+ process.env.HASHNODE_ACCESS_TOKEN ||
+ process.env.PRIVATE_HASHNODE;
+ if (!token) {
+ console.warn(
+ "[Syndicate Content] HASHNODE_ACCESS_TOKEN not configured. Skipping Hashnode live dispatch.",
+ );
+ await client.createOrReplace({
+ _id: syndicationDocId,
+ _type: "syndication",
+ target: { _type: "reference", _ref: data._id },
+ platform: "hashnode",
+ status: "failed",
+ canonicalUrl,
+ error: "HASHNODE_ACCESS_TOKEN environment variable is missing",
+ });
+ continue;
+ }
+
+ const publicationId =
+ process.env.HASHNODE_PUBLICATION_ID || "60242f8180da6c44eadf775b";
+
+ const query = `
+ mutation PublishPost($input: PublishPostInput!) {
+ publishPost(input: $input) {
+ post {
+ id
+ url
+ }
+ }
+ }
+ `;
+
+ const res = await fetch("https://gql.hashnode.com", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: token,
+ },
+ body: JSON.stringify({
+ query,
+ variables: {
+ input: {
+ title: data.title || "Untitled",
+ publicationId,
+ contentMarkdown: `Original: ${canonicalUrl}\n\n${data.excerpt || ""}`,
+ originalArticleURL: canonicalUrl,
+ tags: [
+ { id: "56744721958ef13879b94cad", name: "JavaScript", slug: "javascript" },
+ { id: "56744722958ef13879b94f1b", name: "Web Development", slug: "web-development" },
+ ],
+ },
+ },
+ }),
+ });
+
+ const gqlData = (await res.json()) as any;
+ const publishedPost = gqlData?.data?.publishPost?.post;
+
+ if (!publishedPost) {
+ throw new Error(
+ `Hashnode publication failed: ${JSON.stringify(gqlData?.errors || "No post returned")}`,
+ );
+ }
+
+ await client.createOrReplace({
+ _id: syndicationDocId,
+ _type: "syndication",
+ target: { _type: "reference", _ref: data._id },
+ platform: "hashnode",
+ status: "published",
+ canonicalUrl,
+ externalUrl: publishedPost.url,
+ externalId: publishedPost.id,
+ syndicatedAt: new Date().toISOString(),
+ });
+ console.log(`[Syndicate Content] Successfully published to Hashnode: ${publishedPost.url}`);
+ }
+ } catch (publishErr) {
+ const errMsg =
+ publishErr instanceof Error ? publishErr.message : String(publishErr);
+ console.error(`[Syndicate Content] Error publishing to ${platform}:`, errMsg);
+
+ await client.createOrReplace({
+ _id: syndicationDocId,
+ _type: "syndication",
+ target: { _type: "reference", _ref: data._id },
+ platform,
+ status: "failed",
+ canonicalUrl,
+ error: errMsg,
+ });
+ }
+ }
+ },
+);
diff --git a/apps/sanity/package.json b/apps/sanity/package.json
index 9db0ee1e6..17b3d497f 100644
--- a/apps/sanity/package.json
+++ b/apps/sanity/package.json
@@ -6,17 +6,22 @@
"dev": "sanity dev",
"build": "sanity build",
"deploy": "sanity deploy",
- "typegen": "sanity schema extract --path=./extract.json && sanity typegen generate"
+ "blueprints:deploy": "sanity blueprints deploy",
+ "functions:dev": "sanity functions dev",
+ "typegen:extract": "sanity schema extract --path=./extract.json --workspace production --force",
+ "typegen": "pnpm run typegen:extract && sanity typegen generate"
},
"dependencies": {
- "@codingcatdev/sanity-plugin-podcast-rss": "^1.0.0",
- "@portabletext/block-tools": "^5.1.0",
- "@portabletext/react": "^6.0.3",
- "@sanity/assist": "^6.0.3",
- "@sanity/code-input": "^7.0.11",
- "@sanity/icons": "^3.7.4",
- "@sanity/ui": "^3.1.14",
- "@sanity/vision": "^5.17.1",
+ "@portabletext/block-tools": "^5.2.0",
+ "@portabletext/react": "^7.0.1",
+ "@sanity/assist": "^6.1.19",
+ "@sanity/blueprints": "^0.28.0",
+ "@sanity/client": "^8.9.0",
+ "@sanity/code-input": "^7.3.5",
+ "@sanity/functions": "^1.8.1",
+ "@sanity/icons": "^5.2.1",
+ "@sanity/ui": "^4.0.3",
+ "@sanity/vision": "^6.9.2",
"date-fns": "^4.1.0",
"lucide-react": "^0.577.0",
"micromark": "^4.0.2",
@@ -24,14 +29,18 @@
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-icons": "^5.6.0",
+ "react-inlinesvg": "^4.2.0",
"react-twitter-embed": "^4.0.4",
- "sanity": "^5.17.1",
- "sanity-plugin-media": "^4.1.1",
+ "sanity": "^6.9.2",
+ "sanity-plugin-media": "^6.1.4",
"styled-components": "^6.3.11"
},
"devDependencies": {
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"typescript": "^5.9.3"
+ },
+ "engines": {
+ "node": ">=22.12"
}
}
diff --git a/apps/sanity/plugins/podcast-rss/README.md b/apps/sanity/plugins/podcast-rss/README.md
new file mode 100644
index 000000000..f24dd0936
--- /dev/null
+++ b/apps/sanity/plugins/podcast-rss/README.md
@@ -0,0 +1,21 @@
+# podcast-rss (vendored)
+
+Vendored from `@codingcatdev/sanity-plugin-podcast-rss@1.0.0`.
+
+Vendored rather than consumed from npm because the published package declares
+`peerDependencies: { react: "^18", sanity: "^3" }` and carries `@sanity/ui@2`
+as a **direct** dependency. Under Studio v6 that would render v2 components
+inside the v4 `ThemeProvider`, handing them theme tokens they don't understand.
+
+This copy is not optional: `schemas/documents/podcast.ts` declares a field of
+`type: "podcastRssEpisode"`, which only this plugin registers. Disabling the
+plugin breaks schema extraction.
+
+Changes from upstream:
+- `Menu` / `MenuButton` / `MenuItem` import from `@sanity/ui/menu` (v4 moved them off the root entry)
+- `Stack`/`Select` `space=` -> `gap=` (v4 rename)
+- `xml2js` -> native `DOMParser` (xml2js is a Node lib that only worked via a Vite polyfill; Vite 8 drops it)
+- Controlled `I am hoping to make a Sanity.io plugin for builder. In doing so I thought I would write a little conversion tool before hand to figure out all the ins and outs of Builder.io and Sanity.io, so I thought I would share some of my findings. I will keep this tutorial updated as the plugin progresses.
The repo example has three things that you might find useful.
- Deleting All Data
- Creating all new types
- Adding all new Data
Repo for conversion example https://github.com/CodingCatDev/sanity-to-builder.git
Deleting All Data
https://github.com/CodingCatDev/sanity-to-builder/blob/84d8b5d436b2d674f916670317cb48de916c8a68/src/index.ts#L175
In order to delete all the data you need to first find all the models you want to delete
Get all Models from Builder (even unpublished)
In sanity you deal with things like draft. in documents. In Builder these items are just known to be unpublished version. So you need to have includeUnpublished=true in your API call. I looked for a while in the sdk and have not found this option so far.
You can handle this by using the fetch command instead of the sdk.
https://github.com/CodingCatDev/sanity-to-builder/blob/84d8b5d436b2d674f916670317cb48de916c8a68/src/config.ts#L134
Creating all new types
First you need to find all the types in your sanity.io database. I find it best to use Groq. So we can find all the types in Sanity and then create those in Builder as new Models.
Adding all new Data
Long Term goal
The End goal is to build a plugin for syncing your data...
Builder content link https://github.com/BuilderIO/builder/tree/main/plugins/contentful
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/building-a-web-application-with-next-js-and-firebase-firebase-summit-2021
+ ZKdo3vLUqcb4sYreiCMd4S
+ Thu, 02 Jun 2022 04:00:00 GMT
+
+ GitHub Repo
In order to clone this application locally run the below command. Please note you will need git
Unknown block type "code", specify a component for it in the `components.types` option
Firebase
Make sure to setup a new project with a Firestore database.
Install dependencies
Unknown block type "code", specify a component for it in the `components.types` option
Make sure you are first in the firebase directory
Unknown block type "code", specify a component for it in the `components.types` option
The video will cover how the different page types work for firebase.
Install dependencies
To get started make sure to
Unknown block type "code", specify a component for it in the `components.types` option
Run locally
Make sure to change the .env.local.template to .env.local and update the parameters from firebase.
To run the dev server (staying in the same directory)
Unknown block type "code", specify a component for it in the `components.types` option
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/lets-learn-svelte
+ ZKdo3vLUqcb4sYreiCMWe4
+ Wed, 01 Jun 2022 04:00:00 GMT
+
+ Learn how to build modern web applications with SvelteKit, a quickly growing framework for generating static web pages (SSG) as well as Server Side Rendered content (SSR). In this crash course, you'll learn all the basics including file-based routing, API routes, dynamic routes, global and scoped CSS, and much more!
Source Code
https://github.com/brittneypostma/sveltekit-starter
What is Svelte?
- Svelte is a component framework that allows you to break up your application into reusable chunks.
- Svelte is similar to React & Vue, with one key difference, no virtual DOM. Instead of letting the browser do the work, Svelte compiles itself away during the build process into vanilla html, css, and javascript.
- Easy global state management with Svelte stores.
Why SvelteKit?
- The goal with SvelteKit is to have a single recommended path for building everything from high performing web apps to static blog pages.
- With SvelteKit we get file-system based routing, optional rendering (SSR & SSG), code-splitting, CSS scoping by default, the ability to completely disable JavaScript and more.
Getting Started with a new SvelteKit app
Unknown block type "code", specify a component for it in the `components.types` option
At the time of recording, SvelteKit is in a release candidate phase, which means there are no more planned breaking changes.
Here we create a skeleton project without the TypeScript, ESLint, or Prettier configurations.
Unknown block type "code", specify a component for it in the `components.types` option
Aliases
SvelteKit with Vite provides the ability to setup aliases to folders within your app to make it easier to path to different areas. I have an article on how to set those up if you want to learn more.
Create the lib folder within the src folder and add a new file, header.svelte to it. Inside header.svelte, we are going to add a navigation bar for the site.
Unknown block type "code", specify a component for it in the `components.types` option
Now we need to create the routes that go to these respective links.
Routes
Inside the routes folder, we already have an +page.svelte file that matches to the home or / route. We need to add the /about route by creating a new folder named about and a new file inside +page.svelte. We will add the /blog route later since it is a dynamic route.
Layout
We don't want to have to copy and paste our header onto each page. SvelteKit provides a wrapper file named +layout.svelte that will wrap the entire application in whatever is in that file. Inside of the routes folder, create a new file named +layout.svelte and your app will disappear off your screen if you are running the dev server. This is because Svelte needs a <slot/> component to know where to insert the child content to. Add this code to your +layout.svelte file to see the header and content on the page.
Unknown block type "code", specify a component for it in the `components.types` option
Styling in Svelte
Styling in Svelte is scoped by default to each of the components. If you use an element selector like p inside a component, then it will only effect the p tags within that component. You can also use global styling by creating a global css file and importing it into the +layout.svelte file. Grab the global css I used https://github.com/brittneypostma/sveltekit-starter/blob/main/src/global.css and place it into the src folder. Then, import the file into +layout.svelte like this.
Unknown block type "code", specify a component for it in the `components.types` option
Now the app should start looking a little better.
SvelteKit Context Module
https://kit.svelte.dev/docs#ssr-and-javascript
SvelteKit provides us with different ways to render our application on a page by page basis, by default, it will be rendered on the server and sent to the client as HTML. For the home route, +page.svelte, we only need the content to be statically rendered. There will be no JavaScript added to the page at all. We can add this block to the top of the +page.js route to change it.
Unknown block type "code", specify a component for it in the `components.types` option
Counter component
Grab the counter component - https://github.com/brittneypostma/sveltekit-starter/blob/main/src/lib/counter.svelte and drop it into the lib folder. Inside the counter.svelte file, we can see one of the default animations that Svelte provides out of the box along with some of the sugar syntax Svelte has to handle JavaScript inside of your markup. Now, let's add it to the +page.svelte route to see it on the page.
Unknown block type "code", specify a component for it in the `components.types` option
Static assets
Add the static folder to the root of your application. By default, Svelte will path to that static folder, so you don't have to do relative pathing for assets in your app.
Class Directives and Page Store
Svelte will purge any CSS classes that are unused in your components and provides a $app/page store that allows you to tell which route you are on currently. Using this, let's finish out the header.svelte file.
Unknown block type "code", specify a component for it in the `components.types` option
Hydration and Routing
The browser store in the video is no longer correct. You can set router to false to enable this feature now. Also, hydrate has changed to a boolean instead of the environment.
SvelteKit includes a client-side router that will take control of user interactions like links and the browser back and forward buttons instead of allowing the browser to handle the navigation by reloading. This can be turned off to allow the browser to take control by setting export const router = false in the endpoint.
Hydration in SvelteKit typically hands off the server-rendered HTML to the client to implement any JavaScript needed on the page. If you don't need JavaScript for the page, you can turn off hydration by setting it to false
If both hydrate and router are false, there will not be any JavaScript added to your page at all.
Svelte provides a <svelte:head> component to include things like the title and social images in the head section of the html on the page level. These get injected at compile time. Add a new +page.js file inside of the about directory.
Unknown block type "code", specify a component for it in the `components.types` option
Dynamic Routes
Sometimes, we need a route that will catch more than one url. SvelteKit can do this by using square brackets around the parameter you want to catch, [params]. For our blog route, we can create a folder blog inside of routes. In the blog folder, we need an +page.svelte file that will match the /blog route. Then we need a folder inside blog called [slug] with square brackets around it, this means it is a dynamic route. Then inside create another +page.svelte file.
Load function
If a route needs to fetch data to render for the page or perform any logic around routing, you will need to use the load function inside of the +page.js file. The load function runs before a component is created. It runs during the server-side rendering and again in the client allowing you to get data for a page without showing a loading spinner or needing the onMount lifecycle function. We are going to fetch the data from the dev.to api to grab my posts available.
Unknown block type "code", specify a component for it in the `components.types` option
Props
We return the posts as props and then need to accept the props into the component in a normal script tag inside +page.svelte like this.
Unknown block type "code", specify a component for it in the `components.types` option
We can then finish off the markup.
Unknown block type "code", specify a component for it in the `components.types` option
Slug Route
In the dynamic route we created we need to do a similar load function, but also grab the params from the slug to create the correct route. Create a +page.js file inside the [slug] route. Then, we can use the page store provided in the load function to grab the params.
Unknown block type "code", specify a component for it in the `components.types` option
Grab our post prop.
Unknown block type "code", specify a component for it in the `components.types` option
And finally use the Svelte template syntax {@html} to parse the html onto the page.
Unknown block type "code", specify a component for it in the `components.types` option
That is it! We now have a working svelte blog that pulls in data from the dev.to api. Check out the docs on adapters to find out more on building and hosting your site.
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/aws-amplify-video
+ LzgcvqoNSwz5R8loyka0MD
+ Tue, 31 May 2022 04:00:00 GMT
+
+ AWS Account is required for this lesson. You may (and probably will) incur fees associated with these activities. Livestreaming is VERY expensive so be careful!
Startup
Please make sure that you see the prerequisites, the are Node, NPM and Git. https://docs.amplify.aws/start/getting-started/installation/q/integration/react
Installing Amplify
Make sure you install amplify globally. This will allow you to execute commands within your project.
Unknown block type "code", specify a component for it in the `components.types` option
Configuring Amplify with AWS
Run the below command to add configuration to for AWS.
Unknown block type "code", specify a component for it in the `components.types` option
You will need to finish adding the required role for your amplify CLI to connect with AWS..
Initializing Project
If you prefer to follow the guide see the link in the React section.
Unknown block type "code", specify a component for it in the `components.types` option
This is the setup that I provided for using VSCode and creating a React
Unknown block type "image", specify a component for it in the `components.types` option
React setup
I would recommend this guide for fully setting up a React project.
https://docs.amplify.aws/start/getting-started/setup/q/integration/react#initialize-a-new-backend
npx is an npm command that will fetch the necessary dependencies and create a React Application.
Unknown block type "code", specify a component for it in the `components.types` option
Amplify Add Video
At the time of writing this lesson there are two options that you can use for video, Livestream and Video on Demand (VOD). VOD is currently still in beta but it still produces a great streaming setup for VOD.
Livestream
I just want to repeat, Livestream is a cool experiement but SUPER expensive to run!
For adding Livestream you can accept all of the defaults (I used a custom name for the project).
Unknown block type "image", specify a component for it in the `components.types` option
Whenever you add new services to Amplify you then need to push them to AWS. Amplify uses Cloudformation to create all of the required services. The key item that is created for Livestream is AWS Elemental MediaLive.
Unknown block type "image", specify a component for it in the `components.types` option
You will be able to find a great amount of detail from this link https://aws.amazon.com/medialive/. This image represents exactly how we are utilizing the service. Our input source is OBS instead of a camera like depicted on the left.
Unknown block type "image", specify a component for it in the `components.types` option
Video on Demand (VOD)
Probably the best explantion for what is going to happen for this part of the lesson is described in the GitHub Issue for VOD. https://github.com/awslabs/amplify-video/issues/4
Unknown block type "image", specify a component for it in the `components.types` option
This is very similar to the larger image diagram that AWS has listed, however it is much simpler to follow.
https://aws.amazon.com/solutions/implementations/video-on-demand-on-aws/
Unknown block type "image", specify a component for it in the `components.types` option
Adding VOD to Amplify
We will first use amplify to add VOD by running the below command
Unknown block type "code", specify a component for it in the `components.types` option
In the video I choose to add the GraphQL option, but if you would rather keep things simple you can choose these options to ignore it.
Unknown block type "image", specify a component for it in the `components.types` option
Now that VOD resources have been added to the project you can push them to aws.
Unknown block type "code", specify a component for it in the `components.types` option
You should see a similar message (depending on the changes you have made it could add api).
Unknown block type "image", specify a component for it in the `components.types` option
Once this operation completes you should receive a message like below where your S3 buckets are listed for testing out the process. Select yes to overwrite, you will use this URL from the configuration in your React app.
Unknown block type "image", specify a component for it in the `components.types` option
Showing Videos In React
Now that you have locations for both Livestreaming and VOD, we can update App.js to include a package called video.js.
Unknown block type "code", specify a component for it in the `components.types` option
Once this package is installed we can use it within our React app.
Unknown block type "code", specify a component for it in the `components.types` option
Above you will notice two configruations, videoJsOptions for our Livestream and videOnDemanJsOptions for our VOD. Because I was manually uploading a test file called mediaConvertTest I hardcoded this file directly in the VOD.I have added this file to the GitHub repo for your testing as well, it is located here: TestVideo/mediaConvertTest.mov
Running VOD conversion
In order to start the process of running Media Convert you will need to upload the test file to your S3 bucket it should have the word input within S3.
Unknown block type "image", specify a component for it in the `components.types` option
It will then trigger a Lambda to start Media Convert, you can see what events are subscribed on the properties tab in S3.
Unknown block type "image", specify a component for it in the `components.types` option
This Lambda should have the [yourprojectname]-dev-inputWatcher
Unknown block type "image", specify a component for it in the `components.types` option
You can see in the image above how the trigger is creating a new MediaConvert Job. Below is the full code for the Lambda.
Unknown block type "code", specify a component for it in the `components.types` option
Within Media Convert you should see a job has started, and hopefully completed.
Unknown block type "image", specify a component for it in the `components.types` option
If you select one of those jobs you will find the input and outputs in the summary.
Unknown block type "image", specify a component for it in the `components.types` option
Now you will find that exact structure in your output S3 bucket.
Unknown block type "image", specify a component for it in the `components.types` option
If you open that "folder" you can see that there are several files listed that the conversion created.
Unknown block type "image", specify a component for it in the `components.types` option
The URL to the top one is what we will be using for the example.
Showing VOD
Now that you have a VOD file in S3 you will view this file being served through Cloud Front which is Amazon's Global CDN. The URL can be found in src/aws-video-exports.js. With the full solution in the App.js file for React you will now see the VOD file playing (you may need to refresh).
Unknown block type "image", specify a component for it in the `components.types` option
Final Thoughts
I have a message out to the labs team on GitHub about the next triggers. I believe what they are trying to accomplish next is to take the Lambda form the output file and store all of the meta-data into DynamoDB which can then be served through our GraphQL API. I know this is for sure possible and if I don't hear back I will implement my own for the full app. So far this beta version is really awesome though!
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/send-gcp-build-status-to-discord
+ ZKdo3vLUqcb4sYreiCMU4i
+ Mon, 30 May 2022 04:00:00 GMT
+
+ Creating a new project
You can choose to pick any type of project that you would like, for our example I choose a create-react-app (CRA). This way we still have options for running something within npm. However you could just as easily use this guide for Angular, Vue etc. You could even just write a text file and no site at all. I thought it would lend well to my next lesson of deploying the a storage bucket and CDN.You can create a new project by running and open in VS Code
Unknown block type "code", specify a component for it in the `components.types` option
You will need NPM (which comes with Node), create-react-app, and firebase-tools installed globally for this project. You will also need to add billing (with Credit Card), although you should not be billed at all for the project. https://nodejs.org/en/download/ https://reactjs.org/docs/create-a-new-react-app.html https://firebase.google.com/docs/cli
How to run build locally
The goal is to see a real life example for using Google Cloud build. In order to do this we will run the production build of CRA.https://create-react-app.dev/docs/production-build/After running npm build command in the root of your project you will see a folder created like below.
Unknown block type "image", specify a component for it in the `components.types` option
Setting Up GCP Project
Now it is time to start creating a new GCP project https://console.cloud.google.com/. If you already have an acccount you can skip to the existing users section.
If you do not have an account you will be presented with the screen below. You must agree to the Terms of the platform and APIs.
Unknown block type "image", specify a component for it in the `components.types` option
I would recommend trying out the 12 months $300 free trial so you can checkout everything that GCP has to offer.
Unknown block type "image", specify a component for it in the `components.types` option
To start a project you can now follow the existing Users guide.
Existing Users
Click on Select a project from the toolbar at the top of the screen.
Unknown block type "image", specify a component for it in the `components.types` option
Name your new project something meaningful, for example gcp-build-discord.
Unknown block type "image", specify a component for it in the `components.types` option
Setting Up a GitHub Repo
Making an assumption that you have created a GitHub account before. https://github.com/join
You should see a "New" button inside of your home page like below.
Unknown block type "image", specify a component for it in the `components.types` option
Enter a Repo name that will work for you like below, remember this as we will use it within our Google Platform Cloud Build.
Unknown block type "image", specify a component for it in the `components.types` option
Now you can run the below command in your project to add GitHub as your upstream remote.
Unknown block type "code", specify a component for it in the `components.types` option
Connecting GitHub to Cloud Build
If this is your first time using Cloud Build, you will need to enable the API. Please note the fees related if you start to use 2 hours of build time in a given day (I know a lot).
New Users
Unknown block type "image", specify a component for it in the `components.types` option
Unknown block type "image", specify a component for it in the `components.types` option
Unknown block type "image", specify a component for it in the `components.types` option
Follow the next prompts to setup your billing with Credit Card.
Existing Users
You will receive a prompt to Enable Cloud Build API, if you receive a failure make sure to add billing to your account.
Unknown block type "image", specify a component for it in the `components.types` option
Now you can connect you GitHub repository.
Unknown block type "image", specify a component for it in the `components.types` option
Select the GitHub Source
Unknown block type "image", specify a component for it in the `components.types` option
Once you authenticate you will see all of your repositories listed select the one you want to connect with our trigger, for example gcp-build-discord-webhook.
Unknown block type "image", specify a component for it in the `components.types` option
After you connect you will see that a push default push trigger can be setup, go ahead and create the default push trigger.
Unknown block type "image", specify a component for it in the `components.types` option
You will see a screen similar to below showing the new trigger, you can get into the details of this trigger further, but for now it is enough to trigger on any git push that we run.
Unknown block type "image", specify a component for it in the `components.types` option
Cloud Build YAML
For our example react project we have two phases that are going to trigger
npm install - which will install all of our dependenciesnpm run build - which will create our production build folder
This file will be located in the root of your project and called cloudbuild.yaml.
Unknown block type "code", specify a component for it in the `components.types` option
Once you add this you can now run the below command to push our new commit.
If you like to use VSCode to push instead like below.
Unknown block type "image", specify a component for it in the `components.types` option
Back in Google Cloud Console https://console.cloud.google.com you should see a newly triggered build, like below.
Unknown block type "image", specify a component for it in the `components.types` option
Pub/Sub Topic Creation
You can look for Pub/Sub under Bit Data, otherwise you can simply search for it.
Unknown block type "image", specify a component for it in the `components.types` option
Select Create Topic so we can create a new cloud-builds topic that Google Cloud Build triggers during each job. Please note the topic must be exactly this name! Here you can find more details https://cloud.google.com/cloud-build/docs/send-build-notifications
Unknown block type "image", specify a component for it in the `components.types` option
Initialize Firebase
If you haven't installed firebase-tools, please do that before run this section https://firebase.google.com/docs/cli
Within the root of your project you can run the command firebase init to setup Firebase Cloud Functions. Select Functions by hitting the space bar, then hit enter to continue.
Unknown block type "image", specify a component for it in the `components.types` option
We will be using TypeScript in our example, but you could also choose JavaScript if you have done this type of thing and prefer it.
Unknown block type "image", specify a component for it in the `components.types` option
Creating Firebase Cloud Function for Pub/Sub
In your root folder you will find a new directory called functions. Navigate to functions/src/index.ts file and uncomment the default helloWorld function and we will push this as well just to see the difference on functions. In the video I walk through how you can use Pub/Sub to walk through console logging and other options in your build before running the actual function. Here I will just add the final code code so that you can implement and try it directly.Within the functions/src directory add a new directory and file with the final path beingfunctions/src/DiscordBuildHooks/DiscordBuildHooks.ts
Unknown block type "code", specify a component for it in the `components.types` option
In order for your function to be exposed we need to add this to your index.ts file as well with the final code looking like below.
Unknown block type "code", specify a component for it in the `components.types` option
Final folder structure
Unknown block type "image", specify a component for it in the `components.types` option
Before deploying this function we will need to setup a Discord Web hook and add it to our firebase configuration.
Discord Webhook
You must register on Discord and have rights to manage web hooks for this part.
Select a given channel that you want to create a hook in and go to settings, by clicking the gear icon.
Unknown block type "image", specify a component for it in the `components.types` option
Next navigate to Webhook and click Create Webhook.
Unknown block type "image", specify a component for it in the `components.types` option
You will then need to copy the web hook url so we can add it to our Firebase Cloud Function configuration.
Unknown block type "image", specify a component for it in the `components.types` option
Make sure you keep all of your web hooks private!
Firebase Cloud Function Config
In the video we go into greater detail if you would like to know more about adding a configuration to Firebase Cloud Functions. You may have noticed in the above code this line functions.config().discord.build_hook . This is how Firebase Functions can read configurations. You are going to set this configuration with your Discord Webhook URL.The reason we don't just hardcode this is that we need to keep it private and out of our git repo. There is an argument that this is not the safest place to keep secrets and if you want to take this a step further look into https://cloud.google.com/kms
Deploy Firebase Cloud Functions
Now that you have your configuration set and all of the code you will need it is time to deploy your functions.
Unknown block type "code", specify a component for it in the `components.types` option
If you find later you are working on front end hosting and want to only deploy functions you can run the below command.
Firebase Console
You can now see your functions listed inside of the Firebase Console.
Unknown block type "image", specify a component for it in the `components.types` option
If you want to see if your Cloud Functions are working correctly you can trigger your helloWorld function by entering the url into a browser, here is an example.
Trigger Your Build
It is now time to try building your project to see if our full flow is working as expectedGitHub->Google Cloud Build->Pub/Sub->Firebase Functions->DiscordSo commit and push all of your changes.
You will see the below messages in Discord (notice the bugs, fix them :D)
Unknown block type "image", specify a component for it in the `components.types` option
Failure tests
- Try changing the name of your
cloudbuild.yaml file to failurecloudbuild.yaml and see if it fails. - Putting back
cloudbuild.yaml if you changed it above. Now inside of cloudbuild.yaml you can change the arguments to something npm doesn't understand like 'alex'. This will trigger a failure build within a step and send it to Discord.
This should trigger the two messages below in Discord
Unknown block type "image", specify a component for it in the `components.types` option
Trouble Shooting
If you find something is wrong with your function check the logs, you might find some bugs I missed.
Unknown block type "image", specify a component for it in the `components.types` option
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/backup-sanity-with-cloud-firestore
+ LzgcvqoNSwz5R8loykZzj1
+ Sun, 29 May 2022 04:00:00 GMT
+
+ New Firebase Project
In order to accept webhooks from Sanity you will need to have a firebase project setup, you can search this site for many examples. But most simply run firebase init on your command line and select Functions and Firestore, you can keep all other defaults.
Unknown block type "image", specify a component for it in the `components.types` option
After selecting an existing or creating a new project make sure to select TypeScript
Unknown block type "image", specify a component for it in the `components.types` option
Cloud Firestore
If you run into any issues on the command line, you can open the console and validate your Firestore database has been created.
The main thing that needs to happen in firestore is to make sure that your rules will allow you to write to the database. Make sure in firestore.rules, you have something similar to this based on the content that you are bringing over. Realisticly we are using the the admin functions so you don't need these rules to write, but it is nice to remember if you are getting blocked in your site for read access.
Unknown block type "code", specify a component for it in the `components.types` option
Firebase Cloud Function WebHook
Within the directory functions->src_index.ts you can now update this file with the below code. Making sure to replace 'YOURKEY' with the key from your sanity project id found in settings.
Unknown block type "code", specify a component for it in the `components.types` option
Once this is complete you can run command firebase deploy.
You can then go to the Firebase Console and copy your url, you will need this later for the sanity webhook url.
Unknown block type "image", specify a component for it in the `components.types` option
Sanity Setup
In your Sanity Console you can edit webhooks in your settings https://manage.sanity.io/projects/<your-projectid>/settings/api
You can then add your Firebase Cloud Function URL and select the dataset you would like to get updates from.
Unknown block type "image", specify a component for it in the `components.types` option
Now anytime you make a change in your
Sanity Pro Tip
If you already have data in Sanity and would like to trigger all of your documents to update you can export and then import your entire dataset. Run the below commands inside of a project running sanity.
Unknown block type "code", specify a component for it in the `components.types` option
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/git-your-build-system-right
+ ZKdo3vLUqcb4sYreiCMa0S
+ Sat, 28 May 2022 04:00:00 GMT
+
+ This lesson is on setting up CI (Continuous Integration) and CD (Continuous Delivery) across four of the most popular systems today Google Cloud Platform Cloud Build, Azure Pipelines, AWS Code Pipelines, and GitHub Actions. We will be using a simple Angular application (although you could replace this with almost any app) and using GitHub as a source repository. We will then create triggers from git branch commits to launch the processes.
Please note you will need to create a GitHub repo, which is not covered in this lesson.
Git Flow!
Unknown block type "image", specify a component for it in the `components.types` option
If you have been slapping keys for a long time now and have not worked on a large project, you are probably used to just committing to the master branch and calling it a day. However on large teams/projects most follow the Gitflow workflow. Atlassian does a nice job on explaining what exactly this workflow looks like.
The reason I suggest giving this a good read is both to advance your career on all project types and we will utilizing this flow in our CI/CD method. Albeit a very light version where I will create release branches for each CI/CD type merging in from a develop branch.
Unknown block type "image", specify a component for it in the `components.types` option
Google Cloud Platform - Cloud Build
You will need an account setup on GCP for this tutorial https://cloud.google.com/gcp/
I have been a user of GCP for several years now and what I love about GCP CloudBuild is that you can build just aboutanything! This is due in large part to the fact that Cloud Build uses Docker images to create It offers 120 free build-minutes per day and up to 10 concurrent builds. This works especially well if you work with mono repos, having to build several projects at the same time. Admittedly I usually use Firebase to serve all of my static sites, but to keep things similar across all of the pipelines you will utilize CloudStorage.
Create New Project
Go to GCPConsole and create a newproject.
Unknown block type "image", specify a component for it in the `components.types` option
If you have multiple projects you will have to select the newly created project.
Unknown block type "image", specify a component for it in the `components.types` option
Unknown block type "image", specify a component for it in the `components.types` option
In order to use several of the features we will require, you will need to also enable billing
https://console.cloud.google.com/billing/linkedaccount.
Remember that unless you use over 120 minutes to run your builds this will remain free. I won't walk through all of this, but you can find more details here.
Create Cloud Build Trigger
You may need to scroll down in the menu drawer to findthe Tools heading. This is where Cloud Build will be located. Hover over Cloud Build and click on Triggers https://console.cloud.google.com/cloud-build/triggers
Unknown block type "image", specify a component for it in the `components.types` option
You will need to Enable the Cloud Build API in order to use triggers.
Unknown block type "image", specify a component for it in the `components.types` option
Once the enabling is complete you can then connect our prior created GitHub repository.
Unknown block type "image", specify a component for it in the `components.types` option
Select your source
Unknown block type "image", specify a component for it in the `components.types` option
You will then need to authenticate and provide GCP with access to your repository, once this is complete you will see a list of all your repositories and you will need to find the one associated with this example like below.
Unknown block type "image", specify a component for it in the `components.types` option
You will then receive a prompt to create a push trigger for this repository, click Create push trigger.!
By default this will setup a trigger for every commit, for every branch. Trying to stick with the release methodology from the above Gitflow workflow, you will only build this when the gcp branch has been committed.
Unknown block type "image", specify a component for it in the `components.types` option
Open this trigger and update it just like below, by selecting the branch gcp for builds. You will also leverage a new file named cloudbuild.yaml, more on this in a later step.
Unknown block type "image", specify a component for it in the `components.types` option
Pro tip: if you are missing branches as you type, you might not have pushed them all to GitHub, you can do this by running the command git push --all origin
Set up Storage Bucket
We need a place for our Angular project to be hosted, and on GCP I would typically recommend Firebase like in our Google Cloud Repository Lesson for CI/CD.
As I said before you will use a storage bucket, similar to other cloud hosting providers.
Unknown block type "image", specify a component for it in the `components.types` option
Unknown block type "image", specify a component for it in the `components.types` option
You will then need to give your bucket a unique name, choose Multi-region, Standard storage class, and Fine-grained Access control and then create your bucket. This bucket is currently only accessible by select roles (members), you will need to give read all access by selecting Add members. Add new members called allUsers and the role Storage Legacy Object Reader.
Unknown block type "image", specify a component for it in the `components.types` option
You should see a warning This bucket is public and can be accessed by anyone on the internet. To remove public access, remove allUsers and allAuthenticatedUsers from the bucket's members. This is exactly what you want so that the bucket is available to the internet.
Unknown block type "image", specify a component for it in the `components.types` option
Storage Bucket Inital
I always like to build angular locally once and upload the current dist/ folder to the bucket to see if it works. Make sure that in your src/index.html you update your base href to match your new bucket. If you upload your files an easy way to find this url is by selecting the chain link icon in the index.html row.
Unknown block type "image", specify a component for it in the `components.types` option
Here is an example of the failure without updating your baseref, notice all the 404 missing files.
Unknown block type "image", specify a component for it in the `components.types` option
Update your html to include this new base href.
Unknown block type "code", specify a component for it in the `components.types` option
Make sure to build locally again to see this working.
npm run build
Once again remove all the files from your storage bucket and re-add them, you should see the default Angular app up and running.
Unknown block type "image", specify a component for it in the `components.types` option
Build Setup
Now that you have setup your build trigger, the build needs to do something once this trigger is executed. You will create a cloudbuild.yaml file in the root of your project that will tell the build how to execute. GCP uses Docker images to facilitate environments to build. There are several popular images that are created and maintained directly by Google, and a number more that are communitymaintained.
In order for you to gather all the appropriate packages and use the Angular CLI to create a production build, you will use an npmimage in your build file.
Installing Dependencies`cloudbuild.yaml
Unknown block type "code", specify a component for it in the `components.types` option
BucketIn order to find your Storage Bucket Link for Gsutil, open your bucket and select tab for Overview.
Unknown block type "image", specify a component for it in the `components.types` option
Replace the below example gs://git-ci-cd with your own bucket link.
cloudbuild.yaml
Unknown block type "code", specify a component for it in the `components.types` option
Git Commit and Trigger History
Now anytime you commit to your gcp branch, you will see the trigger execute and all of the history can be found within Cloud Build History.
Unknown block type "image", specify a component for it in the `components.types` option
Following our Git Flow now lets update our development branch to change our app.component.html file to show a cool SVG of this lesson flowing to
GCP.app.component.html
Now you can push your develop branch and have it updated
Unknown block type "code", specify a component for it in the `components.types` option
To Trigger our build we would like to have this merged to gcp, you might get a prompt to update the merge message.
Unknown block type "code", specify a component for it in the `components.types` option
You should see in the Build History that you now have a build active
Unknown block type "image", specify a component for it in the `components.types` option
You can also watch all of the Logs to see what is happening or if there are any errors.
Unknown block type "image", specify a component for it in the `components.types` option
If the build is successful, you can visit the site and see our newly updated screen.
Unknown block type "image", specify a component for it in the `components.types` option
Azure - Pipelines
You will need to create a free microsoft account to build using Azure Pipelines https://azure.microsoft.com/en-us/free/
Create A Project
This project can do everything that you might require, from agile project management, to code creation and deployment. In this demo we are only going to brush the surface of what is possible, please dig into the docs to learn more.
Unknown block type "image", specify a component for it in the `components.types` option
To start select Pipelines from the menu.
Unknown block type "image", specify a component for it in the `components.types` option
Then click create Create Pipeline button to begin creating our first pipeline.
Unknown block type "image", specify a component for it in the `components.types` option
Our code is still hosted on GitHub so we are going to select that option from the menu. You will then need to login to GitHub to grant authorization to Azure Pipelines.
Unknown block type "image", specify a component for it in the `components.types` option
If you have several GitHub repositories, you may need to filter to find the correct one. Once you have done this click on your repository and continue with teh Installation of Azure Pipelines within GitHub. You should see another prompt to allow access to the repository that you had already selected within Azure.
Unknown block type "image", specify a component for it in the `components.types` option
Once this process is complete you will be brought back into Azure DevOps and you can continue configuring your pipeline. Select the appropriate pipeline for your codebase. For your example select Node.js with Angular.
Unknown block type "image", specify a component for it in the `components.types` option
This is a very user friendly experience as it automatically creates an azure-pipelines.yml file for you and commits this to your repository.
Unknown block type "image", specify a component for it in the `components.types` option
Select the Save and Run button and you## Amazon Web Services - Code Pipeline
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/angular-cli-deploying
+ LzgcvqoNSwz5R8loyka0bt
+ Fri, 27 May 2022 04:00:00 GMT
+
+ Deploying Fast
So you have a conference or perhaps a JAMStackGR meeting that you created an Angular app for and need to show it to the world. How do you deploy this out to a CDN (Content Delivery Network) so that the world can access your new app? There are many ways that you can deploy your site, but one of note is the Angular CLI, it allows you to deploy to 6 different platforms.
First lets make sure you have already covered a couple of things.
- Create app command
ng new - Serve command
ng serve - Test full build using
ng build --prod command locally and lite-server and try out on local browser
Create Your App
We are keeping this one simple and running the default Angular New command, pick a name and then answer yes to everything
Unknown block type "image", specify a component for it in the `components.types` option
Serve Locally
We are going to make one simple change and use a different default image on our app. To do this we are going to open VSCode (Visual Studio Code).
Now that VSCode is open add your favorite image to the src/assets/ so that we can access it within our app.
You can do this in your favorite terminal or within VSCode ctrl+~ will open the terminal within VSCode
First try out the default application by running the below command.
Unknown block type "code", specify a component for it in the `components.types` option
Unknown block type "image", specify a component for it in the `components.types` option
Open the browser to see the default Angular Application
Now we will remove all of this and just put our picture on the page. There should be a bunch of html in between the two comments below. You can remove all of this!
app.component.html
Unknown block type "code", specify a component for it in the `components.types` option
When angular serves the asset folder you can find the above added picture in the directory /assets/. We will wrap our picture in a simple flex layout and add leave the <router-outlet></router-outlet> in case this app gets modules added later.
Unknown block type "code", specify a component for it in the `components.types` option
Unknown block type "image", specify a component for it in the `components.types` option
Production Build
Once your picture is showing in the browser this means that we are ready to make a production version of our application. To do this we are going to execute the below command.
Once this completes it will have compiled the complete app and you will find it in a new folder within the dist/ directory. This is the full application and at this point we can serve this locally with a server. A simple one that I like to use is lite-server which is also recommended in the Angular deploy documentation, I like to install this globally.
An example to serve from our base directory is the command below
Unknown block type "image", specify a component for it in the `components.types` option
code
You can see the production site on
https://localhost:3000
This might seem the same as using ng serve, but this is running the production code!
Angular Deploy Platforms
The Angular CLI command ng deploy (introduced in version 8.3.0) executes the deploy CLI builder associated with your project. A number of third-party builders implement deployment capabilities to different platforms. You can add any of them to your project by running ng add [package name]. ~ Angular Guide
Please also note that I am in now way suggesting you should use the quick deploy methods with public repos (as there are keys all over) or that they are ready for true production use. This method is best used as demos.
If you see any 404 issues you need to update the base html element, similar to <basehref="/JAMStackGR-Deploy-v-GIT/">
Firebase Hosting
Firebase Hosting is hosting on CDN edge servers around the world. There are several products in the Firebase suite including Firestore which offers a Backend as a Service. You will need a firebase account (google user account) in order to use this service, and a firebase project created.
Creating a Firebase project https://console.firebase.google.com/ login and select Add Project, give it a unique name.
Deploy command
This command has added the correct npm packages
Login using the CLI, this will direct you to login so that the CLI can be granted to act on your behalf.
Unknown block type "image", specify a component for it in the `components.types` option
You will then need to copy the login token so that you can paste it back in the CLI.
Unknown block type "image", specify a component for it in the `components.types` option
Paste the code like below
Unknown block type "image", specify a component for it in the `components.types` option
Now that the CLI can access your projects it will have them in a list. Select your Firebase project that you have created.
Unknown block type "image", specify a component for it in the `components.types` option
If you take a look at angular.json you will now see that there is an entry for this deploy option listed
Unknown block type "code", specify a component for it in the `components.types` option
The CLI also created some default firebase files in your project .firebaserc and firebase.json. See below.
Unknown block type "image", specify a component for it in the `components.types` option
Now that everything is setup you can run the deploy command below
Unknown block type "code", specify a component for it in the `components.types` option
This will build your project and deploy it to Firebase Hosting
Unknown block type "image", specify a component for it in the `components.types` option
Unknown block type "image", specify a component for it in the `components.types` option
You will need a free Microsoft or GithHub login and Azure Dev Ops account to use this deploy method.
Run the below command
Unknown block type "code", specify a component for it in the `components.types` option
This has added the Azure package to npm dependencies "@azure/ng-deploy": "^0.2.3",
This will initiate the login flow. You will need to follow the link in the console.
Unknown block type "image", specify a component for it in the `components.types` option
Copy the code and paste into the browser.
Unknown block type "image", specify a component for it in the `components.types` option
You will then login to your Microsoft account. And you will receive a message about Microsoft Cross Platfrom CLI Interface.
Unknown block type "image", specify a component for it in the `components.types` option
You should now have a list in the CLI asking "Under which subscription should we put this static site?". Pick the one you would like to use (maybe you only have one). This will once again build out the site and get it ready for deploying.
Unknown block type "image", specify a component for it in the `components.types` option
Something interesting that I found during this process is that Angular overwrites the deploy builder command with Azure. This also adds azure.json file.
Unknown block type "image", specify a component for it in the `components.types` option
azure.json
Unknown block type "code", specify a component for it in the `components.types` option
angular.json
Unknown block type "code", specify a component for it in the `components.types` option
Run the deploy command for Azure
Unknown block type "code", specify a component for it in the `components.types` option
Unknown block type "image", specify a component for it in the `components.types` option
Angular CLI creates a Storage Account and Resource Group Automatically as you can see from the above azure.json.
Unknown block type "image", specify a component for it in the `components.types` option
If your unfamiliar with Azure Storage accounts you can think of them as a container to put just about anything you need to store. Items like blobs, file shares, tables, and queues. For this deploy a Blob Container is used.
Unknown block type "image", specify a component for it in the `components.types` option
Now in order to access that container a static site is also created. This can be though of as the equivalent to running lite-server locally. It is pointed at index.html.
Unknown block type "image", specify a component for it in the `components.types` option
I haven’t confirmed this yet but it appears that you DO NOT get a full CDN from this deployment. You would have to configure this yourself under the Azure CDN option.
Unknown block type "image", specify a component for it in the `components.types` option
Demo site: https://jamstackgrdeployvgstatic.z22.web.core.windows.net/
Zeit Now Hosting
Zeit does not need an account created prior to deploying, you will need access to an email that you will provide to verify login.
Run the below commands
Unknown block type "code", specify a component for it in the `components.types` option
This has added the correct npm packages for deploy "@zeit/ng-deploy": "^0.3.0",
Instead of using a browser Zeit uses an email flow for login (well technically a browser as well). So you click a magic link like below to start the login flow.
Unknown block type "image", specify a component for it in the `components.types` option
Once you select the link this will verify your email address and finish the setup.
Unknown block type "image", specify a component for it in the `components.types` option
Deploy the site to Zeit Now hosting, using the command below.
Unknown block type "code", specify a component for it in the `components.types` option
If you see an error similar to below
Unknown block type "image", specify a component for it in the `components.types` option
You will need to install now-client manually, then try the deploy command again like below.
Unknown block type "code", specify a component for it in the `components.types` option
I have raised an issue about this and it might be resolved later https://github.com/zeit/ng-deploy-now/issues/7
If you visit this base directory you will see your project listings just like in the dist folder, you can update your angular builders to update how you would like this handled (for instance just pushing your main project with index.html).
Below you will find the deploy activity to your account.
Unknown block type "image", specify a component for it in the `components.types` option
Netlify
For Netlify you will need to setup an account ahead of running this command. They accept GitHub, GitLab, Bitbucket or Email. You will also need to create a site so that you can get an API key.
Run the below command
ng add @netlify-builder/deploy
You will then be asked for the API Key of your site.
Unknown block type "image", specify a component for it in the `components.types` option
Login to Netlify with one of the above methods, if this is your first time you will see a page like below where you can create a new site. Netlify tries to keep you in the Git methodology but you can create a site without connecting a Git repo. For this drop our dist folder out here. Yes this seems odd as we are really trying to use the CLI to deploy (well I agree). However, because they require a folder with an index file this seems easiest for now. If you have not run that command yet (or deleted the folder) just run ng build --prod.
Unknown block type "image", specify a component for it in the `components.types` option
Once this happens you will have a new site created and already deployed, with a auto generated name associated to a new netlify URI.
Unknown block type "image", specify a component for it in the `components.types` option
Now technically you could stop here and your site would be fully published, but the idea behind this is to setup a way to quickly deploy any changes. So now we can get back to the CLI questions. In this example we now provide the API ID into the CLI, don't confuse this with the Site Name. This can be found under settings->general, our example would be 7b9f51c1-2296-408c-a208-939892c80dde
Unknown block type "image", specify a component for it in the `components.types` option
You will then need the Personal Access Tokens of Netlify Account, which can be found in User Settings, which is in the dopdown by your initial. Then select the tab for Applications. https://app.netlify.com/user/applications
Unknown block type "image", specify a component for it in the `components.types` option
Once you are here locate the button New access Token, under Peronal access tokens.
Unknown block type "image", specify a component for it in the `components.types` option
Provide a meaningful name like Angular CLI.
Unknown block type "image", specify a component for it in the `components.types` option
Once this is complete you will need to copy the token that has been created and paste this into the CLI where it asks for your Personal Access Tokens of Netlify Account.
Unknown block type "image", specify a component for it in the `components.types` option
The result should look something like below.
Unknown block type "image", specify a component for it in the `components.types` option
package.json
Unknown block type "code", specify a component for it in the `components.types` option
angular.json
Unknown block type "code", specify a component for it in the `components.types` option
Now to deploy from the CLI you can run the below command:
Unknown block type "code", specify a component for it in the `components.types` option
Resulting in the build of the site and the upload of our dist folder.
Unknown block type "image", specify a component for it in the `components.types` option
If you need to ever look at your deploys they will all be listed under the Deploys tab within the site
Unknown block type "image", specify a component for it in the `components.types` option
GitHub Pages Hosting
In order to use GitHub pages you must have a GitHub account and setup the project with an origin.
Run the below command
Unknown block type "code", specify a component for it in the `components.types` option
This will install the correct npm package in package.json
Unknown block type "code", specify a component for it in the `components.types` option
This will also adjust the angular.json file to include the below
Unknown block type "code", specify a component for it in the `components.types` option
Before we move on to deploying we must establish a GitHub Project on https://github.com/new. Enter a unique name for your repository. You don't need to update anything else.
Unknown block type "image", specify a component for it in the `components.types` option
You should see a command similar to the one below listed under "…or push an existing repository from the command line". Copy this and execute within your projects directory.
Unknown block type "image", specify a component for it in the `components.types` option
Unknown block type "code", specify a component for it in the `components.types` option
The result should show that you now have an associated remote branch and pushed all of the objects up to your GitHub site.
Unknown block type "image", specify a component for it in the `components.types` option
If you now refresh the page above you will not see commands on how to start setting up your repo, you will see the actual source code.
Unknown block type "image", specify a component for it in the `components.types` option
Now that we have made a connection to GitHub we can run our deploy command below.
Please make sure to read https://www.npmjs.com/package/angular-cli-ghpages#--base-href without this loaded files will not be found and show a 404.
ng deploy --base-href=/JAMStackGR-Deploy-v-GIT-test/
This command created a new branch in our repo called gh-pages .
Unknown block type "image", specify a component for it in the `components.types` option
It then built our dist folder and uploaded it to this branch.
Unknown block type "image", specify a component for it in the `components.types` option
You can select the environment tab to see your deployments.
Unknown block type "image", specify a component for it in the `components.types` option
Select view deployment to access your new site
Unknown block type "image", specify a component for it in the `components.types` option
Demo Site: https://ajonp.github.io/JAMStackGR-Deploy-v-GIT-test/
NPM Package Publish
This is not really deploying a web app, but I wanted to cover all the current ng deploys for an example This example is on a seperate branch in the repo https://github.com/ajonp/JAMStackGR-Deploy-v-GIT-test/tree/npm-deploy
Unknown block type "code", specify a component for it in the `components.types` option
Give your library a name like jamstackgr2-library
Unknown block type "image", specify a component for it in the `components.types` option
You will then need to build this library, you will find this in the dist folder.
Unknown block type "code", specify a component for it in the `components.types` option
Unknown block type "image", specify a component for it in the `components.types` option
You will need to login to NPM for this next part
Unknown block type "image", specify a component for it in the `components.types` option
Now we are prepped for running the angular command for deployment!
Full angular.json
Unknown block type "code", specify a component for it in the `components.types` option
Deploy portion of angular.json for NPM
Unknown block type "code", specify a component for it in the `components.types` option
I tried this next command while ignoring that I was building with Angular Ivy and received this...
Unknown block type "image", specify a component for it in the `components.types` option
So at this time I would recommend just using Angular 8. I downgraded to 8.3.18 and did npm i.
ng deploy jamstackgr2-library
You should see a message like below
Unknown block type "image", specify a component for it in the `components.types` option
This package will now be available on NPM. Remember anytime you want to update this package you need to bump the NPM version.
Example: https://www.npmjs.com/package/jamstackgr2-library/v/0.0.3
Issue with leaving keys
Unknown block type "image", specify a component for it in the `components.types` option
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/slap-someone-with-slack-commands
+ ZKdo3vLUqcb4sYreiCMSOW
+ Thu, 26 May 2022 04:00:00 GMT
+
+ Slappy Slack
So one of the Purr-fect Peeps on the channel wanted a fun /slap command added to our Slack channel. I basically said, challange accepted! A quick Google search led me down the right path and found Spicefactory Slapbot. I cloned this and then started making some tweaks!
Unknown block type "image", specify a component for it in the `components.types` option
Installation
Note you could run this as a stand alone node server, but I chose to utilize Firebase Cloud Functions since the plan is to add some Machine Learning later.
Before continuing I am committing the files .firebaserc and firebase.json, mainly so I can keep making changes. For your project you will want to delete these files before you begin with the project setup and Firebase initialization.
Initialize Firebase
Unknown block type "code", specify a component for it in the `components.types` option
When presented with your choices arrow down and hit space bar on Functions: Configure and deploy Cloud Functions.
Unknown block type "image", specify a component for it in the `components.types` option
If you would like to start using a new project select Create a new project, for more info checkout Firebase Init Docs.
After that you can select Javascript
Unknown block type "image", specify a component for it in the `components.types` option
For the final settings select N (No) for everything except "Do you want to install dependencies with npm now?", for this Y (Yes).
Unknown block type "image", specify a component for it in the `components.types` option
Now that you have this complete you should see two new files .firebaserc and firebase.json.
Deploy Functions
As it stands right now, this will allow you to test locally (not the intent of this lesson). What we really want is to deploy this to Firebase so that Slack can access it. Since our project should all be setup at this point we just use the deploy command.
In order for us to access an external network you will need to switch your Firebase Subscription for this project, I recommend the Blaze plan as you will not get charged until you go over 125K Invocations a month, I don't know about your slack fans but that would be very impressive! You can select Modify project at the bottom left of the screen and it should open a modal like below.
Unknown block type "image", specify a component for it in the `components.types` option
Now it is time to deploy our function:
Unknown block type "code", specify a component for it in the `components.types` option
It should result in a successful creation of a cloud function with this output:
Unknown block type "image", specify a component for it in the `components.types` option
You can check in the Firebase Console under Develop->Functions
Unknown block type "image", specify a component for it in the `components.types` option
Example of local testing
Unknown block type "code", specify a component for it in the `components.types` option
This command will output where the emulator is running.
Unknown block type "code", specify a component for it in the `components.types` option
So if you want to throw some curl commands at it you could like:
Unknown block type "code", specify a component for it in the `components.types` option
Slack Configuration
Go to Slack services section https://<your_slack_team>.slack.com/apps. For example https://ajonp-com.slack.com/apps.
Incoming Webhook
First we need a way to have Slack accept our /slap messages coming back into Slack's system for our account. We will add an inbound Webhook to allow this interaction to succeed.
Unknown block type "image", specify a component for it in the `components.types` option
- Choose any channel to post to (don't worry, bot will use the channel you type in when you do your slapping)
- Copy and paste tha token part of the URL that you got (it will be something like :
Unknown block type "code", specify a component for it in the `components.types` option
- So it would be the
/T2UR5KNQ2/BMNTDRAG0/mPToTmuARk2BqYhn93Izzbbb portion as we will need this for our slash command.
Unknown block type "image", specify a component for it in the `components.types` option
Slash Command App
Now we need to add and app for the slash command configuration.
Unknown block type "image", specify a component for it in the `components.types` option
Put in the URL your service url or you can use ours -> https://us-central1-ajonp-slack-slap.cloudfunctions.net/api/slap?callback=YOURCALLBACK.
Now you have a working bot with whom you can interact, but it is a private communication between you and him, so you could say that it is not that fun. Let's make it more fun!
- Take the part after
services, as in the above example it will be -> /T2UR5KNQ2/BMNTDRAG0/mPToTmuARk2BqYhn93Izzbbb - Go back to Slash Command and edit the URL to add the token part from above so it should be at the end something like
Unknown block type "code", specify a component for it in the `components.types` option
BTW replace https://us-central1-ajonp-slack-slap.cloudfunctions.net/api with your url, or feel free to use this one.
Usage in Slack
In the easiest form you can just type:
This will tell slapbot to give Chris a big ol' slap with a fish emoji. You can also supply your own emoji by typing:
In this example, Chris will be slapped by the emoji ????. LOL!
By default, the bot will also include some banter at the end. Read on to learn how to configure this.
Configuring the bot behaviour
- To edit the random banter strings, change the
banterArray variable in helpers.js. - To remove the banter, remove the
banter variable from the text property of botPayload in server.js. - To change the bot name or icon, change the
user_name and icon_emoji property of botPayload in server.js. - To change the default emoji, change the
DEFAULT_EMOJI variable in constants.js.
Congratulations, enjoy slapping!
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/design-systems-with-web-components
+ ZKdo3vLUqcb4sYreiCMbmm
+ Wed, 25 May 2022 04:00:00 GMT
+
+ If you have ever worked for a company that has several disparate sites, built with multiple technologies, keep reading. We will walk through building principles behind good Atomic Design, how to create Web Components using Stencil, and finally (perhaps most importantly) how to allow your company to create and adopt a design system.
TLDR - Use Web Components to make a successful Design System.
Flipping the Script on Design Systems
I was recently able to sit down with some of the core members of Ionic, who also created Stencil a toolchain for building Design Systems and Progressive Web Apps. We talked at great length how typically companies are approaching Ionic from a Design Team and need help building components. As a developer I wanted to talk about the Web Components that are used within the Design System first. There was a decent amount of surprise, so I thought I would break down what a Design System is and why it doesn't matter which end you start with, as long as you have both your Design and Development teams working together to build your Design System.
Atomic Design Principle
In my opinion, Atomic Design by Brad Frost is the most expertly written guide. Brad breaks down Atomic design, as it would relate to elements within Chemistry. The key in Atomic Design is to remember that you are building from the most finite component until you build a full page.
There are a 5 stages to Atomic Design:
- Atoms
- Molecules
- Organisms
- Templates
- Pages
From my (limited) experience most Front-End Developers are worried about Atoms, Molecules and Organisms. UI/UX Designers are worried about Organisms, Templates, and Pages. When in reality they BOTH need to consider all 5 stages.
Atoms
Atoms are the smallest parts in a good design, these are things like buttons, text elements, and images. You will notice that even in these basic atoms, your developers will want to know how performant the atom renders and reacts. For example in a button the "click" animation must remain very fast on both desktop and mobile. Your designers will want to allow the color, font, border, and icon placement. We can already start to see that it is necessary that both roles will need to have agreement on each of the components.
Molecules
Molecules are a grouping of these atoms into a logical relationship. For instance we could use an avatar image, label text, and button to create a avatar-item-nav molecule. Now we have a simple, functional, reusable component that we can use in many different locations.
Now Organisms can use both Atoms and Molecules to build out complex components. You can even get really meta and have Organisms use Organisms! I like to use cards (maybe even way too much), so lets use our components from above. Lets say we are building a Book Design System and we want to have an Author Card. This Author card will have the avatar-item-nav which will show the author avatar, the author name, and a button to navigate to the Author's Bio. So lets go down that meta thought, we can take this author-card and place it inside a larger book-card comopnent that has a Book hero-image component and Title. The great part about these organisms is that we can repeat them easily based on dynamic data.
Templates
This is the standardized way that we implement all of the Atoms, Molecules and Organisms across all of your different technologies. Many people use design tools like Sketch or my favorite Figma, to layout each of these different template types. This is many times known as the skeleton of your page, where each of the elements will be placed on the different page types like Home Page, Product Page, and Contact Page. The important part here is to get the correct structure not the content.
Pages
Now you can continue to use those design systems to build the actual content within each template. This is "where the rubber hits the road", you will find out how many tweaks each template, organism, molecule and atom will require to make utilzing all of the components across all of your different systems.
Why did you say flip the script?
Seems all straight forward right, doesn't everyone think this way? Well you are probably a developer not a designer. My design friends would have said we need to set the Typography, Color, and Styles. This is why I said you need to work TOGETHER to create all 5 stages in the design. While the designer is creating lets say the colors, the developer should be making the CSS portable enough to allow every single component to utilize a single source to update those colors. Often companies will start by designing multiple pages and then telling the devs to just figure out how to create them. This is why I think you should "Flip the Script".
A good resource for more on Design Systems is Figma Blog.
Web Components with Stencil
So we talked a lot about the Atomic Design Principle, but you could just use that in any system and start creating. You could have Angular components, React Components, and Vue Components. But if you notice these don't easily work Everwhere. So the solution is to use Web Components because the modern browser can already understand these, and any Front-End framework can then utilize these components. You can use Electron for desktop (Slack, VSCode), PWA for both Android and iOS, and across all browsers Can I Use.
This naturally allows tools like Stencil to create the web components that can be centrally located and updated. This also means that you can then use those components in any Framework or in no Framework at all.
Create Stencil Components
First we are going to create a stencil component project and start creating all of the required components. If you are familiar with ReactJS or JSX you will feel very comfortable. Similar to ion-core that the Ionic team used Stencil to build, we will be creating <yourname>-core, so for example ajonp-core.
Unknown block type "code", specify a component for it in the `components.types` option
Choose:
- component
- Project Name: ajonp-core
Unknown block type "image", specify a component for it in the `components.types` option
Now if you change into your project directory you will see a simple setup that has components directory with a starter my-component. This is a basic component that returns a <div> with your first, middle, and last names that were passed in using props.
This new component can be used simply by providing it into an <html> page. You can see this in index.html, in this new Web Component it is passing 2 props
first="Stencil"
last="'Don't call me a framework' JS"
Unknown block type "code", specify a component for it in the `components.types` option
This might seem like magic, well it is kinda magic! If you look closely at the index.html you will notice that there is a script being imported <script src="/build/mycomponent.js"></script> which provides the required javascript that is used for your component.
You can then run this project and see it in your browser.
Unknown block type "code", specify a component for it in the `components.types` option
For a much further detailed guide please checkout Getting Started.
Create Atoms
For the button we will build out a simple Blue Button which takes in a slot that allows for child components to be passed. In our case this will just be some text.
src/components/ajonp-button/ajonp-button.tsx
Unknown block type "code", specify a component for it in the `components.types` option
src/components/ajonp-button/ajonp-button.css
Unknown block type "code", specify a component for it in the `components.types` option
index.html
To get an idea of how detailed a button can become see what Ionic built with ion-button!
This is just a simple text element that you can pass in a hex color code to change the color, however this element will always be uppercase and a predefined size of 13. Notice here we introduces the element
src/components/ajonp-label/ajonp-label.tsx
Unknown block type "code", specify a component for it in the `components.types` option
src/components/ajonp-label/ajonp-label.css
Unknown block type "code", specify a component for it in the `components.types` option
index.html
Unknown block type "code", specify a component for it in the `components.types` option
This is again a very simple atom style of component that takes in an image url again using props into the src attribute and makes it round like an avatar.
src/components/ajonp-avatar/ajonp-avatar.tsx
Unknown block type "code", specify a component for it in the `components.types` option
src/components/ajonp-avatar/ajonp-avatar.css
Unknown block type "code", specify a component for it in the `components.types` option
index.html
Now we should start to see that all of these Web Components can easily be shown on the screen each time when we added them to our index.html. These are by no means production ready, but you get the general idea of what the makeup of an Atom should contain.
You should see something like this if you run
Unknown block type "code", specify a component for it in the `components.types` option
Unknown block type "image", specify a component for it in the `components.types` option
Create Molecules
We can now take the three atoms that we built above and create a single Molecule for displaying the three items inline.
src/components/ajonp-item/ajonp-item.tsx
Unknown block type "code", specify a component for it in the `components.types` option
src/components/ajonp-item/ajonp-item.css
Unknown block type "code", specify a component for it in the `components.types` option
Unknown block type "image", specify a component for it in the `components.types` option
Create Organisms
Now that we have a molecule we can use this inside of an organism, to repeat the molecule several times. We will take a very simple form which will allow us to add each of the ajonp-item molecules, but you will notice we are also going to use the ajonp-button atom inside this organism as well.
In hindsight maybe I should have called this a list
This grid starts to become more complex as it takes in @Props like our other components, but now we introduce @State so that our component can manage the internal data to that component, in our case all the items that belong within the grid. The final class property is @Elementwhich allows us to access the host HTMLElement, allowing us to find the form input fields and for each individual ajonp-item-grid.
src/components/ajonp-item-grid/ajonp-item-grid.tsx
Unknown block type "code", specify a component for it in the `components.types` option
src/components/ajonp-item-grid/ajonp-item-grid.css
Unknown block type "code", specify a component for it in the `components.types` option
Now if you look closely we are using the two organisms within our page, but we could also use them 100's of times in ANY of our pages!
index.html
Unknown block type "code", specify a component for it in the `components.types` option
Unknown block type "image", specify a component for it in the `components.types` option
Templates
I am not going to dive into all of the code it would take to create a template, implementation but Ionic has offered up a solid solution for this as well in its Layout functionality. What is great here is that you can again work between your development and design team to quickly create a Hi-Fi Wireframe that will fit for your future components. I again have to say I love Figma, as anything built with Web Tech will always hold a special place in my heart. With that said an easy solution for this common task of creating templates can be found in Product Design Kit, checkout the Intro to Product Design Kit for Figma.
What we can do very quickly is setup the template for our page, so that the designers and developers can talk through breaking down all of the components and making sure that you have a template to be successful.
Please don't kill me I am not a designer (yes I should work with one). Here is an example of our template.
Unknown block type "image", specify a component for it in the `components.types` option
Pages
So what templates allow are us to see is that we can adjust our components right away. We can see that our ajonp-button will need to allow for both a primary and secondary type, so we should make sure to update a property to allow for that as well as the corner radius on all buttons should be the same, so we should update the css. The colors and content stay out of the way, until it is time to see page examples. Now we can start to see the real page as the user will interact with it on a daily basis, but because we built our entire system on components from the ground up we can adjust eact Atom, Molecule, and Organism to guarantee that we have the perfect fit in our system.
So something as simple as primary vs. secondary can be adjusted
Unknown block type "code", specify a component for it in the `components.types` option
Unknown block type "image", specify a component for it in the `components.types` option
Creating and Adopting an Enterprise Design System
I have seen many great companies spend millions of dollars maintaining disparate systems, just in order to try and keep up with the pace of their marketing departments branding changes. An example of a disjointed company might have sites built utilizing Sitefinity, Adobe AEM, Angular, and React. Now on each of these stacks lets say they have built 5 different sites. In order to update just the button component across these systems would mean at best you need to change it in 4 places, but more than likely you will need to update it in 20 places then build, test, and deploy each of those systems.
In a component system this takes a single update to that component!!
Add central location
In order for the adoption to take place you will need to host your components in a centralized location like NPM. Here is a great guide on how to Publish to NPM. Because you worked so hard to create this system you will most likely want to keep it within your companies private npm registry.
How to be successful
You need to get buy-in from both departments and work as a cohesive unit to become truly successful, but in the end you will save multitudes of time and money!!!
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/rxfire-in-svelte-3-using-firebase-firestore-and-authentication
+ ZKdo3vLUqcb4sYreiCMUrk
+ Tue, 24 May 2022 04:00:00 GMT
+
+ RxFire was created by David East, who is a Firebase Developer Advocate. Although Firebase Blog introduced RxFire in September, 2018. I thought it would be a good point of reference for beginners starting out with both Svelte 3 and Firebase.For those coming from an Angular background you might be familiar with RxJS and the Angularfire2 package. I know from my experience you MUST learn RxJS if you are going to use Angular on any size application. It can be one of the hardest parts to learn but there are several fantastic tutorials and sites dedicated to how RxJS works.Some of my favorites
New to Svelte 3, Please checkout
Example
In the example below it shows a quick preview osf how the final React application will work. As you can see there are essentiall 4 steps that will occur.
- Sign In
- Add Cat Fact
- Show Added Cat Fact in List
- Sign Out
Setup
You will need NPM which comes bundled with nodejs. Also npx is the easiest using the template provided on git.
npx degit sveltejs/template rxfire-svelte cd rxfire/svelte
You will also need to create your own Firebase project and initialize a Firestore database, please create this using strict mode. Example rules;
Final App Structure
Unknown block type "image", specify a component for it in the `components.types` option
Unknown block type "image", specify a component for it in the `components.types` option
Install dependencies
You will need rxfire, firebase, and rxjs
npm i rxfire firebase rxjs
Create Firebase.js
Firebase.js will be our main Firebase file and will include the tools for our firebase application, including the instantiation of firebase. This file includes rxfire for both the auth and firestore helper functions.Firebase.js
Unknown block type "code", specify a component for it in the `components.types` option
You can skip the Instructions part of the lesson if you are already familiar with Svelte 3.
Simple Initial Component
Remove App.svelte and replace it with the following
You can basically think of a .svelte file equal to an html file. There are a few things to note here, any styles are scoped to the current component by default, in order to get styles outside of this you can place them within something like :global(div.flex-row). However (maybe best practice), I found it easier to move these from App.svelte over to /public/global.css;App.svelte
Unknown block type "code", specify a component for it in the `components.types` option
Create Instructions.svelte
This is a very simple Svelte Component that takes no props, it just returns straight html.components/Instructions.svelte
Unknown block type "code", specify a component for it in the `components.types` option
Update collection catfacts
Create AddCat.svelte
The first button that we are going to add is simple enough it calls an API and pushes the data returned into a firestore collection as a new document. I always like to work from top down, so first lets import AddCat.svelte into our App.svelte.
Update App.svelte
App.svelte
Unknown block type "code", specify a component for it in the `components.types` option
We will now pass our first prop to AddCat, this is done simple by declaring a variable and passing it, in our case we will use const user = loggedIn$. Is this necessary, probably not but I wanted to show passing a prop, we could ideally just grab this from Firebase.js.Take note on the child we can then access this prop by defining export let user;.The addCatFact function calls an API that returns a single random cat fact from https://cat-fact.herokuapp.com. Because of CORS we have to pass it through a proxy, but you will see that we get a value back. We then use the javascript destructuring assignment to create a new object by adding our catFactDate to the object. This allows us to later sort the facts in our list in descending order.We add a random cat fact to firestore and by adding our catFactDate to the object. This allows us to later sort the facts in our list in descending order.Things we need to pay special note to here are
- #if - If blocks just a standard if that you would expect
- $user - Subscriptions if you come from angular this is like the async pipe.
on:click={addCatFact - [Component Events](https://svelte.dev/docs#Component_events) this is similar to the events normally found on html componens but here we are passing the function addCatFact by just surrounding with curlys.
Unknown block type "code", specify a component for it in the `components.types` option
Now if you try this right now it should fail with this alert
Unknown block type "image", specify a component for it in the `components.types` option
This is expected as our firestore.rules are set to strict mode, we will add those after we get through our authentication section next.You may also notice in the console (and on the screen fact jumps in and out) that firebase actually adds to our array, until failing on the backend. This is done on purpose as it gives us the fast UI that we expect while still maintaining the data integrity.
Add Firebase Authentication
Update App.svelte
In the SignIn component we will again use props, we will pass the user state.App.svelte
<script> ... import SignIn from './components/Signin.svelte'; ... </script> ... <SignIn user={user} /> ...
Unknown block type "code", specify a component for it in the `components.types` option
Update AddCat to include user uid
Pass user to AddCat
Update our main app to pass the user prop.App.svelte
<AddCat user={user} />
Now we can use this to include with our data going to firestore. AddCat.svelte
As well as whether or not to show the Add Cat Fact button, we check to see if the user exists. This button should only show when a user is signed in.
Unknown block type "code", specify a component for it in the `components.types` option
Update firestore.rules
Unknown block type "code", specify a component for it in the `components.types` option
Create List of Cat Facts
Create ListCatFacts
This is probably the most important part of RxFire it will return an Observable that you can subscribe to for all changes to a collection by using the function collectionData which takes the collection as paramater as well as an option id to create, in our case we pass catFactsRef.orderBy('catFactDate', 'desc') and 'catFactId'.Now we can just use a map to iterate on each catFact, whenever the Observable updates the current catFact state the array is updated and we can show the full list update using {#each $catFacts as catFact} then if you are the owner of the fact you get catheart eyes using a if block.
Unknown block type "code", specify a component for it in the `components.types` option
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/rxfire-in-reactjs-using-firebase-firestore-and-authentication
+ ZKdo3vLUqcb4sYreiCMVA8
+ Mon, 23 May 2022 04:00:00 GMT
+
+ RxFire was created by David East, who is a Firebase Developer Advocate. Although Firebase Blog introduced RxFire in September, 2018 I thought it would be a good point of reference for beginners starting out with both ReactJs and Firebase.For those coming from an Angular background you might be familiar with RxJS and the Angularfire2 package. I know from my experience you MUST learn RxJS if you are going to use Angular on any size application. It can be one of the hardest parts to learn but there are several fantastic tutorials and sites dedicated to how RxJS works.Some of my favorites
New to React, Please checkout
Example
In the example below it shows a quick preview of how the final React application will work. As you can see there are essentiall 4 steps that will occur.
- Sign In
- Add Cat Fact
- Show Added Cat Fact in List
- Sign Out
Setup
You will need NPM which comes bundled with nodejs.Since we wil be the create-react-app cli you must install this first
You will also need to create your own Firebase project and initialize a Firestore database, please create this using strict mode.
Final App Structure
Unknown block type "image", specify a component for it in the `components.types` option
Install dependencies
You will need both rxfire and firebase
npm i rxfire firebase
Create Firebase.js
Firebase.js will be our main Firebase file and will include the tools for our firebase application, including the instantiation of firebase. This file includes rxfire for both the auth and firestore helper functions.Firebase.js
Unknown block type "code", specify a component for it in the `components.types` option
You can skip the Instructions part of the lesson if you are already familiar with ReactJS.
Simple Initial Component
Remove App.js and replace
Remove the core of App.js to start as we will be using the pattern of extends React.Component. We will now start simple by adding our first React Component Called Instructions.
Unknown block type "code", specify a component for it in the `components.types` option
Create Instructions.js
This is a very simple React Component that takes no props, it just returns straight html.components/Instructions.js
Unknown block type "code", specify a component for it in the `components.types` option
Update collection catfacts
Create AddCat.js
The first button that we are going to add is simple enough it calls an API and pushes the data returned into a firestore collection as a new document. I always like to work from top down, so first lets import AddCat.js into our App.js.
App.js
Unknown block type "code", specify a component for it in the `components.types` option
Now that it is in our App lets create the AddCat component. The only html that this passes back is onClick={this.addCatFact}. This is the JSX way of using onClick and passing a function. If you are new to ES6 arrow functions addCatFact = () => { is still just defining a function using a shortened syntax, it is becomming more standard these days.The addCatFact function calls an API that returns a single random cat fact from https://cat-fact.herokuapp.com. Because of CORS we have to pass it through a proxy, but you will see that we get a value back. We then use the javascript destructuring assignment to create a new object by adding our catFactDate to the object. This allows us to later sort the facts in our list in descending
order.components/AddCat.js
Unknown block type "code", specify a component for it in the `components.types` option
Now if you try this right now it should fail with this alert
Unknown block type "image", specify a component for it in the `components.types` option
This is expected as our firestore.rules are set to strict mode, we will add those after we get through our authentication section next.You may also notice in the console (and on the screen fact jumps in and out) that firebase actually adds to our array, until failing on the backend. This is done on purpose as it gives us the fast UI that we expect while still maintaining the data integrity.
Add Firebase Authentication
We need to store using state within our application, so we can define a state object with a field user we will then use this from firebase when a user is logged in. We will also import a new SignIn button that we can use to hide show Sign In / Sign Out depending on the current state.In the SignIn component we will for the first time start using props, we will pass the user state as well as a couple of our functions that are needed in the component to update our user.App.js
Unknown block type "code", specify a component for it in the `components.types` option
SignIn.js
Unknown block type "code", specify a component for it in the `components.types` option
Update AddCat to include user uid
Pass user to AddCat
Update our main app to pass the user prop.App.js
Now we can use this to include with our data going to firestore. AddCat.js
Unknown block type "code", specify a component for it in the `components.types` option
As well as whether or not to show the Add Cat Fact button, we check to see if the user exists. This button should only show when a user is signed in.
Unknown block type "code", specify a component for it in the `components.types` option
Update firestore.rules
Unknown block type "code", specify a component for it in the `components.types` option
Create List of Cat Facts
Create ListCatFacts
This is probably the most important part of RxFire it will return an Observable that you can subscribe to for all changes to a collection by using the function collectionData which takes the collection as paramater as well as an option id to create, in our case we pass catFactsRef.orderBy('catFactDate', 'desc') and 'catFactId'.Now we can just use a map to iterate on each catFact, whenever the Observable updates the current catFact state the array is updated and we can show the full list update using {this.state.catfacts.map(catFact => {.ListCatFacts.js
Unknown block type "code", specify a component for it in the `components.types` option
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/angular-pwa-to-google-play-store-using-trusted-web-activity
+ ZKdo3vLUqcb4sYreiCMdfE
+ Sun, 22 May 2022 04:00:00 GMT
+
+ PWA’s are still changing rapidly. This tutorial may not be as up to date, so I have removed the content and left the video for history]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/firebase-multisite-hosting
+ ZKdo3vLUqcb4sYreiCMb2o
+ Fri, 20 May 2022 04:00:00 GMT
+
+ This tutorial will focus on a very easy solution to having multiple web projects hosted under the same domain.
Firebase limits a single domain for all of your projects. Once you have associated this domain you not be able to use it again for any other project. For example we would like to use lessons.ajonp.com but since we are using ajonp-ajonp-com for ajonp.com domain we are unable to use this.
We also wanted to mention that the video may not match exactly what is in the lesson7 repo, we are sorry about that, initially we were going to use two projects with a single domain. Please pull the repo directly and these steps should help you get to a finished setup quickly.
Steps
- Switching Firebase Plans
- Create 4 Firebase Hosting Sites
- Example Multiple Hugo Themes deploying to Firebase Multisite
- Example Angular Project with Multiple projects deploying to Firebase Multisite
- Google Cloud Builder - Trigger
Firebase Pricing Plans
New Project
If you have just created a new project you will need to upgrade in order to use Multisite hosting.
Unknown block type "image", specify a component for it in the `components.types` option
Firebase Pricing is ever changing, so make sure to check on this.
Hosting Free limit on the Blaze plan
Unknown block type "image", specify a component for it in the `components.types` option
Create 4 Firebase Hosting Sites
You can name these whatever fits your project, if you using ajonp* you will probably have issues as we am already using those.
Example for your site:
- mysite
- mysite-admin
- mysite-amp
- mysite-app
Unknown block type "image", specify a component for it in the `components.types` option
Unknown block type "image", specify a component for it in the `components.types` option
Example Multiple Hugo Themes deploying to Firebase Multisite
You don't need to install anything if you just deploy to Google Cloud. In this lesson reference Cloud Build Also see https://ajonp.com/lessons/google-cloud-repositories-ci-cd for more details on CI/CD.
Once you clone https://github.com/AJONPLLC/lesson-7-firebase-multisite-hosting you can run it locally, but you must build out all of the sites before doing this.
Remove git reference
Please remove the reference to AJONPLLC, then you can add any git repo you would like.
Unknown block type "code", specify a component for it in the `components.types` option
Add new reference
Create a new github site (or Gitlab or Google Cloud or...)
Then add the new repo back to your site using this command, replace ajonp/a-sample-repo.git with yours.
Unknown block type "code", specify a component for it in the `components.types` option
Steps for building hugo sites
- Git Submodule commands, make sure you have Git CLI.
Unknown block type "code", specify a component for it in the `components.types` option
This will update to the latest remote builds for ajonp.com, including both the content and themes that we host publicly
- Hugo commands, make sure you have the Hugo CLI installed.
Unknown block type "code", specify a component for it in the `components.types` option
Example Angular Project with Multiple projects deploying to Firebase Multisite
Steps for building Angular sites
NPM commands, make sure you have Node Js installed.
Unknown block type "code", specify a component for it in the `components.types` option
Angular commands, make sure you have the Angular CLI installed.
Unknown block type "code", specify a component for it in the `components.types` option
Serve locally using firebase, make sure you have Firebase CLI
Unknown block type "code", specify a component for it in the `components.types` option
Google Cloud Builder - Trigger
Please add a trigger to your repository reference Firebase CI/CD lesson for more details.
Cloud Build
Now after having tried all of the manual build steps out, you really don't need any of them. You can run all of this in the cloud by setting up the commit trigger from above.
cloudbuild.yaml
Unknown block type "code", specify a component for it in the `components.types` option
Cloud Build locally
If you would like to debug locally please make sure you read Building and debugging locally. This will require that you have Docker installed locally.
Install necessary components
Install docker-credential-gcr
Configure Docker
Check to see if version is working
In the root directory of your project, you can then run this command (don't forget the "." as this tells Cloudbuild to use the current directory)
You will notice that this output matches what happens in the cloud when Google Cloud Build executes
Unknown block type "code", specify a component for it in the `components.types` option
Google Cloud Builder Output
Unknown block type "image", specify a component for it in the `components.types` option
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/hugo-ionic-template
+ ZKdo3vLUqcb4sYreiCMYou
+ Thu, 19 May 2022 04:00:00 GMT
+
+ Please note that while working through this tutorial I chose to make branches instead of tags to see if people would enjoy that more. However there was an update to the Victor/Hugo template to start using webpack and I don't want to confuse anyone, so please don't pull the master branch to start.
- master (initial)
- netlify-victor-hugo (moves everything to npm setup)
- complete-lesson-4 (has everything you need to go-live)
- prod (shows the example trigger, as well as moving over to git submodule)
Official Documentation
Google Cloud Build - Deploying Artifacts
Lesson Steps
- Clone Lesson 4
- Add Netlify Victor/Hugo template to project.
- Update branding.
- Configure Firebase Hosting
- Configure Algolia Indexing
- Configure Google Cloud Build
Add Netlify Victor/Hugo template to project
Get the initial setup
You can follow along step-by-step at the Initial Setup Lesson, and YouTube Video.
Or you can start by cloning Hugo Ionic from GitHub, we are using a specific branch to start this lesson as the master was for the original lesson.
Unknown block type "code", specify a component for it in the `components.types` option
Structure
- site // Everything in here will be built with hugo
- content // Pages and collections - ask if you need extra pages
- data // YAML data files with any data for use in examples
- layouts // This is where all templates go
- partials // This is where includes live
- index.html // The index page
- static // Files in here ends up in the public folder
- src // Files that will pass through the asset pipeline
- css // Webpack will bundle imported css seperately
- index.js // index.js is the webpack entry for your css & js assets
Running Hugo
You can still run this project as a normal hugo site at this point, by running the command while in the site folder.
Make sure to return to your root folder cd ..
Running Victor/Hugo
There are a few additional requirements at this point that we need to install.
Install the files via npm npm comes with NodeJs
Unknown block type "code", specify a component for it in the `components.types` option
Start a browsersync server
Unknown block type "code", specify a component for it in the `components.types` option
You should see something like:
Unknown block type "code", specify a component for it in the `components.types` option
Update to use Victor/Hugo asset pipeline
Why this becomes powerful is that you can start installing all of your favorite npm modules and importing them like you would with most non-static projects.
Copy the baseof.html file from our theme to our base directory so that we can override the themes file.
Unknown block type "code", specify a component for it in the `components.types` option
Remember to edit baseof.html in your files not the themes (or they won't be saved.)
Now we are going to edit the baseof.html and remove these two lines (just before </head>)
Unknown block type "code", specify a component for it in the `components.types` option
These were originally coming from our site folder, both in theme and direct, we are going to update this file to allow for victor/hugo to handle this requirement. So we will replace that line in the html with
Also while we are in /site/layouts_default/baseof.html add a reference right above </body>.
You should now see the theme change back to the stock Ionic Blue color, as we have not moved our styles to the source location. However there will be a new addition to the console in debugger.
Move our custom.css file to the source folder.
Unknown block type "code", specify a component for it in the `components.types` option
Now we need to import this file into our src/css/main.css
Unknown block type "code", specify a component for it in the `components.types` option
Example of loading software to use
Lets try adding something simple that we might use like lodash
Unknown block type "code", specify a component for it in the `components.types` option
Now this will be avalabel for us to use in our app.js or any other file we would like to reference the module.
src/js/app.js
Unknown block type "code", specify a component for it in the `components.types` option
In the console you will now see the current version of lodash.
Update branding
I am sure that you love looking at AJ on your favicon and manifest (he likes being there ????). But you probably want to update a few things.
icons
In the themes/ajonp-hugo-ionic/static/icons folder you will find several images that can be used for your manifest file. I typically use www.favicon-generator.org to update these files based on an image that I create. You can now add all of these images to site/static/icons so they can be referenced later in manifest.json or anywhere in your site.
manifest.json
You should update this file to reflect any changes in your icons folder.
Unknown block type "code", specify a component for it in the `components.types` option
Favicons
If you are using an older browser the main icon that goes in your browser will be used by /site/static/favicon.ico. Otherwise you should hvae larger images spelled out in the themes/ajonp-hugo-ionic/layouts/partials/head.html, they look similar to this...
Unknown block type "code", specify a component for it in the `components.types` option
Please feel free to update the entire head.html partial, or any of the layouts for that matter AJ doesn't mind too much :P.
Configure Firebase Hosting
Please make sure to checkout Google Cloud Firebase CI/CD for a great example.
Quick steps
Unknown block type "code", specify a component for it in the `components.types` option
Select Hosting
Unknown block type "image", specify a component for it in the `components.types` option
Pick your project
Unknown block type "image", specify a component for it in the `components.types` option
Set the output folder to dist, and select no for rewrite all urls.
Unknown block type "image", specify a component for it in the `components.types` option
Configure Algolia Indexing
First signup on www.algolia.com/.
Algolia Signup
About You! Algolia Signup about
Your Datacenter! Algolia Your Data Center
Your Project Type - Depends on what you are making but I would choose Media for most sites.
Unknown block type "image", specify a component for it in the `components.types` option
Pick Dashboard once you get here
Unknown block type "image", specify a component for it in the `components.types` option
You can run the tutorial or just skip for now.
Now select Indicies, and Create Index! Indicies
Example of an Index Creation
Unknown block type "image", specify a component for it in the `components.types` option
API Keys are where we can now get the remainder of our information that we will need.
Unknown block type "image", specify a component for it in the `components.types` option
Add Algolia for Search
In your site/config.toml file you will see params.algolia. These are used just for the search bar to make the search happen...do not use your Admin API KEY!!
Unknown block type "code", specify a component for it in the `components.types` option
You will notice a section later when we use Cloud Depoy that will require you to add
- ALGOLIA_APP_ID
- ALGOLIA_ADMIN_KEY
- ALGOLIA_INDEX_NAME
- ALGOLIA_INDEX_FILE
Configure Google Cloud Build
An important part to any project is setting up a solid CI/CD pipeline (Continuous Integration/Continuous Delivery). I still believe that Google Cloud offers the best pricing for any size development team! If you are a very small shop you will run builds for free, for a very long time.
Docker Images / Cloud Build
I found out recently from Mike McDonald, that there are a great set of Google Cloud Platform Community Images. They should work really well most of the time, but you might have to be careful on versioning. For instance Hugo is currently at .49 and I need something above .50 for my site. This is why I remain using my own Docker files.
cloudbuild.yaml
This file controls all of the steps necessary throughout our build process. You can manually execute each step locally to see that everything works as expected on your docker images.
Unknown block type "code", specify a component for it in the `components.types` option
Please checkout all of the cloud builder images.
Step 0 and 1
Docker is an available image we will use this to create our own image instance from our Dockerfile in dockerfiles/hugo/Dockerfile. The Google Cloud build image Node seems to offer a lot of the tools we will need already so we just add Hugo into this Dockerfile.
Unknown block type "code", specify a component for it in the `components.types` option
The next step in this will execute our local file deploy.sh so that we can execute a few key commands. The purests out there will say that we can execute both the git command and the npm commands using the two external images. I find that it is faster to not spin up a new image since we are sitting in a node image already for npm. The git submodule also is a bit of a pain because you have to go through a complicated process (from what I have gathered) to either link github to google source repos or create tokens for one off logins.
Unknown block type "code", specify a component for it in the `components.types` option
- Update any submodules (like our theme).
- Install all of the victor/hugo dependencies (or all other).
- Run the build command which will push all of our files to the
/dist folder.
Step 2
Now I know everything I said up above about npm image...sorry for any confusion. However, I thought it was better to leave these npm commands seperate from the deploy as they are not required and can be easily commented out of your builds.
In the case of this evn variables, we can again leave these out of the public git repos of the world and only have them stored wihtin Google Cloud builder variable values, using the notation ${_variable}.
Unknown block type "code", specify a component for it in the `components.types` option
This is again using atomic-algolia to look at our newly created dist/algolia.json file and compares that to what is in the Algolia index.
Step 3
Want a PWA still? Then we need to have a service worker. No AJ not an Animal service worker.
Unknown block type "code", specify a component for it in the `components.types` option
Step 4 and 5
Please make sure to checkout Google Cloud Firebase CI/CD for a great example.
The biggest item that you need to remember here is adding your token to the cloud builder.'
Unknown block type "code", specify a component for it in the `components.types` option
Now you should have your full Hugo site up and running on Firebase Hosting!
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/ajonp-hugo-ionic-template
+ ZKdo3vLUqcb4sYreiCMebS
+ Wed, 18 May 2022 04:00:00 GMT
+
+ Just a little donation reminder as Hugo says "Hugo stands on the shoulder of many great open source libraries", as does many of my tutorials. Brew Hugo
Hugo Getting Started
Checkout the guide at gohugo.io. My guides will always be on a Mac, but I will always try to provide a link for additional operating systems.
Lesson Steps
- Install Hugo
- Create new Hugo site
- Start Using AJonP Template
Lesson 2 (required for Algolia)
Please chekcout Lesson: Hugo Ionic for the next set of features 1. Victor Hugo 1. Deploy
Install Hugo
Brew
If you are like me and just bought a new Mac, you probaly are taking brew for granted and think it is just there right ????! Well first you can head over to brew.sh they will tell you to run
Unknown block type "code", specify a component for it in the `components.types` option
Hugo
Unknown block type "code", specify a component for it in the `components.types` option
Create new Hugo site
Unknown block type "code", specify a component for it in the `components.types` option
At this point you will notice that the project remains pretty empty in a generic skeleton.
Unknown block type "image", specify a component for it in the `components.types` option
It only has two files config.toml and archetypes/default.md
If you run hugo serve right now, you will see a blank screen as there is no content to show.
Index.html
Adding this to the base of our site will be used for setting up the Home Page. This is the only required page you will ever need to build a Hugo based site. Please keep in mind that this is still an html page.
layouts/Index.html
Unknown block type "code", specify a component for it in the `components.types` option
Again if you run hugo serve right now, you will see a blank screen as there is no content to show.
_index.md
content/_index.md
I know this is getting a little frustrating! Again if you run hugo serve right now, you will see a blank screen as there is no content to show.
Update Index.html
This will be the first page that will show anything in the browser!
layouts/index.html
Now run the command hugo serve and you will see a page that has
Unknown block type "image", specify a component for it in the `components.types` option
Making a Point
Now I wanted to walk you through all of that to show 1. The steps really necessary to make a Hugo site 1. Prove that laying out a site from scratch is time consuming
Start Using AJonP Template
Theme Download Location
You can find the link on Hugo's Theme site https://themes.gohugo.io/ajonp-hugo-ionic/
Git integraiton
Clone (Easy, not updated)
Unknown block type "code", specify a component for it in the `components.types` option
Submodule (Better, updated)
Unknown block type "code", specify a component for it in the `components.types` option
Adding the submodule will allow you to receive all of the updates that you want, or lock into a specific commit to run your site from. Then later you are able to run
Unknown block type "code", specify a component for it in the `components.types` option
Theme Benefits
Now you should have a new folder in themes/ajonp-hugo-ionic. This has the full theme including an example site found in themes/ajonp-hugo-ionic/exampleSite
Features
Ionic Theming
Taking things a step further you can change any of the colors on the site by using Ionic's Color Generator.
Here is a Hugo inspired look.
Unknown block type "image", specify a component for it in the `components.types` option
Just copy the CSS Variables it produces into static/css/custom.css
Unknown block type "code", specify a component for it in the `components.types` option
Theme updates
I used the theme for AJonP so it may change (you can always stay at a commit), but please contact me or Pull request on Github.
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/google-cloud-repositories-ci-cd
+ ZKdo3vLUqcb4sYreiCMZ7I
+ Mon, 16 May 2022 04:00:00 GMT
+
+ We will continue our hello world example from Firebase Project Hosting and extend this into publishing our site to firebase using CI/CD. The final hosting example will create the site in the initial project folder (in my case ajonp-lesson-1)
- Source Code Repositories Pricing
- Create Github Repository
- Create Google Cloud Source Repository
- Create Dockerfile for uploading to Firebase
- Create cloudbuil.yaml for Google Cloud Build to trigger build
- Setup Triggers
- Commit to cause a trigger to run
- Update content to change Hello World
Why do I use Google Source Repositories?
Because it is cheaper! (and I develop on a shoestring budget) The negative is that it is not as straight forward to use as git clone
Unknown block type "image", specify a component for it in the `components.types` option
Github
Don't get me wrong I think that $7 a month is a great deal, but you have to pay it the minute you want to start using private repos.
Unknown block type "image", specify a component for it in the `components.types` option
Unknown block type "image", specify a component for it in the `components.types` option
Google Cloud Source Repositories
Basically free until you scall to a large level.
Unknown block type "image", specify a component for it in the `components.types` option
Create Repository
Select new
Unknown block type "image", specify a component for it in the `components.types` option
Create name for repo
Unknown block type "image", specify a component for it in the `components.types` option
Clone our existing "Hello World" project
Unknown block type "code", specify a component for it in the `components.types` option
Remove remote references
When you are cloning an existing repository you need to cleanup the remote reference to store this into a new repository.
Lets look at the remotes currently
This should show something like
Unknown block type "code", specify a component for it in the `components.types` option
Let's remove this remote
Unknown block type "code", specify a component for it in the `components.types` option
Add Newly created repo
Unknown block type "code", specify a component for it in the `components.types` option
Unknown block type "image", specify a component for it in the `components.types` option
Google Cloud
As of right now December 3, 2018 Google has this message: This version of Cloud Source Repositories will permanently redirect to the new version of Cloud Source Repositories starting December 3rd. Try the new version today for fast code search, an improved code browser, and much more.
Google Cloud Source Repositories
Open Source Cloud Repositories. Now you can select "Add repository".
Add Google Cloud Repository (standalone)
Select "Create new repository" option.
Unknown block type "image", specify a component for it in the `components.types` option
From the first lesson you should have a project that was created from firebase, you can use the dropdown under "Project" to select. Then Click "Create" (Not Create Project, unless you want this to be seperated).
Unknown block type "image", specify a component for it in the `components.types` option
Add Code to Google Cloud Repository
Typically on a fresh repo you would now clone this and start working.
Unknown block type "image", specify a component for it in the `components.types` option
For this example we are going to select "Push code from a local Git repository" because we have an example we are already working with.
Unknown block type "image", specify a component for it in the `components.types` option
You can skip the first command (unless you skipped both the Lesson 1 and hosting in GitHub).
First verify that we have the correct origin for your project (mine should be lesson-2 not lesson-1)
origin https://github.com/AJONPLLC/lesson-2-firebase-ci.git (fetch) origin https://github.com/AJONPLLC/lesson-2-firebase-ci.git (push)
Now add Google Source Repository as an additional remote location
Unknown block type "code", specify a component for it in the `components.types` option
Look at your remotes once again and you should see two.
google https://source.developers.google.com/p/ajonp-ajonp-com/r/ajonp-lesson-2 (fetch) google https://source.developers.google.com/p/ajonp-ajonp-com/r/ajonp-lesson-2 (push) origin https://github.com/AJONPLLC/lesson-2-firebase-ci.git (fetch) origin https://github.com/AJONPLLC/lesson-2-firebase-ci.git (push)
Helpful hint for multiple remotes in VSCode, you can access the git tab, then "...", then "Push to...". This will show you the remotes.
Unknown block type "image", specify a component for it in the `components.types` option
To remove your GitHub based remote you can execute, however we will not do this now as we want to test pushing code to both repositories.
Unknown block type "code", specify a component for it in the `components.types` option
Create Dockerfile
I like to create a folder for all of my dockerfiles, this allows you to easily locate and access them all. There are many references in the Official Guide, but they always seem to place this file alongside your cloudbuild.yaml file (in my opinion this confuses things).
Unknown block type "image", specify a component for it in the `components.types` option
This Dockerfile is utilizing a prebuilt node image from node. dockerfiles/firebase/Dockerfile
We need to make sure that billing and the cloud build API are setup. You can follow a great guide Enable Billing.
Again we are doing this on the cheap, other places will charge you a monthly fee for this. Google allows for 120 build minutes a day!!
Unknown block type "image", specify a component for it in the `components.types` option
Create Cloudbuild
This cloudbuild.yaml file will leverage the gcloud trigger. https://cloud.google.com/cloud-build/docs/cloud-builders
cloudbuild.yaml (place in root directory)
Unknown block type "code", specify a component for it in the `components.types` option
You may have noticed an interesting line here, this allows for an argument called _FIREBASE_TOKEN to be setup in our cloud deploy, so it doesn't leak out in our GitHub/GCP Repositories.args: ['deploy', '--token', '${_FIREBASE_TOKEN}']
We will need a token from firebase for this next Patterson
This will walk you through the process (same as login before). When this finishes back in the terminal you should receive a token like 1/8V_izvEco3KY8EXAMPLEONLYpnLGpGLPAvofC_0YX3qx2NE_Zxs Along with a message Example: firebase deploy --token "$FIREBASE_TOKEN"
You will need to capture this token, or leave your terminal open for setting up a trigger.
Setup trigger
Go back to Google Cloud Platform Console. Using the hamburger navigation Cloud Build > Triggers.
Unknown block type "image", specify a component for it in the `components.types` option
Github Trigger
Select Github as source, then authenticate.
Unknown block type "image", specify a component for it in the `components.types` option
Select Repository
Unknown block type "image", specify a component for it in the `components.types` option
Setup Trigger Settings
Pay close attention to add the Firebase Token from the steps above.
Unknown block type "image", specify a component for it in the `components.types` option
GitHub Commit for trigger
Now you can run the commands to add the changes dockerfile and cloudbuild.yaml
Now remember we have commited these changes locally, we still need to push them to origin (which is our GitHub remote)
You can now check that your trigger didn't fail by going to Google Cloud Platform - Cloud Build History
Google Source Repository Trigger
This will be the identical setup.
Unknown block type "image", specify a component for it in the `components.types` option
Unknown block type "image", specify a component for it in the `components.types` option
Now we just need to push to a different remote (Google Cloud Repository)
Update Content - see CI/CD in Action
Update your index file to show something changed.
public/index.html
Unknown block type "code", specify a component for it in the `components.types` option
Add, commit, push files
Unknown block type "code", specify a component for it in the `components.types` option
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/learning-builder-io-from-a-developers-perspective
+ ZKdo3vLUqcb4sYreiCMWwS
+ Wed, 13 Apr 2022 04:00:00 GMT
+
+ I sat down with Tim Benniks, who is a professional developer advocate, to introduce him to Builder.io. It was great to hear what questions a developer might have when being first introduced to the tool. I know Tim is well versed in all things Jamstack, so he wasn't coming into the conversation completely blind.
Key Takeaways
It was a huge help to talk with Tim about a product that I am supporting and building awareness about.
What Developers need to know
- Developers want to see code
- Understanding of what data is required to build out the pages, user interface, and state management
- How custom components can be used
What I learned
Because I have been learning the product over several months now there were many areas that I take for granted. When I show people the product it is going to be important to mention very key items that both developers and content creators will find useful. For instance, when I was entering one of the data bindings for state Tim was surprised by the editor.
Unknown block type "image", specify a component for it in the `components.types` option
Something I forgot to mention was the fact this is not the only editor. Not only can you use components and bind dynamic data, there is also the capability to access global styling and JavaScript with a built in editor as well. In the image below you can see that you are also able to run JavaScript on the server via Builder.isServer as well as only on the client via Builder.isBrowser.
Unknown block type "image", specify a component for it in the `components.types` option
More Details
Below are some more details that may have been missed in the video. I am sure as I learn more I will release some more videos, both on CodingCat.dev and Builder.io.
This is probably one of the biggest areas to understand in builder. In most of the common frameworks that we use today they have very specific and well defined methods of calling APIs. Explaining that the framework is best suited to do this in must cases for creating pages and structure to your site. For instance when we were walking through how to create pages in Next.js, it is key to call your CMS (no matter if this is builder or something else) to build out post pages.
Below is how CodingCat.dev creates pages in Next.js. This will be the same in Nuxt, SvelteKit, Gatsby or anything that creates SSG pages.
Unknown block type "code", specify a component for it in the `components.types` option
When the page is then generated you must call your CMS for that specific page and pass the data that is required to generate the visual page.
Unknown block type "code", specify a component for it in the `components.types` option
Then when this data passes into the system it is in two different areas of BuilderComponent, content and data.
Unknown block type "code", specify a component for it in the `components.types` option
Below is a breakdown that I made so that you can understand what all the props for the BuilderComponent actually do. As you can see data is meant to include dynamically changing state data, and content will be your pages data as Described by the Builder.io JSON.
https://cdn.sanity.io/files/hfh83o0w/dev/1b236cbae3dc5c84d31a1542a65f6fbf41379947.png
Builder JSON
As you are using the visual editor, ever developer knows that all of this has to end up as HTML and CSS in the end. Because Builder.io creates a special serialized JSON this is what allows it to be translated into any framework using Builder.io's open source project Mitosis.
Below is an example of what that JSON looks like (again available directly within the visual editor data section.
Unknown block type "code", specify a component for it in the `components.types` option
The "blocks" item is where ever single item that is in our layers panel will be described as a unique block component. It is far too large to show everything. Below is an example of one of those blocks. In this case it is our custom Next.js component called NextImage.
Unknown block type "code", specify a component for it in the `components.types` option
I can't come close to teaching you everything about Builder.io in a single post, but my team is building out documentation, courses, and videos to help developers be successful!
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/codingcatdev-is-joining-the-learn-build-teach-community
+ ZKdo3vLUqcb4sYreiCMcEM
+ Sun, 10 Apr 2022 04:00:00 GMT
+
+ A new home for CodingCat.dev's Discord
After a long chat with Brittney about how hard it is to maintain our Discord and how I don't like breaking up communities, she agreed that we could make a move. I wanted to make sure we stick with our goal of creating great communities and developing in the open, the first thing that came to mind was Learn Build Teach (LBT). Brittney agreed that if that community was up for letting us join it would make the most sense.
Brittney's 2 Cents
Communities are an amazing resource for learning, getting help, and finding people that like the same things as you. However, when you have too many communities it can feel overwhelming to participate and keep up in all of them. We thought the Learn Build Teach philosophy fit well with what we are doing at CodingCat.dev. I think it is a great initiative and we are excited to keep combining more communities in and grow stronger together.
The Ask
So I typed up this over to James Q. Quick
Hey James,
Brittney and I have been talking a lot about the problem with breaking up groups on Discord and feeling like overkill.
I was wondering if you would be up for a conversation around codingcat just utilizing LBT as the central place for community.
It would allow us to grow together and we can utilize Purrfect to push people there as well so we can all grow together.
On top of that I can then stop boosting my server and start boosting LBT too.
In my head I am thinking we could have just a dedicated channel (maybe section) to codingcat, for specific questions about our courses.
We would just send out a message saying we are joining up with LBT and start pushing people over.
Let me know your thoughts.
Thanks,
James needed to check with the mods in LBT, and luckily that meant Brittney since she was a mod already, lets just say it helps to know people.
Oh and if you haven't heard of James Q. Quick you should maybe join the other 138K+ people already learning from him!
He gets to hang out with Amy Dutton @selfteachme on Compressed.fm.
Some pretty cool peeps to hang with.
Learn Build Teach
So what is learn, build, teach all about?
The website says it all.
Learn Build Teach is a welcoming and open community of web developers. Whether you are a seasoned expert or just getting started in the field of development, you can be certain our Discord server remains a friendly place to ask questions and engage in discussions.
How can we keep the space a safe place to learn, build, and teach?
More Content
Our main goal in this Discord server change as well as splitting up our YouTube channels is to allow people to find what they come to CodingCat.dev to focus on.
If you love the podcast and learn a lot from our courses and tutorials awesome you will love the CodingCat.dev YouTube Channel!
If you are more interested in the ever evolving tech world and the different companies that support creating the future then the Purrfect.dev YouTube Channel is purrfect for you!
The move to Learn Build Teach will allow us more time to create this great content and allow us a dedicated place to answer questions and get your feedback.
Changing Platforms
There are a few more coming so please be patient as we might snag our claw on something (cat jokes)! We have been busy re-platforming the site so that we can bring on more authors and content creators. In the past we have always run into limitations with either my time constraint on our custom backend, not enough role restrictions on a headless CMS, or hard to create content due to limitations with editors.
We have solved a great deal of this with moving over to Builder.io. We have combined the performance that Next.js gives us with the ability to register custom components directly into a Visual editor.
For example when I want to use either a Cloudinary based image in markdown, a Next/Link, or a YouTube embed. These are all super hard things to accomplish and in the past we would try to use MDX to solve these basic problems. Now we can use our own custom components and see them all visually while creating content.
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/changing-the-channel-for-youtube-engagement
+ ZKdo3vLUqcb4sYreiCMdDe
+ Sun, 20 Mar 2022 04:00:00 GMT
+
+ UPDATE: Moving channel was a bad idea
I thought it was such a great move moving channels as it increased our CTR for educational videos. In reality what happened was it confused our user base. What I found out the hard way was that we needed to keep all of our content in one place and actually update Purrfect.dev to become CodingCat.dev Podcast as well. Now our branding is all surrounding the CodingCat brand going forward, on a single channel.
If you read nothing else in this article
~~We are moving our podcast to a new channel. https://studio.youtube.com/channel/UCD5oDjUZTYM13irzpRjnVeQ~~
I am moving off the company twitter account https://twitter.com/codingcatdev and onto my own account https://twitter.com/codercatdev.
I know pretty close username right, gotta keep on brand.
We want you to write and create for us too, so please DM or email to join us.
Why I made CodingCat.dev
Back when I started CodingCat.dev I wanted a place where people could come and learn web development. What I learned was getting people to come to yet another website for another coding tutorials is really hard!
Feeling still like Kevin Costner in a Field of Dreams, I continue to push.
https://www.youtube.com/embed/NOPTByHw5RA
What was broken
YouTube has this unfortunate feature where it takes into account two things.
Click Through Retention
Unknown block type "image", specify a component for it in the `components.types` option
Average View Duration
Unknown block type "image", specify a component for it in the `components.types` option
You will notice that in our top 5 all time vides for CTR they are all programming videos. Even though Brittney and I have spent around 60 hours on screen, recording the podcast each week. The second part of this shows that our average view is around 3 minutes and 42 seconds on average.
This told us that not only is no one watching the podcast, but they are not finding our other content. We love the podcast and love talking to all the amazing people we get to talk with on it. So we don't want to give it all up and let it go, but we also want people to find all the awesome content that we plan to send out this year too!
How Jeff Delaney taught me to fix it
Jeff has a great video on how he got to 1 million subscribers. It was a great guide for me to understand why we needed to change channels.
https://www.youtube.com/embed/j_q0D_jbMk8?start=99
Whats next?
What we are going to do now is switch from a recorded, cleaned up version of the podcast over to a live version where we still are able to run advertisements realtime.
This will free up all our editing time for content writing time.
The next step is to move to my new company's platform Builder.io. This will allow us to setup custom roles and ease the capability to produce content that we can view live. While allowing some of our custom components from Firebase authentication. It is honestly so amazing what this product can do and it is evolving every day, the team is growing like crazy!
Builder.io is not only going to allow us to have a live preview of the page, but it will let us drop in any React component that we won't with that live preview. Instead of using something like MDX which we still love btw, we can actully drop a full component in while we are creating. There is no reason to have a compile step to run the code and parse on the otherside. We can ship JSON representing all blocks within our page. For now we are still leveraging all the utilities of TailwindCSS, but I would not be surprised if we don't get even closer to raw css and html in case we decided to pickup the next latest framework that is moving the web to the next level like Qwik. In our beta builds I am already seeing massive performance gains using Partytown as well!
So please come join us and sorry for all the trouble while we change gears!
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/how-to-achieve-16-9-resolution-while-using-divvy-on-a-mac
+ ZKdo3vLUqcb4sYreiCMZrG
+ Sat, 19 Mar 2022 04:00:00 GMT
+
+ <script lang="ts"> import YouTube from '$lib/components/content/YouTube.svelte'; </script>
Have you ever tried to record a video and notice that it is a few pixels off? Lets fix that so you can produce good quality videos!
Easy Solution
So before I go down this crazy exercise, I do want to call out one of the simpiliest tricks to when recording videos. Just hide the menubar on MacOS.
Unknown block type "image", specify a component for it in the `components.types` option
Is this still a dev post?
Most of the time as a developer I am looking for cool side projects. It just so happens that I have been looking for an excuse to try out Remix as we have been looking to partner with them at Builder.
What I wanted to do was create a simple tool that would calculate what to set Divvy's margins to so that I can capture my screen correctly for recording both at Builder.io and CodingCat.dev.
I wanted to have a little fun with it and added a downloadable background for you computer that has the menubar set, the margins called out, and the amount of boxes you are using within Divvy add by using Cloudinary's React SDK and overlays.
Dont care about the math? Cool I don't really either but I needed to understand it to write this purrfect Divvy Calculator. See something broken, PR's more than welcome. GitHub Repo
Quick example of the Cloudinary transformation code below.
Unknown block type "code", specify a component for it in the `components.types` option
MacOS and the menu bar
When you are using a display on a Mac the windows all stay under the menu bar at the top. So your beautify 4K monitor that you are recording on will be in a purrfect 56.25% ratio (2160/3840) also commonly referred to as 16:9 (9/16).
So if you screen is 4K 3840px by 2160px, and your menu is 24px high then your actual usable height is 2160px - 24px => 2136px. Which of course means it will be 3840px by 2136px so if we divide 2136/3840.
How Divvy Window Manager Works
Divvy allows you to break your monitor up into sections columns by rows shown below.
Unknown block type "image", specify a component for it in the `components.types` option
This is an amazingly simple, but yet very powerful tool. You can break your screen up into anything from 1x1 all the way to 10x10 blocks. There are a lot more features check out the below video to see more of what is possible.
<YouTube src="https://youtu.be/Z6eBPC-4ZOY" title="Divvy Blocks" />
What is the big deal
So if you are using Divvy Window Manager and placing your windows into 4 equal locations, what this really means is that you will end up with a ratio closer to 55.625%. This small difference of only .625 might not seem like much but when you are producing a 4K screen capture those 40+ pixels really show up (trust me)!
Unknown block type "image", specify a component for it in the `components.types` option
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/the-basics-of-big-data-for-web-developers
+ ZKdo3vLUqcb4sYreiCMQua
+ Fri, 18 Mar 2022 04:00:00 GMT
+
+ Pursuing a career in web development can be exciting and incredibly lucrative. In fact, The modern world is more reliant on technology than ever before, to the point that tech defines much of our day-to-day lifestyle. It should come as no surprise then that we created nearly 65 zettabytes of data in 2020 alone — with each zettabyte equivalent to a trillion gigabytes.
This information can provide so much critical insight on how we think, feel, and act. This is ultimately why leveraging Big Data is now vital in everything from business analytics to marketing. And now, even web developers can use Big Data to make more strategic adjustments in their line of work. Here's how.
It enhances the user experience
With customizable websites, a given set of options allows users to control everything from layout to system functionality. However, the options don't facilitate a seamless experience that caters to users' specific preferences.
In the book Everybody Lies, former Google data scientist Seth Stephens-Davidowitz writes that the nearly 8 trillion gigabytes of data we produce each day uncovers both the conscious and unconscious decisions people make on the internet. This information can be tracked with AI models for the purposes of modifying sites to each user's liking. Retail giant Amazon exemplifies how this practice can go: It analyzes previous search behavior such that users receive personalized product recommendations instead of having to continually browse the vast database.
It automates your updates
Big Data can streamline your workflow by facilitating the automation of manual tasks. Gone are the days of wondering what users want to see in your next website update. Software developer Al Sweigart writes in Automate The Boring Stuff With Python that you can instead set up data streams that automatically collect data points for you. These points can include information from your website itself or feedback indirectly given by users on the internet. They can then be automatically compiled in an upgrade module for immediate release.
Software like WordPress allows site administrators to automatically implement updates including security and bug fixes, as well as new feature rollouts.
It improves traffic analysis
Big Data also enables you to get comprehensive information on a vast array of variables, which comes in handy when you're looking to drive more traffic to a website. By using third-party traffic analysis tools like Google Analytics (or creating your own such tools), you can track valuable website metrics more efficiently.
According to Entrepreneur, these metrics can include page views, average visitor duration, total unique visitors, and bounce rate. Big Data can even provide enough information for heat map analysis, which helps to determine which parts of your site are visited the most and least frequently. You can then add, remove, or enhance features that better cater to visitors' interests.
It reduces costs
In a recent article titled Cost to Develop a Website in 2022: The Complete Guide, B2B development company Imaginovation estimated that web development costs can run anywhere between $1,000 and $95,000. Since Big Data helps streamline a variety of web development processes however, it can lower these costs significantly both in and outside web development.
Big Data informs fast and accurate decision-making and even predicts trends that may affect you in the long term –– so you won't make expensive mistakes, and you'll use resources more wisely. For example, data collected from Uber's flagship ride-sharing service was used to improve the efficiency of its UberEats delivery app.
Disrupting web development?
Many have theorized that since Big Data allows for more automation in web development, it's also making the profession obsolete. In fact, the opposite is true: It is instead spurring demand for experts proficient in both basic web development and data analytics.
If you've been experiencing Imposter Syndrome as a web developer, we've previously mentioned that one way you can combat it is to challenge yourself to improve. Updating your skillset with knowledge on how to leverage Big Data will unlock the benefits mentioned above, give you better career prospects, and allow you to make the best websites possible.
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/reviewing-2021-and-predicting-2022
+ ZKdo3vLUqcb4sYreiCMVSW
+ Sat, 01 Jan 2022 05:00:00 GMT
+
+ What technology we see coming for 2022
Newsletter’s you should follow
What was hot in 2021?
- Vite took over
- Rust explosion (WASM)
- Crypto, blockchain, web3
State of JS 2021
I hope this link works eventually, for the meantime checkout 2020.
2021 - Recap
March
April
May
June
July
- This one hit late so I put it in July - SolidJS releases version 1.0.0. Never heard of it checkout the perf chart Ryan Carniato wroteup.
- Qwik finally becomes a thing with a big update version 0.0.12. We got to record with Miško Hevery in Qwik the HTML First Framework, checkout the Demo in that one at minute 19:30.
- GitHub Copilot was released in Technical Preview and half of all our jobs were lost. It seemed harmless at first.
- Svelte Storm was announced, pretty sure you will have to ask Brittney what this is all about.
- GitHub comes out with another banger, GitHub Code Spaces. These are fast VM’s (maybe faster than your Mac, well at least my son’s Mac) that you can run VSCode in. Stay on those individual accounts though, otherwise hanging out and searching Stack Overflow could cost you a pretty penny. Oh yeah and did I say banger, see why in the Newsletters section below.
- Alert Chrome broke the web, then rolled it back (this might happen again) https://bugs.chromium.org/p/chromium/issues/detail?id=1065085#c72
September
- Brittney found her dream job at Grainger writing Svelte components.
- Builder.io raises 14M in series A funding and caught my attention ( yes I somehow convinced them I can be a good DevRel) - https://www.builder.io/blog/14m-series-a-funding-builder
- Gatsby adds Deferred Static Generation and is 10x faster in the 4.0 release - https://www.gatsbyjs.com/blog/whats-new-in-gatsby-4/
- Meta, or Metaverse, or this weird world we are living in - https://about.fb.com/news/2021/10/facebook-company-is-now-meta/
- Alex Starts at Builder.io and is ready to make devs lives easier by providing all those marketing peeps a visual no-code tool that can be exported to your favorite framework.
- Brittney starts Svelte Sirens, A Svelte society for women, non-binary people, and allies - https://sveltesirens.dev/
- Well the Angular team was not kidding, good bye View Engine! Angular version 13 removes View Engine, standardized on ES2020, no more
ngcc , good bye IE11 (so happy for this), and components no longer need ComopnentFactoryResolver ! https://blog.angular.io/angular-v13-is-now-available-cce66f7bc296 - Ionic a cross-platform app tool, built on web tech released 6.0 - https://ionicframework.com/blog/announcing-ionic-6/
I am sure I am missing a million things, feel free to DM me and I will happily update 😸
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/reflections-from-2021-and-goals-for-2022
+ ZKdo3vLUqcb4sYreiCMW3I
+ Fri, 31 Dec 2021 05:00:00 GMT
+
+ The year of 2021 has been the biggest year of my career. It is sometimes difficult to talk about success when the entire world is struggling and you can really see your privilage showing. However, it is still important to talk about our wins and our losses and to be proud of what you have accomplished.
I set some lofty goals after watching Zero to Mastery's Future Proof Yourself Workshop early in 2021. My big goals were to start a business of my own and to make more money than I had in any previous career, I didn't care how I did it (freelancing, courses, new job), just that I got it done. Well, I am happy to say, not only did I do it, I blew it away. I did some freelance work early in the year, filed for my LLC, bDesignedDev in July, and at the time got my dream job at Grainger, building design systems with Svelte. I also did things I didn't even think were possible this year. I started co-hosting Purrfect.dev with Alex and got to talk to some incredible people and learned so much from this opportunity. It made me more comfortable speaking in public and helped me grow as a developer from the things I was learning about.
Communities
I dove hard into communities this year, especially my Twitter where I broke 1k followers and Discord communities like Party Corgi, Learn Build Teach, Svelte, our own Coding Cat discord, and recently found Frontend Horse and the new Jamstack community. This was honestly my key to success and staying sane in a year where it wasn't safe to go out and make friends. I found freelance clients, podcast guests, networked, got help, met many amazing people, worked on lots of open source, and have several places to chat with those I can call my friends. Communities are such a powerful thing in the dev world if you treat them right, participate, be kind, and help where you can.
Learn Build Teach
This philosophy from James Q. Quick that he doesn't want to take credit for and the name of the Discord Community he created, Learn Build Teach, is exactly how I got where I am today. This idea isn't new, in fact it pulls a lot from Learning in Public a blog post by Swyx. In essence, it just means that learning isn't the end of your journey, but the beginning. By building things out of what you've learned and then passing that knowledge on through teaching others, you end up learning more than you ever imagined. James had me on Learning Quick right after I got hired to talk about why I love Svelte so much and I talked about how this philosophy really helped put me where I am today.
Svelte Community
Of all the communities I am apart of, I feel like the Svelte community has really been the place I've come to love the most. I've had a love affair with Svelte since 2019, when Scott Tolinski talked about it on Syntax.fm. The people are friendly and helpful if you run into issues, but they are especially involved in improving and making the community better. We had Swyx on Purrfect.dev What's up with Svelte in April and after the podcast he mentioned this idea he had for a women's group in the Svelte community, Svete Sirens. He asked me if I would be interested in spearheading it and at the time, I turned him down. I thought I had too much going on and couldn't fit it into my busy schedule. Fast forward a few months and I asked him if anyone had taken him up on the offer. No one had, so I took the opportunity to say okay, I have no idea what I'm doing, but I can do this! I bought up SvelteSirens.dev and started rounding up people to help me. I found 3 co-founders and planned to do a monthly meetup/talk on the Svelte Society YouTube Channel. We quickly put things together and decided to officially announce the new community at Svelte Summit in the fall. Swyx had put together a watch party in New York and very last minute a lot of us decided to fly out. Rich Harris had just been hired by Vercel and Guillermo decided to fly out a few members of the team to join the watch party as well, including one of my co-founders, Steph Dietz. I flew in and out of New York in less than 24 hours, but this was the biggest opportunity I had all year. I met some of the most inspirational people to me, 2 of my co-founders of Svelte Sirens, and many of my other community friends. Hopefully this will pave the way for the future of the Svelte Sirens, we already have lots of things in the pipeline for 2022.
2022 Expectations
Of course I have to be optimistic and think that 2022 will be the best year for my career yet. I'm setting lots of goals already and really focusing on the shorter term goals this year as well as some bigger long term ones. I have the regular old ones like read more, lose weight, work on myself, be present, but hopefully by getting down to the details and setting really short term goals I will be able to achieve more than I ever have. For January, I am planning an Intro to Svelte couse on CodingCat.dev and to start out my goal of livestreaming every 2 weeks for the Svelte Sirens. My big goals for this year are to put out some kind of content every month on Coding Cat, including some paid courses, and to really focus on the Svelte Sirens community and making it a place that everyone wants to come and be apart of! I hope you all have a happy new year and truly have your best year yet!
]]>
+ Brittney Postma
+
+
+ -
+
+ https://codingcat.dev/post/make-pathing-easier-with-aliases-in-sveltekit
+ ZKdo3vLUqcb4sYreiCMWUs
+ Fri, 05 Nov 2021 04:00:00 GMT
+
+ If you haven't heard about SvelteKit yet, go checkout the beautiful site over at kit.svelte.dev. SvelteKit is a component framework similar to React and Vue with one key difference, there is no virtual DOM. Svelte is a compiler that builds itself away into a sleek and fast end user experience. If you haven't tried Svelte or SvelteKit before, you can check out my quick intro tutorial where we build a blog in 30 minutes 🤯
Links:
TLDR: To setup an alias add the following lines to the svelte.config.js. For the code editor you are using to understand the alias, we also need to add to the jsconfig.json paths with the aliases you want to create.
Unknown block type "code", specify a component for it in the `components.types` option
Unknown block type "code", specify a component for it in the `components.types` option
Default Starter
To start a new SvelteKit project run npm init svelte@next app-name into the terminal where you want the folder to live. Change directory into your new app, cd app-name and run npm i to install the dependencies. Out of the box SvelteKit provides a $lib alias setup for the src/lib folder and a number of modules are available from $app and $service-worker. Outside of those, it is up to us to set up our own aliases and preferences on how to use them. These all use the $ syntax to use it, however, we are able to change the lib folder to @ or other symbol if preferred.
Change default alias
To update the default $lib alias to @lib, we need to tell vite that we want to use it by updating the svelte.config.js and the alias will work in our application. For the code editor to understand the alias, we need to edit the jsconfig.json file. Inside the svelte.config.js by default we see this setup.
Unknown block type "code", specify a component for it in the `components.types` option
Inside of the kit property, we need to add a vite config for the aliases we are setting up. I prefer the @ symbol and will show how to setup the @lib, @components, and @utils aliases. We need to import the included path module from node at the top and add the vite property under the kit property. The new svelte.config.js will look like this.
Unknown block type "code", specify a component for it in the `components.types` option
Next, we also need to set them up inside the jsconfig.json file. By default that looks like this.
Unknown block type "code", specify a component for it in the `components.types` option
As you can see, the default $lib is added to this config already. We need to update that and add the paths for our other two aliases. The updated file will look like this.
Unknown block type "code", specify a component for it in the `components.types` option
Under paths we have added the @components, updated the $lib to @lib, and added @utils with the corresponding paths.
Using the aliases
Now we need to use the aliases inside of a file. The skeleton starter doesn't come with any folders outside of the routes folder for the routes of the application. If you chose this option, you can simply add the lib folder inside of the src folder to use the @lib alias. The other paths and aliases we setup are nested inside the lib folder. Add a components folder and utils folder inside of the lib folder. The folder structure should look like this.
Unknown block type "code", specify a component for it in the `components.types` option
Now any files created inside of lib, components, or utils can be imported using the alias rather than having to type out the absolute path, like so.
And that's it 🥳 now you can use all of the aliases we created or create more of your own. I'm @brittneypostma on Twitter if you have more questions or just want to chat about Svelte 😃 Cheers!
]]>
+ Brittney Postma
+
+
+ -
+
+ https://codingcat.dev/post/the-7-strategies-i-used-to-defeat-imposter-syndrome
+ ZKdo3vLUqcb4sYreiCMRG2
+ Sun, 12 Sep 2021 04:00:00 GMT
+
+ Imposter Syndrome is a term that was coined in 1978. It means someone who is insecure about their skills and accomplishments, worried that they are not qualified enough to succeed.
This feeling of self-doubt can affect anyone at any time. But what's so interesting is how it disproportionately affects women, minorities, and people who are the first to do something in their field.
Since Imposter Syndrome can affect so many people, it's important to know how to deal with this problem if you're feeling it yourself or if you're trying to help someone else who's dealing with it.
The 7 Strategies I Used To Defeat Imposter Syndrome:
- Know that the feelings are real
- Accept your feelings
- Learn from those feelings
- Push Yourself
- Fake it until you make it
- You can’t know everything (that’s what Google is for)
- Reward yourself
Know that the Feelings are Real
Often you will find you tell yourself a story. You will be sitting in an interview with such high expectations of yourself that you start to completely loose confidence. Or you won’t even want to start an interview because you have already told yourself you are not good enough. These are feelings that everyone has from time to time.
Accept Your Feelings that You Get From Imposter Syndrome
The following is an excerpt from a blog post by Michelle Zarrillo.
"I don’t know when I started to feel confident in my writing, but I always felt like something was off. Like maybe I didn’t deserve it or maybe I would get found out."
This is an example of how Imposter Syndrome can affect your feelings on your own ability.
It is not uncommon for people to feel this way. One study revealed that more than 70% of students have had this feeling while they are pursuing their studies in college or university. This can be caused by the pressure of getting high grades, wanting to impress others, the desire to be the best at what you do, and more.
The key here is to accept that this will happen and learn how to push through this mindset.
Learn From Feeling Like an Imposter
There will be moments in your life when you will feel like an idiot. This happens to everyone at some point in their life, but it doesn’t necessarily mean that you are an idiot.
The key here is to know the difference. If you are applying for a frontend development job and someone starts asking you about Rust, most likely it is okay to feel this way. Just be very open and say something like “I don’t know Rust, isn’t that a backend language”.
A sense of belonging fosters confidence. When people feel connected to their community, they are more likely to be confident in themselves and their abilities. Join groups like codingcat.dev and ask what you should prepare for in an interview.
Push Yourself
Regularly challenge yourself to do something that scares you.
To feel alive, we must consistently challenge ourselves to do things that scare us. These challenges can be mental, physical or emotional. Experiencing fear is a sign of progress and shows that you are actively living life to the fullest. Life is an adventure and it is our responsibility to make sure we are doing all we can to make it a good one.
For instance – You might not know Rust, but if you really want to get good at it start practicing. You will struggle and it will be painful, but that is okay this is how we learn and grow.
Fake It Until You Make It
Sometimes there are no shortcuts to becoming more confident in your role, but if you fake it at first, and see and hear yourself doing well, eventually that will turn into actual confidence.
Listen I am a complete introvert at heart. For me, there are times when I go into full meltdown mode when we started doing the podcast. I would get in my head like, “people hate me, why would they listen to this mumbling dude”.
I just started doing it and as I kept doing it those self-talk items that I talked about above and mentioned again here about the podcast, they start to melt away. It is still true there are people that don’t like my style and this is okay!
You can’t know everything (that’s what Google is for)
This topic focuses on the fact that there is no need for us to know everything. Every time we think of something, we can find an answer to our question online. If we don't find the answer online, there's always Google and get close and then build knowledge around it.
We cannot know everything and that is not a bad thing because it leaves some space for us to learn and explore new things.
We should not be judged by what we do not know, but instead by what we do know and how we go about learning new things.
The biggest piece of advice I try to give junior devs is: “It is okay to say I don’t know”. Sure you can fake it until you make it but if you are in an interview be general, but if it gets specific it is totally fine.
Reward Yourself
Do you have a reward system for yourself after completing a difficult task, finishing a work assignment, or achieving some other objective? If so, what are your rewards?
If you only Push Yourself, you will eventually burn out and then you will get back into the self-talk cycle. Take time to accept your successes! Go to that movie you have been wanting to see. Read that book that everyone is talking about. Run a 5k.
Just make sure it is something that isn’t going to make you feel bad about yourself!
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/accessibility-best-practices-for-html-and-css
+ LzgcvqoNSwz5R8loyka1S9
+ Thu, 19 Aug 2021 04:00:00 GMT
+
+ Accessibility Best Practices for HTML & CSS
Great accessibility is not always an easy task and goes much more in depth than what is listed here. I still recommend having experienced screen readers and accessibility experts test your site and run your site through the WebAIM Web Accessibility Evaluation Tool, WAVE. If you see anything that should be added, updated, or removed, please get in touch with me either in the comments or @brittneypostma on Twitter. This article is here to serve as a starting point to summarize and guide your way to better accessibility. Having better accessibility isn't only the right thing to do, it also increases your Search Engine Optimization, SEO, which can rank you higher in Google's algorithm. Here are some links to provide more information on web accessibility.
Additional Links
Foundation
Everything should start with a good foundation and with Hypertext Markup Language, HTML, that is the structure of your page. A huge part of web accessibility is just making sure the HTML elements are used correctly. A web page may be styled in many different ways to organize information visually, especially with different viewport sizes, but a blind user only hears the order defined in the HTML of a page. In my opinion, using semantic HTML along with mobile-first design allows you to see the layout of a page linearized into one column and the order of the markup to see if it makes sense.
Semantic HTML
The word semantic relates to the meaning of something in a language or logic. In semantic HTML the HTML element used gives meaning to define its content and how it is classified. There are currently over one hundred different elements in HTML, but not all of them have a semantic meaning. The layout of the page defines the page regions that are identified by web browsers and assistive technologies.
Structural
- Use the
header and footer elements to define the top and bottom of a page, but also can define the top and bottom of an article or section. - Use the
nav element for major blocks of links to allow assistive technologies to quickly and easily navigate through the site. Thenav element can be used around unordered lists, ul, or around a block of content referring to multiple links. You can use the aria-label property to identify different navigation areas. - Use the
main tag to define the main content of the page and aside to define anything related to the main content. - Only use
divs and spans for styling purposes. Use the section tag to group related elements with a heading and the article tag to group a self-contained item that is independent or reusable. The article element can be posts, news articles, product cards, comments, or any other form of independent items or content. Here is a great Smashing Magazine article on Why You Should Choose HTML5 article Over section explaining that article can be used in almost all instances, but a section with an aria-label can be helpful. - Use the correct levels for headings, from the most important as an
h1 down through h6 and do not skip heading levels. Each page should have a heading level 1, h1 , that represents the most important idea on the page. Any sub-headings should be an h2 , sub-sections can then be divided into h3s and on down to h6 s based on the nested structure. - Use the paragraph,
p, element to wrap self-contained blocks of text that focus on a single topic or theme. Also, the strong and emphasized, em, elements have semantic meaning where bold, b, and italic, i, are simply style differences. - Use an
alt attribute on images if the image is important to the content, such as information to understanding or interacting with something. Otherwise, if an image is decorative and adds no value to the content, the attribute can be declared alt="" and it will not be read by assistive technologies. - Try to provide alternative to audio and video content, such as captions or transcripts for those that are hard of hearing or using screen readers.
- Use the anchor,
a, tag for links that go somewhere else and the button tag for an action like submitting a form or a click event. - Don't use dashes if you can avoid it. Instead of writing 5–7, write 5 to 7.
- Expand abbreviations — instead of writing Jan, write January.
- Expand acronyms, at least once or twice. Instead of writing HTML in the first instance, write Hypertext Markup Language.
Forms
- Forms are one of the most common problems for accessibility on sites. Make sure not to disable any of the default keyboard functionalities or tab order and use the proper markup. Also, consider keeping forms short and only ask users for the minimal information needed to complete the process. https://www.w3.org/WAI/tutorials/forms/
- Use the
label element with a for attribute that matches an id attribute on the accompanying input or wrap the label around the input. https://www.w3.org/WAI/tutorials/forms/labels/ - Make sure instructions needed to complete an input are listed before the input area and make clear if an input is required and any validation requirements. Make any errors that prevent completing a form known to the user.
Styling
Styling, whether with CSS or JavaScript, can cause accessibility issues to occur when some browser defaults are removed or incorrectly changed. Including heading and text sizes, outline styling, color contrast, and more. The best practice is to leave the browser defaults if you don't know how to correctly restyle them. I've listed some of the most common issues below. https://developer.mozilla.org/en-US/docs/Learn/Accessibility/CSS_and_JavaScript
- There should be sufficient contrast between any foreground text and the background color behind it. The minimum contrast ratio for normal sized text is 4:5:1 and 3:1 for larger text. These as AA minimum contrast criteria.
- There are enhanced AAA criteria at 7:1 for regular text and 4:5:1 for larger text. There are many extensions and plugins that check for contrast issues, but they can be inaccurate and inconsistent. The WebAIM provides a contrast checker at https://webaim.org/resources/contrastchecker/.
- For non-text items the contrast ratio should be a minimum of 3:1.
States
- The focus state shows when a user is focused, either by click, keyboard, or voice, on an element. Only interactive elements need to have a focus state. A common practice is to remove the default outline for focus states with CSS, never do this without replacing it. A background should be 3:1 contrast ratio for buttons and inputs and a minimum 1.4.3 underline for links. More information and examples are listed here https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast.html.
- The hover and active states are sometimes styled similarly. The active state shows when something is activated, usually a button or a link, and the hover shows when it is being hovered on. The styles are not the important part for accessibility on these elements, but telling assistive technologies when an element is active is important. Using the correct aria-pressed attribute based on the state will tell a user whether the element is active or not. More info on states can be found here https://www.w3.org/WAI/tutorials/menus/styling/#hover-and-focus-states.
Animations
- Users should be able to control any moving, flashing, or blinking content on a page. Examples include ads, videos, auto-updating visuals, moving backgrounds, and more. Basically, anything that is initiated by the page without the user interacting with it. A good practice is to use a prefers-reduced-motion media feature to turn off animations. If the animation is critical to understanding something, add it back in with a selector after turning down all animations. https://css-tricks.com/revisiting-prefers-reduced-motion-the-reduced-motion-media-query/
Unknown block type "code", specify a component for it in the `components.types` option
By no means is this the be all, end all of things that should be done for accessibility. It is an ongoing, ever-evolving process that is an extremely important aspect for inclusion and usability. These are some of the core fundamentals of good User Experience, UX, and helping others understand, navigate, and interact with your site. At a bare minimum, follow these best practices and run your site through the WebAIM Web Accessibility Evaluation Tool, WAVE. If you see anything that should be added, updated, or removed, please get in touch with me either in the comments or @brittneypostma on Twitter.
]]>
+ Brittney Postma
+
+
+ -
+
+ https://codingcat.dev/post/finding-a-company-with-great-values
+ ZKdo3vLUqcb4sYreiCMbda
+ Mon, 19 Jul 2021 04:00:00 GMT
+
+ Why Company Values Are Important
There are many companies out there that have values, that often are just placed there by Human Resources because they felt they “should” have values. Then there are other companies that build their entire way of working around their values. For instance lets take a look at Google’s Mission, Vision and Values.
Google Mission Statement
Organize the world's information and make it universally accessible and useful
Google Vision Statement
To provide access to the world’s information in one click
Google Values
- Great isn’t good enough.
- Focus on the user, all else will follow.
- It’s best to do one thing really well.
- Fast is better than slow.
- Democracy on the web works.
- You can make money without doing evil.
- There’s always more information.
- The need for information crosses all borders.
- You can be serious without a suit.
- You don’t need to be at your desk.
These are great goals and I especially think that the "You can make money without doing evil." is one to highlight. Although you might want to take a pause as these goals seem to put the companies success before the employee, just make sure you consider this before taking a job.
Below you will find Amazon's Mission and Values. I have heard this repeatedly that Amazon's focus on the customer is second probably only to a greeter at Walt Disney. It is important to note that there are some good values in here. I especially like "Hire the Best" and "Learn and Be Curious".
Amazon Mission Statement
"Amazon is guided by four principles: customer obsession rather than competitor focus, passion for invention, commitment to operational excellence, and long-term thinking. Customer reviews, 1-Click shopping, personalized recommendations, Prime, Fulfillment by Amazon, AWS, Kindle Direct Publishing, Kindle, Fire tablets, Fire TV, Amazon Echo, and Alexa are some of the products and services pioneered by Amazon."
Amazon Vision Statement
Our vision is to be earth's most customer-centric company; to build a place where people can come to find and discover anything they might want to buy online.
Amazon Values
- Customer Obsession
- Ownership
- Invent and Simplify
- Learn and Be Curious
- Hire the Best
- The Highest Standards
- Think Big
- Bias for Action
- Earn Trust
- Deliver Results
The large FANG companies above are only an example that represent GIANT companies. There are a lot of startups, small companies, universities, and non-profits that allow for a much greater focus on those working for the company and the values they are trying to uphold. Don't get stuck on just the paycheck, make sure you feel like you are appreciated at work, it will make up 2,080 hours (if you are lucky) every single year of your life.
Why I chose to work at OST
Many don’t know that my day job is at an amazing company called OST (Open Systems Technologies) in Grand Rapids Michigan. The reason that I decided to work at OST was their company culture and values. At OST we have a blue card that is given to every employee that lists our values. Our slogan below is what we live by.
We honor our people and their families first, clients second and the rest will fall into place. ~OST
OST’s Five Guiding Principles
- Honor - our employees and their families.
- Delight - our clients.
- Serve - with humility.
- Embrace - entrepreneurship and innovation.
- Learn - through curiosity and empathy.
Find What Works for You
If you are just getting started you most likely are prioritizing your goals around how big your salary will be, health care, 401k. Basically your overall compensation is what you will be looking to examine. However, when you only focus on this as your number one goal it can cause you pain down the road.
For instance during the 2020 pandemic many found their job was eliminated because their company only cared about hitting a specific Gross Margin, Revenue, or Owner's Equity number. I would caution against any company that is not putting their employees first. Make sure you do your research and see how a company reacted to the events this past year. Don't be afraid to ask this in your interview as well.
Remember you are a valuable resource in the technology field. There are more jobs than people available and you should be treated as that valued resource!
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/nextjs-11-features
+ ZKdo3vLUqcb4sYreiCMWLg
+ Wed, 14 Jul 2021 04:00:00 GMT
+
+ Please take note the above video is not codingcat.dev's. I thought it was important to see the announcement in its full context.
In research for my next (or should I say Next.js) series of courses I wanted to dive back into Next.js 11's new features. The items that really blew me away were best summarized by one of my favorite guest on our podcast Swyx in this tweet.
- Collaborative preview edits (Next.js Live)
- "Conformance" perf guidelines
<Image> size detection<Script> loading strategy- Automatic Font optimization
Collaborative preview edits
This is an interesting one to call a Next.js feature, one could argue that it is a Vercel feature but I am really splitting hairs here because the server that you need is in Next.js but the account you most likely will use is Vercel in the near future. You have most likely seen this same type of collaboration if you have worked on Figma or Miro. This tends to stay in the design phase of how we work at OST in the "Design. Architect. Build. Run." experience. It is rarely something that you are seeing collaborated with on a live site that you are running code behind. I think this will bring together the Design, Architect, and Build phases of the project.
Conformance
Conformance is a system that provides carefully crafted solutions and rules to support optimal loading and Core Web Vitals, with further additions coming to support other quality aspects like security and accessibility.
Basically this is the thing every developer wants without having to work so hard to achieve it. Next.js 11 will support ESLint directly so it will find any framework-specific issues during development. I am excited about the Core Web Vitals but I can't wait to see what the team has in store for Security and A11y!
Make sure to checkout Google's plans for project Aurora and specifically their blog on Conformance in Next.js.
Image Size Detection
For those of you not using the already built in feature for Image Providers like I talk about on Media Jams, this feature is amazing! It will allow you to import your local images and it will automagically determine width and height.
Unknown block type "code", specify a component for it in the `components.types` option
Something to also note is the cool placeholders you can add now as well using placeholder="blur". You can read more about Image Imports in Next.js Docs
Script Loading Strategy
This new Script Optimization is pretty bananas just like Gwen always said!
<iframe src="https://giphy.com/embed/q1VYLPcIVNzH2" width="480" height="360" frameBorder="0" class="giphy-embed" allowFullScreen></iframe><p><a href="https://giphy.com/gifs/gwen-stefani-gif-bananas-q1VYLPcIVNzH2">via GIPHY</a></p>
If you are a developer you 100% have run into that analytics package or ad requirement that you must use external scripts. They are typically a performance nightmare and often go unseen until your app is out in the public view. Now you can use the new next/script tag and define the appropriate strategy. There are three strategies involved and broken down very easily
beforeInteractive: For critical scripts that need to be fetched and executed before the page is interactive, such as bot detection and consent management. These scripts are injected into the initial HTML from the server and run before self-bundled JavaScript is executed.afterInteractive (default): For scripts that can fetch and execute after the page is interactive, such as tag managers and analytics. These scripts are injected on the client-side and will run after hydration.lazyOnload For scripts that can wait to load during idle time, such as chat support and social media widgets.
You can read all about it in the Next.js 11 blog post on Script Optimization or in the Next.js Docs.
Automatic Font Optimization
Okay technically this one was added in Next.js 10.2 but it was talked about in the conference and I for sure didn't know about it! There are several methods to include web font loading into your system. You can create them at build time and reference, have TailwindCSS do it for you like we do, or add the link manually. It will now look like the below code.
Unknown block type "code", specify a component for it in the `components.types` option
The great part about this optimization especially in your static sites during build time Next.js optimizes and inlines font CSS at build time which will eliminate an extra round trip to fetch font declarations. This should also give you a better FCP and LCP. Checkout the Next.js Docs for more information on Font Optimization.
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/aws-amplify-vs-aws-cdk-2020
+ LzgcvqoNSwz5R8loyka06X
+ Wed, 18 Nov 2020 05:00:00 GMT
+
+ What are AWS Amplify and AWS CDK?
First things first acronyms are always a tough start to a post so lets clear up a few of them.
AWS's description for the AWS CDK
The AWS Cloud Development Kit (AWS CDK) is an open source software development framework to define your cloud application resources using familiar programming languages.
https://aws.amazon.com/cdk/
AWS's description for AWS Amplify
AWS Amplify is a set of products and tools that enables mobile and front-end web developers to build and deploy secure, scalable full stack applications, powered by AWS. With Amplify, you can configure app backends in minutes, connect them to your app in just a few lines of code, and deploy static web apps in three steps. Get to market faster with AWS Amplify.
https://aws.amazon.com/amplify/
At first glance it would seems like these two products are not related at all. However both are tools that you can use to build cloud applications while maintaining the resource definitions in your code, what we call Infrastructure as Code (IaC). There are several tools that help with IaC across many cloud platforms, but especially with AWS and we will cover those in further details.
The AWS CDK was released at AWS New Your Summit 2019 which you can watch Martin Beeby talk about in the below YouTube video.
https://youtu.be/bz4jTx4v-l8
The most amazing part about the CDK is that you can write your definitions in the language of your choice (well of the supported choices). At the time of this article the supported languages are TypeScript, JavaScript, Python, Java, and C#.
Developers can use one of the supported programming languages to define reusable cloud components known as Constructs. You compose these together into Stacks and Apps.
Below you can see it is very readable in your IDE
Unknown block type "image", specify a component for it in the `components.types` option
From: https://docs.aws.amazon.com/cdk/latest/guide/home.html
To get started all you have to do is install the AWS CDK globally with npm like below.
Once you do this I would suggest reading through the getting started guide that AWS provides.
AWS Amplify
AWS Amplify was released in 2018, probably the best example of the release I could find was from Nader Dabit, Senior Developer Advocate at Amazon Web Services.
https://youtu.be/yp3nNcqH5is
I had an opportunity to sit down with Nader on Purrfect.dev to discuss how AWS Amplify fits into a developer's ecosystem. You can see the video below
https://youtu.be/kUM8Zcb369U
Or please subscribe to the podcast and listen.
<iframe src="https://anchor.fm/purrfect-dev/embed/episodes/0-10---Amplify-with-Nader-Dabit-edvjpb/a-a3lu891" width="400px" height="102px" frameborder="0" scrolling="no"></iframe>
When you start using AWS Amplify you will notice that it is geared more towards the entire platform to be supported. Making it very simple for a Web or Mobile developer to get started very easy. The configurations for your backend are done mainly through the CLI and some configurations on your GraphQL (or REST) definitions.
Why do they both exist?
What is similar between AWS CDK and AWS Amplify is that they both create CloudFormation files and deploy using AWS CloudFormation. This is the key when comparing them as Infrastructure as Code tools. However where the difference lies is the focus for each tool.
AWS Amplify
AWS Amplify is all about mobile and web apps and has a very specific setup to get you writing your web or mobile application fast! It does this by using 3 components libraries, UI components, and a CLI toolchain. What Amplify does is allow for creating the below services very easily.
User registration & authentication
Offline synchronization & conflict resolution
Access data from multiple data sources
Manage user content
Collect analytics data for your app
AI/ML including text translations
Create conversational chatbots
Send targeted communications
Manage messaging & subscriptions
Much of this is done via the CLI and one comment that I hear a lot is that there is too much hidden "magic" that is happening when using the CLI to create all of these configurations.
If you know for a fact that your next application is going to need tools outside of the ones listed above I would look to leverage another IaC like AWS CDK. The biggest thing is going to be developer happiness and testability within your application. If you spend all day fighting with customized CloudFormation written in customized JSON resources in AWS Amplify your developers are probably going to start to dislike the maintenance of the entire Amplify solution.
The wonderful part about both AWS Amplify and AWS CDK is that they are both powered by CloudFormation, so you get all the benefits of CloudFormation, including repeatable deployment, easy rollback, and drift detection.
Both tools also allow you to develop infrastructure and runtime code together.
One of the biggest things that I believe AWS CDK has that Amplify does not are the idea of Constructs. Constructs are vetted architecture patterns, available as an open-source extension of the AWS Cloud Development Kit (CDK), that can be easily assembled declaratively to create a production-ready workload. You can find an entire AWS Solutions Constructs repository.
So a great example of using the CDK over Amplify would be if you are developing an IoT solution. This isn't a great fit for Amplify but below are all the Constructs that will get you started quickly.
Unknown block type "image", specify a component for it in the `components.types` option
There is a great list of features still coming on the AWS CDK roadmap.
So is AWS Amplify or AWS CDK the most powerful in 2020?
I know you read through all of the above and you did your best to already formulate an opinion and that was my goal. Don't let the tools stop you from creating exactly what you need, make sure you list out what you are trying to accomplish and then you can easily determine what IaC will work best for you. I believe as long as you have your Infrastructure in some type of code and it allows you to repeat your work easily, that is going to be the best solution for you.
WHY NOT BOTH??
Better yet if you are looking to start with AWS Amplify and then continue building on top of this base you absolutely can! Checkout Nader Dabit's Mixing Amplify with CDK - Building an Authenticated GraphQL API with TypeScript on AWS
https://youtu.be/rjiiNpJzOYk
Alternative IaC Tools
Ernesto Garbarino has a great article breaking down Infrastructure as Code over on Spacelift. He breaks down the benefits and disadvantages of IaC. There is also a great list of tools for SCM that they have listed as well.
If you haven't watched Purrfect.dev's Pulumi Episode you should check it out now!
https://youtu.be/X0LF2LugNTY
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/google-cloud-reference
+ ZKdo3vLUqcb4sYreiCMZPg
+ Wed, 16 Sep 2020 04:00:00 GMT
+
+ This is all from https://github.com/gregsramblings/google-cloud-4-words/blob/master/README.md please check it out!
This is mainly for me to remember where this great list is located @gregsramblings.
All Products
Compute
- Cloud Run: Serverless for containerized applications 🔗Link 🔗Link
- Cloud Functions: Event-driven serverless functions 🔗Link 🔗Link
- Compute Engine: VMs, GPUs, TPUs, Disks 🔗Link 🔗Link
- Kubernetes Engine (GKE): Managed Kubernetes/containers 🔗Link 🔗Link
- App Engine: Managed app platform 🔗Link 🔗Link
- Bare Metal Solution: Hardware for specialized workloads 🔗Link
- Preemptible VMs: Short-lived compute instances 🔗Link 🔗Link
- Shielded VMs: Hardened VMs 🔗Link 🔗Link
- Sole-tenant nodes: Dedicated physical servers 🔗Link 🔗Link
Storage
- Cloud Filestore: Managed NFS server 🔗Link 🔗Link
- Cloud Storage: Multi-class multi-region object storage🔗Link 🔗Link
- Persistent Disk: Block storage for VMs 🔗Link 🔗Link
- Local SSD: VM locally attached SSDs 🔗Link 🔗Link
Database
- Cloud Bigtable: Petabyte-scale, low-latency, non-relational 🔗Link 🔗Link
- Cloud Firestore: Serverless NoSQL document DB 🔗Link 🔗Link
- Cloud Memorystore: Managed Redis and Memcached 🔗Link 🔗Link
- Cloud Spanner: Horizontally scalable relational DB 🔗Link 🔗Link
- Cloud SQL: Managed MySQL, PostgreSQL, SQL Server 🔗Link 🔗Link
Data and Analytics
- BigQuery: Data warehouse/analytics 🔗Link 🔗Link
- BigQuery BI Engine: In-memory analytics engine 🔗Link
- BigQuery ML: BigQuery model training/serving 🔗Link
- Cloud Composer: Managed workflow orchestration service 🔗Link 🔗Link
- Cloud Data Fusion: Graphically manage data pipelines 🔗Link 🔗Link
- Cloud Dataflow: Stream/batch data processing 🔗Link 🔗Link
- Cloud Dataprep: Visual data wrangling 🔗Link 🔗Link
- Cloud Dataproc: Managed Spark and Hadoop 🔗Link 🔗Link
- Cloud Pub/Sub: Global real-time messaging 🔗Link 🔗Link
- Data Catalog: Metadata management service 🔗Link 🔗Link
- Data Studio: Collaborative data exploration/dashboarding 🔗Link 🔗Link
- Looker: Enterprise BI and Analytics 🔗Link
Hybrid and Multi-cloud
- Anthos: Enterprise hybrid/multi-cloud platform 🔗Link 🔗Link
- Anthos GKE: Hybrid/on-prem Kubernetes Engine 🔗Link 🔗Link
- Anthos Config Management:Policy and security automation 🔗Link 🔗Link
- Anthos Service Mesh: Managed service mesh (Istio) 🔗Link 🔗Link
- Cloud Run for Anthos: Serverless development for Anthos 🔗Link 🔗Link
- Google Cloud Marketplace for Anthos: Pre-configured containerized apps 🔗Link 🔗Link
- Migrate for Anthos: Migrate VMs to Kubernetes Engine 🔗Link 🔗Link
- Operations: Monitoring, logging, troubleshooting 🔗Link 🔗Link
- Cloud Build: Continuous integration/delivery platform 🔗Link 🔗Link
- Traffic Director: Service mesh traffic management 🔗Link 🔗Link
- Apigee API Management: API management, development, security 🔗Link
AI/ML
- AI Hub: Hosted AI component sharing 🔗Link 🔗Link
- AI Platform Data Labeling: Data labeling by humans 🔗Link
- AI Platform Deep Learning VMs: Preconfigured VMs for deep learning 🔗Link 🔗Link
- AI Platform Deep Learning Containers: Preconfigured containers for deep learning 🔗Link 🔗Link
- AI Platform Notebooks: Managed JupyterLab notebook instances 🔗Link🔗Link
- AI Platform Pipelines: Hosted ML workflows🔗Link
- AI Platform Predictions: Autoscaled model serving 🔗Link
- AI Platform Training: Distributed AI training 🔗Link
- AI Platform: Managed platform for ML 🔗Link
- AutoML Natural Language: Custom text models 🔗Link
- AutoML Tables: Custom structured data models 🔗Link 🔗Link
- AutoML Translation: Custom domain-specific translation 🔗Link 🔗Link
- AutoML Video Intelligence: Custom video annotation models 🔗Link 🔗Link
- AutoML Vision: Custom image models 🔗Link 🔗Link
- Cloud Natural Language API: Text parsing and analysis 🔗Link 🔗Link
- Cloud Speech-To-Text API: Convert audio to text 🔗Link 🔗Link
- Cloud Talent Solutions API: Job search with ML 🔗Link 🔗Link
- Cloud Text-To-Speech API: Convert text to audio 🔗Link 🔗Link
- Cloud TPU: Hardware acceleration for ML 🔗Link 🔗Link
- Cloud Translation API: Language detection and translation 🔗Link 🔗Link
- Cloud Video Intelligence API: Scene-level video annotation 🔗Link 🔗Link
- Cloud Vision API: Image recognition and classification 🔗Link 🔗Link
- Contact Center AI: AI in your contact center🔗Link 🔗Link
- Dialogflow: Create conversational interfaces 🔗Link 🔗Link
- Document AI: Analyze, classify, search documents 🔗Link 🔗Link
- Explainable AI: Understand ML model predictions 🔗Link 🔗Link
- Recommendations AI: Create custom recommendations 🔗Link 🔗Link
- Vision Product Search: Visual search for products 🔗Link
Networking
- Carrier Peering: Peer through a carrier 🔗Link
- Direct Peering: Peer with GCP 🔗Link
- Dedicated Interconnect: Dedicated private network connection 🔗Link
- Partner Interconnect: Connect on-prem network to VPC 🔗Link
- Cloud Armor: DDoS protection and WAF 🔗Link 🔗Link
- Cloud CDN: Content delivery network 🔗Link 🔗Link
- Cloud DNS: Programmable DNS serving 🔗Link 🔗Link
- Cloud Load Balancing: Multi-region load distribution/balancing 🔗Link 🔗Link
- Cloud NAT: Network address translation service 🔗Link
- Cloud Router: VPC/on-prem network route exchange (BGP) 🔗Link
- Cloud VPN (HA): VPN (Virtual private network connection) 🔗Link
- Network Service Tiers: Price vs performance tiering 🔗Link 🔗Link
- Network Telemetry: Network telemetry service 🔗Link 🔗Link
- Traffic Director: Service mesh traffic management 🔗Link 🔗Link
- Google Cloud Service Mesh: Service-aware network management 🔗Link 🔗Link
- Virtual Private Cloud: Software defined networking 🔗Link 🔗Link
- VPC Service Controls: Security perimeters for API-based services 🔗Link 🔗Link
- Network Intelligence Center: Network monitoring and topology 🔗Link 🔗Link
Internet of Things (IoT)
- Cloud IoT Core: Manage devices, ingest data 🔗Link 🔗Link
Gaming
- Google Cloud Game Servers: Orchestrate Agones clusters 🔗Link
Identity and Security
- Access Transparency: Audit cloud provider access 🔗Link 🔗Link
- Binary Authorization: Kubernetes deploy-time security 🔗Link 🔗Link
- Cloud Audit Logs: Audit trails for GCP 🔗Link 🔗Link
- Cloud Data Loss Prevention API: Classify and redact sensitive data 🔗Link 🔗Link
- Cloud HSM: Hardware security module service 🔗Link 🔗Link
- Cloud EKM: External keys you control 🔗Link 🔗Link
- Cloud IAM: Resource access control 🔗Link 🔗Link
- Cloud Identity: Manage users, devices & apps 🔗Link 🔗Link
- Cloud Identity-Aware Proxy: Identity-based app access 🔗Link 🔗Link
- Cloud KMS: Hosted key management service 🔗Link 🔗Link
- Cloud Resource Manager: Cloud project metadata management 🔗Link 🔗Link
- Cloud Security Command Center: Security management and data risk platform 🔗Link 🔗Link
- Cloud Security Scanner: App engine security scanner 🔗Link 🔗Link
- Context-aware Access: End-user attribute-based access control 🔗Link 🔗Link
- Event Threat Detection: Scans for suspicious activity 🔗Link
- Managed Service for Microsoft Active Directory: Managed Microsoft Active Directory 🔗Link 🔗Link
- Secret Manager: Store and manage secrets 🔗Link 🔗Link
- Security Key Enforcement: Two-step key verification 🔗Link
- Shielded VMs: Hardened VMs 🔗Link 🔗Link
- Titan Security Key: Two-factor authentication (2FA) device 🔗Link
- VPC Service Controls: VPC data constraints 🔗Link 🔗Link
Management Tools
- Cloud APIs: APIs for cloud services 🔗Link 🔗Link
- Cloud Billing API: Programmatically manage GCP billing 🔗Link
- Cloud Billing: Billing and cost management tools 🔗Link 🔗Link
- Cloud Console: Web-based management console 🔗Link
- Cloud Deployment Manager: Templated infrastructure deployment 🔗Link 🔗Link
- Cloud Mobile App: iOS/Android GCP manager app 🔗Link
- Private Catalog: Internal Solutions Catalog 🔗Link 🔗Link
- Cloud Debugger: Live production debugging 🔗Link 🔗Link
- Error Reporting: App error reporting 🔗Link 🔗Link
- Cloud Logging: Centralized logging 🔗Link 🔗Link
- Cloud Monitoring: Infrastructure and application monitoring 🔗Link 🔗Link
- Cloud Profiler: CPU and heap profiling 🔗Link 🔗Link
- Cloud Trace: App performance insights 🔗Link 🔗Link
- Transparent SLIs: Monitor GCP services 🔗Link 🔗Link
Developer Tools
- Cloud Build: Continuous integration/delivery platform 🔗Link 🔗Link
- Cloud Code for IntelliJ: IntelliJ GCP tools 🔗Link
- Cloud Code for VS Code: VS Code GCP tools 🔗Link
- Cloud Code: Cloud native IDE extensions 🔗Link 🔗Link
- Cloud Scheduler: Managed cron job service 🔗Link 🔗Link
- Cloud SDK: CLI for GCP 🔗Link 🔗Link
- Cloud Shell: Browser-based terminal/CLI 🔗Link 🔗Link
- Cloud Source Repositories: Hosted private git repos 🔗Link 🔗Link
- Cloud Tasks: Asynchronous task execution 🔗Link 🔗Link
- Cloud Tools for Eclipse: Eclipse GCP tools 🔗Link
- Cloud Tools for Visual Studio: Visual Studio GCP tools 🔗Link
- Container Analysis: Automated security scanning 🔗Link 🔗Link
- Container Registry: Private container registry/storage 🔗Link 🔗Link
- Artifact Registry: Universal package manager 🔗Link 🔗Link
- Gradle App Engine Plugin: Gradle App Engine plugin 🔗Link
- Maven App Engine Plugin: Maven App Engine plugin 🔗Link
Migration to GCP
- BigQuery Data Transfer Service: Bulk import analytics data 🔗Link 🔗Link
- Cloud Data Transfer: Data migration tools/CLI 🔗Link
- Google Transfer Appliance: Rentable data transport box 🔗Link 🔗Link
- Migrate for Anthos: Migrate VMs to GKE containers 🔗Link 🔗Link
- Migrate for Compute Engine: Compute Engine migration tools 🔗Link 🔗Link
- Migrate from Amazon Redshift: Migrate from Redshift to BigQuery 🔗Link
- Migrate from Teradata: Migrate from Teradata to BigQuery 🔗Link
- Storage Transfer Service: Online/on-premises data transfer 🔗Link
- VM Migration: VM migration tools 🔗Link
- Cloud Foundation Toolkit: Infrastructure as Code templates 🔗Link
API Platform and Ecosystems
- API Analytics: API metrics 🔗Link
- API Monetization: Monetize APIs 🔗Link
- Apigee API Platform: Develop, secure, monitor APIs 🔗Link
- Apigee Hybrid: Manage hybrid/multi-cloud API environments 🔗Link🔗Link
- Apigee Sense: API protection from attacks 🔗Link
- Cloud Endpoints: Cloud API gateway 🔗Link
- Cloud Healthcare API: Healthcare system GCP interoperability 🔗Link
- Developer Portal: API management portal 🔗Link
- GCP Marketplace: Partner & open source marketplace 🔗Link
Google Maps Platform
- Directions API: Get directions between locations 🔗Link
- Distance Matrix API: Multi-origin/destination travel times 🔗Link
- Geocoding API: Convert address to/from coordinates 🔗Link
- Geolocation API: Derive location without GPS 🔗Link
- Maps Embed API: Display iframe embedded maps 🔗Link
- Maps JavaScript API: Dynamic web maps 🔗Link
- Maps SDK for Android: Maps for Android apps 🔗Link
- Maps SDK for iOS: Maps for iOS apps 🔗Link
- Maps Static API: Display static map images 🔗Link
- Maps SDK for Unity: Unity SDK for games 🔗Link
- Maps URLs: URL scheme for maps 🔗Link
- Places API: Rest-based Places features 🔗Link
- Places Library, Maps JS API: Places features for web 🔗Link
- Places SDK for Android: Places features for Android 🔗Link
- Places SDK for iOS: Places feature for iOS 🔗Link
- Roads API: Convert coordinates to roads 🔗Link
- Street View Static API: Static street view images 🔗Link
- Street View Service: Street view for JavaScript 🔗Link
- Time Zone API: Convert coordinates to timezone 🔗Link
G Suite Platform
- Admin SDK: Manage G Suite resources 🔗Link
- AMP for Email: Dynamic interactive email 🔗Link
- Apps Script: Extend and automate everything 🔗Link
- Calendar API: Create and manage calendars 🔗Link
- Classroom API: Provision and manage classrooms 🔗Link
- Cloud Search: Unified search for enterprise 🔗Link
- Docs API: Create and edit documents 🔗Link
- Drive Activity API: Retrieve Google Drive activity 🔗Link
- Drive API: Read and write files 🔗Link
- Drive Picker: Drive file selection widget 🔗Link
- Email Markup: Interactive email using schema.org 🔗Link
- G Suite Add-ons: Extend G Suite apps 🔗Link
- G Suite Marketplace: Storefront for integrated applications 🔗Link
- Gmail API: Enhance Gmail 🔗Link
- Hangouts Chat Bots: Conversational bots in chat 🔗Link
- People API: Manage user's Contacts 🔗Link
- Sheets API: Read and write spreadsheets 🔗Link
- Slides API: Create and edit presentations 🔗Link
- Task API: Search, read & update Tasks 🔗Link
- Vault API: Manage your organization's eDiscovery 🔗Link
Mobile (Firebase)
- Cloud Firestore: Document store and sync 🔗Link
- Cloud Functions for Firebase: Event-driven serverless applications 🔗Link
- Cloud Storage for Firebase: Object storage and serving 🔗Link
- Crashlytics: Crash reporting and analytics 🔗Link
- Firebase A/B Testing: Create A/B test experiments 🔗Link
- Firebase App Distribution: Trusted tester early access 🔗Link
- Firebase Authentication: Drop-in authentication 🔗Link
- Firebase Cloud Messaging: Send device notifications 🔗Link
- Firebase Dynamic Links: Link to app content 🔗Link
- Firebase Extensions: Pre-packaged development solutions 🔗Link
- Firebase Hosting: Web hosting with CDN/SSL 🔗Link
- Firebase In-App Messaging: Send in-app contextual messages 🔗Link
- Firebase Performance Monitoring: App/web performance monitoring 🔗Link
- Firebase Predictions: Predict user targeting 🔗Link
- Firebase Realtime Database: Real-time data synchronization 🔗Link
- Firebase Remote Config: Remotely configure installed apps 🔗Link
- Firebase Test Lab: Mobile testing device farm 🔗Link
- Google Analytics for Firebase: Mobile app analytics 🔗Link
- ML Kit for Firebase: ML APIs for mobile 🔗Link
Additional Resources
- Google Cloud Home Page: 🔗Link
- Google Cloud Blog: 🔗Link
- Google Cloud Open Source: 🔗Link
- GCP Medium Publication: 🔗Link
- Apigee Blog: 🔗Link
- Firebase Blog: 🔗Link
- G Suite Developers Blog: 🔗Link
- G Suite GitHub: 🔗Link
- G Suite Twitter: 🔗Link
- Google Cloud Certifications: 🔗Link
- Google Cloud System Status: 🔗Link
- Google Cloud Training: 🔗Link
- Google Developers Blog: 🔗Link
- Google Maps Platform Blog: 🔗Link
- Google Open Source Blog: 🔗Link
- Google Security Blog: 🔗Link
- Kaggle Home Page: 🔗Link
- Kubernetes Blog: 🔗Link
- Regions and Network Map: 🔗Link
Everything below is not included in the PDFs/PNGs due to space limitations
Additional Resources
- Source for this document: 🔗Link
- Google Cloud Solutions Library: 🔗Link
- G Suite Solutions Gallery: 🔗Link
- Google Cloud Support Hub: 🔗Link
- GCP Pricing: 🔗Link
- GCP Pricing Calculator: 🔗Link
- Qwiklabs Home Page: 🔗Link
- Codelabs Home Page: 🔗Link
- Podcasts:
- Google Cloud Podcast: 🔗Link
- Kubernetes Podcast: 🔗Link
- Reddit:
- Big Data / Data Analytics Product Comparisons: 🔗Link
- Compute Product Comparisons: 🔗Link
- Database Product Comparisons: 🔗Link
- Networking Product Comparisons: 🔗Link
- Storage Product Comparisons: 🔗Link
GCP Foundational Open Source Projects
- Apache Beam: Batch/streaming data processing 🔗Link
- Go: High Concurrency Programming Language 🔗Link
- gRPC: RPC framework 🔗Link
- gVisor: Secure container runtime 🔗Link
- Istio: Connect and secure services 🔗Link
- Knative: Serverless framework for Kubernetes 🔗Link
- Kubeflow: ML toolkit for Kubernetes 🔗Link
- Kubernetes: Management of containerized applications 🔗Link
- OpenCensus: Cloud native observability framework 🔗Link
- TensorFlow: ML framework 🔗Link
Platform Comparisons
- Google Cloud Platform for AWS Professionals: 🔗Link
- Google Cloud Platform for Azure Professionals: 🔗Link
- Google Cloud Platform for Data Center Professionals: 🔗Link
- Google Cloud Platform for OpenStack Users: 🔗Link
Language Specific Documentation
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/debug-angular-9-in-chrome-console
+ ZKdo3vLUqcb4sYreiCMc5A
+ Mon, 20 Apr 2020 04:00:00 GMT
+ These functions are exposed via the global
ng "namespace" variable automatically when you import from @angular/core and run your application in development mode. These functions are not exposed when the application runs in a production mode.Chrome Console Utilities
The great part about using the Chrome console is that it gives you access any DOM element that you have selected. For the last item you can get the reference by typing $0 in the console. Below you will see that you can use the selection tool to easily find the element. Once this is selected you can then use $0 as it will be the latest in your selection history. You can read further about this in Console Utilities API Reference.
Unknown block type "image", specify a component for it in the `components.types` option
Getting the Angular Component reference
Now that we know how to get a DOM reference we can use the Angular @angular/core/global utilities, you can find more details here: https://angular.io/api/core/global#entry-point-exports.Using ng.getContext($0) we can access the angular component instance.
Unknown block type "code", specify a component for it in the `components.types` option
Changing values in the Component
Now that you have a reference to the component using let dialogComponent = ng.getContext($0) we can now update the properties within the component. For this example we will change the qty in our recipeIngredient object.
Unknown block type "code", specify a component for it in the `components.types` option
You should also note that you can display the entire component as well incase you are unaware of the structure.
Unknown block type "image", specify a component for it in the `components.types` option
Making Component Update
In order to get the value change to show within the component you must trigger change detection.
Unknown block type "code", specify a component for it in the `components.types` option
]]>
+ Alex Patterson
+
+
+ -
+
+ https://codingcat.dev/post/scully-io-angular-static-site-generator
+ ZKdo3vLUqcb4sYreiCMUN6
+ Tue, 17 Dec 2019 05:00:00 GMT
+
+ JAMStack with SSG
2019 has been another amazing year building up JAMStack (Javascript APIs and Markup) sites. AJonP joined this trend by moving over to Gatsby a React based Static Site generator. Growing up as an Angular developer I missed the fact that Angular didn't have an option that existed. For AJonP it came down to Hugo and Gatsby (writing and then rewriting).
Scully
The best way to build the fastest Angular apps. Scully is a static site generator for Angular projects looking to embrace the JAMStack.
Just like the other frameworks what Scully offers is the ability to pre-render pages and then create HTML and CSS that loads highly performant pages for your browser to utilize without the SPA effect and whitescreen where you get loading times in the mix.
Make sure to checkout the Scully Docs.
Scully pre-renders each page in your app to plain HTML & CSS. To do this, Scully uses machine learning techniques to find all of the routes in your project. Scully then visits each route, rendering the view and saving it to an HTML file. You can then ship all of those HTML files to production. Each view in your app can now be delivered to your users in just a few KBs, as opposed to the hundreds/thousands of KBs require to download your entire Angular app. Your app appears INSTANTLY on any device (including mobile 3G).
Alpha Release Video
Make sure to checkout Aaron Frost and the HeroDevs team Introducing Scully: Angular + JAMStack.
Try Scully
Of course you just need an Angular project. For an example I cloned the example from our Angular Material Course.
Unknown block type "code", specify a component for it in the `components.types` option
Initialize
Then opening the terminal at the root of this project I ran the commands to initialize scully
This produced 6 file changes in my examplepackage.json and package-lock.json
Unknown block type "image", specify a component for it in the `components.types` option
scullyconfig.js
Unknown block type "image", specify a component for it in the `components.types` option
polyfills.ts
Unknown block type "image", specify a component for it in the `components.types` option
app.component.ts
Unknown block type "image", specify a component for it in the `components.types` option
app.module.ts
Unknown block type "image", specify a component for it in the `components.types` option
Build
Next you will create an Angular build just like normal, followed by npm run scully .
If you get an error I did have to place "fs-extra": "^8.1.0", as a dev dependency https://github.com/scullyio/scully/issues/28
You will see your normal build file in dist folder and a new static folder that contains your SSG files. You can then run the site by switching to the static directory and using a local server like http-server to run the compliled code.
What to Try Next
Scully Generators
Add scully to your project and utilize @schematics/angular. This command will add the necessary packages that allow you to use the Angular CLI for scaffolding out
Blogs
The below command allows you to create a blog and make posts. Try this out in a new Angular app to start up your blog from scratch.
Unknown block type "code", specify a component for it in the `components.types` option
Final Thoughts
I know that Aaron Frost, Sander Elias, Jorge Cano, and Andres Villanueva are Angular Wizards so the project is in good hands. It is just the beginning, but if you are looking for an amazing Angular project to be part of make sure you star this one!
]]>
+ Alex Patterson
+
+
+
+
\ No newline at end of file
diff --git a/apps/site/baseline/podcasts_rss.json b/apps/site/baseline/podcasts_rss.json
new file mode 100644
index 000000000..37d40a976
--- /dev/null
+++ b/apps/site/baseline/podcasts_rss.json
@@ -0,0 +1,664 @@
+{
+ "version": "https://jsonfeed.org/version/1",
+ "title": "CodingCat.dev - podcast feed",
+ "home_page_url": "https://codingcat.dev/podcasts",
+ "feed_url": "https://codingcat.dev/podcasts/rss.json",
+ "description": "CodingCat.dev - podcast feed",
+ "icon": "https://codingcat.dev/icon.svg",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev"
+ },
+ "items": [
+ {
+ "id": "292601cc-5365-4644-9b34-82e67d033e4b",
+ "content_html": "
By the end of this post, you’ll understand:
- The new engineering landscape GenAI has created
- Practical best practices for leveraging GenAI in your work
- Mistakes and misconceptions you absolutely want to avoid
- Inspiring tools and frameworks that are already changing the game
- What’s on the horizon, both thrilling and a little overwhelming
Let’s get started!
Introduction: Why GenAI Has Every Engineer Talking
You know the feeling—everyone’s suddenly talking about automation, agents, and how AI is about to revolutionize everything. You read about companies “eliminating jobs” thanks to GenAI, and you’re wondering… Is this really happening? Is this a threat, or is it a huge opportunity?
If you’ve ever sat with those questions, you’re in good company! On our latest episode, I spoke with Balki, a seasoned CTO, about how GenAI is reshaping engineering roles, what’s actually working in startups right now, and how you can avoid the typical pitfalls to come out ahead.
Why this blog post matters:
“First time founders focus on product. Successful founders focus on distribution.” — Balki
That means it’s not just about building with AI—it’s about making it useful, scalable, and profitable.
Meet Balki: Everyday Startup Battles, AI Style
Before we jump into the nitty-gritty, let’s meet our expert guest.
Balki's Career: From BigCorp to Fractional CTO
Balki spent his first decade as an engineer at Fiserv—deep in the online banking trenches, working primarily on Microsoft’s tech stack. Eventually, he pivoted to leading his own startup, taking the leap from “comfortable” to “complete accountability.”
Takeaway: The happiness came less from money and more from ownership and learning, even when the risks were high.
“There's nobody else to complain about. I’m accountable for everything.”
How the Leap Happened (and What You Can Learn)
Far from a sudden “drop everything and start a company,” Balki’s transition was informed by education (an EMBA program) and practical testing. The final pivot occurred in an entrepreneurship course—where building a product from scratch “felt like coming home.”
Lesson: Big jumps are rarely one moment. Often, it’s a series of small pivots, each featuring late-night emails, experimental coding, and a few “blind confidence” moments.
CTO Roles: Full-Time, Fractional, and What That Means for Startups
Ever wondered what the difference is between a full-time CTO and a fractional CTO? Balki’s journey is a fascinating example:
Full-Time CTO vs. Fractional CTO
- Full-Time CTO: Committed to one company.
- Fractional CTO: Provides strategic, high-impact guidance to several startups at once—usually part-time.
Fractional CTOs thrive by:
- Bringing deep expertise from multiple sectors
- Targeting high-leverage activities (think: 20 hours a month laser-focused on the biggest problems)
- Accelerating product clarity and distribution
Big insight: Every minute counts. When you’re dropping in for a few hours a week, priorities become crystal clear!
GenAI for Engineers: From “Just Another Tool” To Business Superpower
GenAI isn’t new—but the way it’s used by engineering teams is evolving fast. Balki says:
“The market is flooded with proof-of-concept tools targeting non-engineers. In my world, there’s a big chasm between building a prototype, getting traction, and scaling.”
You can hack together a quick POC with Lovelace or Bolt in minutes. But scaling that prototype—making it reliable, distributable, and profitable—is a different sport.
Why Scaling Is So Hard (and Where GenAI Can Help)
Most startups stall, even after they get initial traction. Balki specializes in bridging this “chasm”—helping teams go from “scrappy” to “scalable.”
“Engineering Excellence”: Balki’s Five Pillars + GenAI
Let’s get super practical. Any successful engineering team needs engineering excellence. Before GenAI, Balki set out four pillars. With GenAI, a fifth was added. Here’s the breakdown:
The Four Classic Pillars
- Automated Testing
- Validates code. Makes sure your software can withstand real-world scenarios.
- CI/CD Pipelines
- Observability
- Let’s engineers be the first to know when something breaks (not your customers).
- Example: Datadog
- Modular Architecture
- Flexible, scalable code organization—critical for rapid evolution.
The Fifth Pillar: GenAI Fluency
Today, GenAI fluency is just as critical. In fact, Balki breaks this down further into three sub-pillars (detailed below).
Why GenAI Matters
Thanks to GenAI, startups can rapidly accelerate improvement in these pillars—without blowing the budget or risking bankruptcy. You can stay agile and address technical debt much faster.
GenAI and Job Roles: Are We Really Automating Ourselves Out?
The media loves to declare “X jobs eliminated by GenAI!”—but is this really the story?
What’s Actually Changing in Engineering
Specialization is risky.
Engineers who only focus on one niche (e.g., frontend specialist, TypeScript expert) are seeing those tasks rapidly commoditized.
“For 20 years it was similar—good engineers could rest on their laurels. Now that’s been commoditized. Founder mindset is what matters.”
Full-stack skills are becoming standard. The ability to build, deploy, test, and validate end-to-end is the new norm.
So what tasks are ripe for GenAI automation today?
Balki notes: pure specialization may soon be obsolete, and “mundane tasks” (like repetitive API wrappers, boilerplate code, out-of-date documentation) are prime targets for automation.
Real-World Example: API Wrappers & SDKs
Many engineers are scrambling to build “MCP servers”—wrappers around APIs and OpenAI SDKs. With the release of the OpenAI App SDK, a new “walled garden” effect is emerging. Is this healthy? Are we risking another App Store scenario?
It’s still hard to say if this is a passing trend or a lasting shift. For B2B SaaS, the immediate risk may be low, but keeping up AI fluency is more important than ever.
Engineering AI Fluency: Climbing the Three GenAI Sub-Pillars
What does “AI fluency” really mean for engineering teams? Balki’s framework divides it into three practical levels:
Level 1: Leveraging AI Tools for Engineer Productivity
This is all about using AI tools—like Cursor, Claud Code, Code Rabbit, and GitHub Copilot—to supercharge your coding workflow.
Tip:
Cursor and Cloud Code excel for different personalities—Cursor for leaders, Cloud Code for craftspeople. Setting team-wide Cursor rules keeps best practices consistent.
Level 2: Using AI Tools to Boost Engineering Excellence
Apply AI directly to accelerate the four classic pillars.
- Testing & Coverage (e.g., auto-generating tests)
- Deployment Pipelines (e.g., AI-powered CI/CD suggestions)
- Modularity & Code Review
- Code Rabbit: Cuts review time on pull requests, boosting speed.
- Axel AI: Embeds in legacy code to help modernize architecture.
- Antithesis: Runs containers to catch edge-case bugs in staging.
“Anything that can accelerate engineering excellence lights up my eyes.”
Level 3: Incorporating AI Directly Into Product Features
This is the “cool stuff”—voice agents, automated support, and smarter applications.
But beware:
Before over-engineering with tools like LangChain and fancy agents, start simple. OpenAI API calls often get the job done. As you get closer to product market fit, start worrying about human evals, prompt tuning, and security concerns.
Pro move:
Start with human-in-the-loop. Fully automated AI features rarely work perfectly from Day 1.
Building Distributed Teams: How GenAI Changes Remote Collaboration
If your team is spread around the world—Eastern Europe, Latin America, Hong Kong—you know communication and documentation aren’t easy.
GenAI is a game-changer for:
- Communication: Coaching team members to use GenAI for clear, culturally-appropriate communication.
- Documentation: Auto-generating code and onboarding docs that are actually useful.
“With proper documentation and plan, new engineers can be effective in two weeks—if not one.”
Challenge your team:
No more 90-day onboarding. With GenAI-assisted docs, you can cut that down to 1-2 weeks.
Real-World Use Case: devIQ—Bridging Technical and Non-Technical Teams
Balki’s tool, devIQ (try it for free!), was born from a common startup pain: the disconnect between technical and non-technical staff.
How devIQ Helps
- Assesses engineering teams: Across Balki’s five pillars
- Translates complexity: Makes technical progress educational for CEOs and business leads
- Gives grades and actionable steps:
- “Your grade is C in automated testing, B in GenAI fluency.”
- Offers prescriptive paths: Specific ideas to improve grades
Use-case:
When Balki drops into an engagement, devIQ helps quickly pinpoint engineering gaps and opens authentic conversation between product and engineering.
“If you methodically invest in the right pillars, your org will be 95th percentile or better—no matter your product.”
Practical Tips for Staying Current in GenAI Engineering
Let’s face it—AI moves insanely fast. It’s easy to feel lost, overwhelmed, or permanently behind.
Balki’s Learning Strategy
- Focus 80% on problems inside your organization.
Go out and find solutions that fit your actual engineering needs (e.g., legacy code modernization, engineering excellence pillars). - Spend 20% dabbling in shiny new things.
Keeps your creative juices flowing and occasionally unlocks breakthrough opportunities.
Discipline matters.
Without intentional focus, you’ll spend all your time chasing distractions.
“Will AI Replace Us?”: The Real Question to Ask Before Adding GenAI
This is the million-dollar question—and it’s not theoretical. Before shoving AI into your product, be honest:
“Is this something the customers want, and does it make their lives better by using GenAI inside the tool?”
Don’t waste time making your prompts perfect or optimizing the next toolchain before confirming user demand.
Pro advice:
- Insert a human eval into early prototypes.
- Gradually replace manual involvement as the product matures.
- Automation isn’t instant—chip away bit by bit.
The Future: Next-Level Tools and “The Stone Age to Modern Age” Shift
AI moves at a ridiculous pace. Here’s what’s coming—and why you should care.
For New Engineers
Starting today is easier than ever—you’re entering the AI-native world. But those legacy problems will catch up eventually.
Building Team Resilience: Don’t Go It Alone
Here’s a simple tip: get out of your shell. Connect with peer groups, ask for feedback, and share what you’re planning.
“When I see a new tool, I don’t jump in right away. I bring it to my cohorts for feedback, and I shave off weeks and hundreds of hours of research.”
You get richer, higher quality feedback by collaborating—AI or not.
Summary: Key Takeaways for GenAI Engineers
Let’s wrap it all up.
- GenAI has transformed engineering—rapid POCs, scalable products, and changed job roles.
- “Engineering excellence” depends on five pillars, with GenAI fluency now essential.
- Don’t specialize too narrowly—full-stack, founder mindset is becoming the norm.
- Use AI for real productivity: code review, documentation, collaboration, and direct product features.
- Start simple—and always validate with customer demands before ramping up automation.
- Leverage frameworks like devIQ to bridge business/engineering gaps and accelerate progress.
- Stay disciplined in your learning, and seek feedback from peers to keep your engineering sharp.
",
+ "url": "https://codingcat.dev/podcast/genai-for-engineers-what-s-real-what-s-not-and-what-s-coming",
+ "title": "GenAI for Engineers, What's Real, What's Not and What's Coming",
+ "summary": "Curious about GenAI for engineers? Learn what's real, what's not, and what's next in AI. ",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/3e48f44739be2aabf3aaa805e51b3a6328979153-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2025-11-19T13:10:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "ab22f27a-2d5d-4dfa-ad61-b5dd9ef85334",
+ "content_html": "Ever feel like user authentication is one hot mess of protocols, random forms, and endless “Sign in with Google” buttons? Or maybe you’ve heard rumblings about “OAuth” and “MCP servers” but wondered how they actually fit into the web (and now, AI) landscape? Well, you’re in the right place!
In this deep dive, I sit down with Max from Stytch, now part of Twilio!, to break down everything you’ve ever wanted to know about OAuth, why it matters, and how it’s suddenly become crucial for connecting Large Language Models (LLMs) and agents to external services in a secure, user-friendly way. Plus, we’ll get under the hood of the coolest Tamagotchi-inspired AI app, Chatagotchi, built using OpenAI’s Apps SDK and MCP.
By the end of this post, you’ll:
- Know what OAuth, MCP, and the Apps SDK are and why they’re key for AI integrations
- See a real-world Chatagotchi demo and learn how it works
- Understand why authentication flows matter—especially when security is at stake
- Get actionable insights for building your own secure, interactive AI-powered apps
Ready to level up your understanding? Let’s jump in!
What Is Stytch & Why Authentication Is So Hard
Let’s kick things off with a little background. Stytch (now acquired by Twilio) is one of those rare companies laser-focused on making authentication simpler—think passwords, magic links, Face ID, social logins, and all the enterprise protocols (“SAML,” “OpenID”), often in places you wouldn’t even notice.
When I asked Max, product engineer at Stytch, about the job, his answer was simple:
"We take all of that data and we build tools that help developers manage that so you don't have to worry about building your own sign in form."
Stytch spent their early years helping developers pull user data in securely, but over the past year, their focus has flipped to pushing user data out to other systems—the critical piece for modern integrations, cross-app experiences, and now, AI agents.
Why Is Authentication So Crazy Complicated?
Here’s the fun part: Picking just authentication with passwords is easy-ish if you’re careful with things like hashing. But add multi-factor authentication (MFA), social logins, SAML, OTP, magic links, mobile integrations, and you’re suddenly looking at months of build-out and endless docs.
Max summed it up hilariously:
"You can build a simple login form in an afternoon. You can build like full consumer identity, access management in a lifetime."
And if you’re wondering, “Why not just roll my own?”, you’re not alone. But trust me, it’s a lot of work—unless you tap into an expert solution.
Breaking Down OAuth—More Than Just a “Login with Google” Button
Alright, so what is OAuth? If you’ve ever clicked “Login with Google” (or Facebook, or Microsoft), you’re using OAuth under the hood.
But OAuth is not just a protocol—it’s a collection of more than 40 RFCs (tech specs) developed and refined by thousands of security experts and companies like IETF and the OpenID Foundation.
Here’s why that matters: OAuth lets you securely share user data from one place to another—from web apps to desktop clients, mobile apps, even smart TVs—and every step is designed to keep the user in control and the systems secure.
OAuth: The Three Key Pieces
Let’s nerd out for a second. OAuth relies on three main parts:
- Authorization Server / Identity Provider
This is what knows who you are (Netflix.com, accounts.google.com, etc.). - Resource Server
The API that actually holds the data (the giant boxes with Netflix movies, your Google Drive). - Client
The “thing” (app, TV, desktop program) trying to access the data on your behalf.
Here’s how it fits together:
- The client asks the authorization server, “Can I get access?”
- The authorization server gives the client an access token if the user consents.
- The client hands the token to the resource server and gets the data.
Netflix Example
Let’s say you want to watch Netflix on your smart TV:
- Netflix.com is your authorization server (handles login and your identity)
- Netflix’s servers with all the movies are the resource server
- Your smart TV is the client
The flow ensures your TV only gets access to watch movies—not, for example, to change your credit card!
“It's really common to share your data from a website to a native app... even to devices like televisions. And OAuth is about how the user and the service provider work together to agree on how this data should be shared.”
A flow diagram showing OAuth authorization between TV, Netflix, and the servers---
OAuth in Action: Real-World Example
Let's walk through a classic OAuth flow—the Authorization Code Flow—just like you’d see when logging in to a new app:
- User tries to connect an app (e.g. smart TV to Netflix)
- TV sends login request to the Authorization Server (Netflix.com)
- User authenticates (maybe with password, Face ID, or magic link)
- Authorization server issues an access token to the TV
- TV now uses this token to fetch movie data and play content
OAuth doesn’t tell Netflix how to do its login (could be password, SAML, Face ID, etc.)—that’s left flexible so you can customize for your users.
And that’s where Stytch shines—helping you build flexible login flows so you don’t have to reinvent the wheel.
Why Is OAuth So Important?
Beyond “login with Google,” OAuth lets you:
- Limit permissions to just what’s needed (TV can’t change your billing info)
- Support lots of login methods (Face ID, magic links, social sign-in, enterprise SSO)
- Enable secure integrations between systems
- Trust that the protocols have been tested for years by security experts
How OAuth Powers MCP (Model Context Protocol) for AI Agents
So what does all this have to do with AI agents, LLMs (like ChatGPT), and the hot new ecosystem of “apps” running on top of them?
Enter MCP: The “Plug-and-Play” Data Ecosystem for LLMs and AI Agents
The Model Context Protocol (MCP) is trying to standardize how AI agents and LLMs (think ChatGPT, Claude, Gemini) talk to external sources—whether that’s your files, Google Drive, GitHub, Slack, or a custom backend.
Instead of writing custom code for every connector, MCP aims to be a unified protocol so agents and data sources can connect with minimal fuss.
“If you wanted to build an educational service using MCP and roll it out to a high school... are you going to give every student their own API key? I just don't see it going well.”
That’s where OAuth comes in—making these flows secure and approachable for users, versus requiring every person to find and store their own API keys.
Typical MCP Architecture Using OAuth:
- MCP Clients (like ChatGPT, Claude): want to access external data
- MCP Servers: host the data, are “resource servers” in OAuth terms
- Authorization Server: handles identity (as in “who’s asking?”)
- OAuth: bridges the gap, so the MCP client gets a token and can do stuff only the user has approved
This design means users never see an API key and MCP servers don’t directly handle logins—they just rely on the secure flow provided by OAuth.
A diagram showing MCP clients, servers, Authorization Server, and OAuth tokens flowing between them---
Local vs Remote MCP Servers—And Why OAuth Wins
Now things get spicy: When you’re building out MCP connectors, you’ll deal with local (on your machine) vs remote (on the cloud) MCP servers.
Local MCP Servers
- Great for editing code, local files, or pictures
- Doesn’t make sense for mass-market products (people don’t want to install/maintain software, security hassles)
- Okay for developer tools, not for mainstream commercial apps
Remote MCP Servers
- Typical web APIs, running in the cloud
- Easier for everyone to access, since no installing of local software
- Often rely on API Keys - not ideal for mass-market apps (most people don’t know how to manage API keys or environment variables)
OAuth is a familiar and user-friendly solution.
- Most people have gone through OAuth flows (“Sign in with Google/Facebook”)
- Removes the need for users to wrangle technical setup
- Widely adopted and secure
“OAuth is something that all of us do every day, multiple times a day... and you don't need to be technical in order to take advantage of it.”
Chatagotchi: Building a Tamagotchi-Inspired App with MCP and OAuth
Now for the fun part! Max built a Chatagotchi app—basically Tamagotchi for modern AI chat—using the OpenAI Apps SDK and MCP protocol, all securely wired up with OAuth.
Let’s break down how it all fits together.
Unknown block type \"image\", specify a component for it in the `components.types` option
Step 1: Setting Up Your App in ChatGPT
You start off by:
- Running an MCP server (the backend for Chatagotchi)
- Going to ChatGPT → Settings → Apps and Connectors → Advanced Settings → Developer Mode
- Hitting “Create” to add your app
- Specify name, description, MCP server URL
- See a list of available tools (Start game, Feed pet, Play with pet, List achievements)
Step 2: OAuth Flow in Action
To keep everything secure and remember who you are, you:
- Hit Connect inside ChatGPT, triggering an OAuth flow
- Log in via Google (with Stytch handling the backend)
- Review OAuth consent screen, authorizing ChatGPT to:
- Start a new game, feed your pet, play with pet, list achievements
“So Stytch was responsible... This is our drop-in SDK. This is a little React component and then a little bit about Stytch under the hood... You can use that to build whatever experience you want.”
The cool part? The UI and consent screen are highly customizable, letting you maintain your brand and user experience.
Unknown block type \"image\", specify a component for it in the `components.types` option
OAuth consent screen inside ChatGPT displaying Stytch’s customized React
Step 3: Playing the Game and Handling Data
With the app installed, you can chat with Chatagotchi inside ChatGPT. For example:
- Ask “What tools are available?”
- Start a new game (give your virtual pet a name—say, “AJ”)
- Feed your pet (“Pizza,” “Apple,” “Cookie,” etc.)
- ChatGPT inspects the MCP tool schema and provides valid options
- ChatGPT checks with you for permission to invoke each tool
“ChatGPT understands the app and is kind of co-playing it with you. Like a Dungeons and Dragons Dungeon Master experience, almost.”
All UI interactions, loading screens, and game state are handled by:
- MCP server returning JSON data and dynamic HTML for widget rendering
- ChatGPT rendering widgets in iframes, powered by your custom React micro-frontends
Inside the Code: MCP Server, OAuth, and Widget Rendering
Okay, let’s get nerdy! Here’s how the MCP tools, OAuth, and output templates work in the actual code.
Defining MCP Tools with TypeScript SDK
Max uses the official MCP TypeScript SDK to define tools—simple, readable, and powerful.
Here’s a snippet for how you’d define the New Game tool:
Unknown block type \"code\", specify a component for it in the `components.types` option
What does this do?
- Defines the schema
- Specifies input requirements
- Links to an output template (pet.html) for UI rendering
- Includes metadata for special UI handling in OpenAI
Explanation
The tool is registered with the MCP server, so ChatGPT knows its functionality and what data it expects. The metadata allows custom messages and UI rendering to keep the experience engaging and aligned with your brand.
Dynamic UI, Output Templates, and Security Insights
Each tool can return custom HTML/JS to render beautiful widgets directly inside ChatGPT chats. Here’s the basic structure for an output template:
Unknown block type \"code\", specify a component for it in the `components.types` option
pet.js grabs the JSON result (the “structured content”), renders the pet state with React/Vue/vanilla JS—whatever you prefer.
How Does ChatGPT Render Widgets?
- Grabs the output template (“pet.html”) from your server
- Loads its contents into an iframe, sandboxed for security
- Runs your bundled JS code to display the tool call details (passed into the iframe via the
window.openai global) as structured HTML to the user - Keeps everything isolated, so you can use whatever UI stack you want
Unknown block type \"code\", specify a component for it in the `components.types` option
“There's been a lot of work... around iframes since Yahoo days... Now we found out how bad iframes were. Oh, well, don't say that, because this is all iframes too. We'll see how much trouble they cause, but hopefully they stay.”
Security Notes
OpenAI is locking things down—once a tool or app is published, names, signatures, descriptions are locked. No sneaky changes!
If you try to roll out breaking changes or new behavior, you’ll need to resubmit for human review, keeping the ecosystem clean and secure for users.
Also, there are considerations around referencing remote JS/CSS assets: Should your chats be deterministic (never changing), or flexible for updates? Expect best practices around static CDNs, asset hashes, and widget isolation to evolve as these ecosystems mature.
Security, User Experience, and What’s Next in AI App Distribution
As cool as this all sounds, there are big implications for security, user education, and app distribution.
OAuth Isn’t Optional—It’s Essential
Whether you’re dealing with students, mass-market users, or enterprise customers, asking them to manage API keys is dead on arrival. OAuth flows are familiar and easy, allowing anyone to connect apps without technical frustration.
AI Bots and App Stores—The New OS?
ChatGPT and similar platforms are turning into new “Operating Systems” for interacting with external services—much like how the Apple App Store changed mobile distribution.
“Is ChatGPT an app? Is ChatGPT an operating system or a runtime?”
Mockup of ChatGPT with a side bar of installed apps, resembling an app store experienceWe’re already seeing the beginnings of an “App Store” for AI agents and plugins—apps compete for distribution, brand engagement, and new discovery.
Developer Experience
For developers, it’s shockingly easy to get started if you understand web fundamentals and a little OAuth. MCP’s TypeScript SDK, Apps SDK documentation, and Stytch’s drop-in APIs are accessible, flexible, and well-supported.
“I was very surprised by how easy it was to get set up. There's a little bit of a learning curve and a mental curve around the RPC model... but it's not hard to think about—just a little different from classic web dev.”
Conclusion & Key Takeaways
Let’s sum it all up! Here’s what you should remember from this deep dive:
- OAuth is everywhere—It’s how most users expect to authenticate and share access, and it’s critical for both web and AI agent integrations
- MCP (Model Context Protocol) is the key bridge for enabling LLMs and agents to talk to external data sources and tools, safely and flexibly
- Remote MCP servers + OAuth is the right combo for mainstream, user-friendly integrations (not API keys!)
- Apps SDK is turning ChatGPT into a new “App Store”—with custom micro-frontends powered by widgets
- Building these integrations isn’t rocket science—once you grasp the OAuth flow, MCP protocol, and widget architecture, you’re off to the races
- Security and user experience are top priorities—expect standards to keep evolving, but OAuth’s staying power is guaranteed
- Open standards (like MCP) mean your integrations can work across many platforms, not just closed ecosystems
What Do You Think?
Have you built an MCP server or a ChatGPT app? What’s your biggest hang-up with OAuth flows or widget rendering? Are you excited about the future of “app stores” for AI agents—or maybe a little worried?
Drop a comment below—we love hearing what fellow developers, educators, and curious minds think!
Ready to Learn More?
- Watch the full video with Stytch’s Max and CodingCat.dev on YouTube:
OpenAI Apps SDK Video - Dive deeper into official docs:
And of course—subscribe to CodingCat.dev on YouTube for more in-depth interviews, walkthroughs, and tutorials!
“If you maintain a web presence and care about Google search and reaching customers, ChatGPT is a new distribution mechanism. Even if it’s not ChatGPT, something else will take its place, and OAuth will always be in the mix.”
Thanks again to Max from Stytch for breaking down the intersecting worlds of OAuth, MCP, and AI agent apps. Can’t wait to see what you build next!
Max’s Chatagotchi widget in action inside ChatGPT, showing custom UI, game state, and seamless OAuth flow---
Got questions? Post them below, and we’ll dive in together. Happy coding, perfect peeps! 🚀
",
+ "url": "https://codingcat.dev/podcast/how-oauth-mcp-and-the-openai-apps-sdk-power-the-next-generation-of-interactive-ai-experiences",
+ "title": "How OAuth, MCP, and the OpenAI Apps SDK, Power the Next Generation of Interactive AI Experiences (with Stytch & OpenAI)",
+ "summary": "Curious about OAuth, MCP servers, and building cool ChatGPT apps? Hear from Max of Stytch as we dive deep, break down the tech, and build a Tamagotchi together! ",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/0beb99900f4c24e284df2dee3e2d740062c6ce96-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2025-11-04T18:01:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "7fffc615-ed5a-491a-a5f3-e1dcf3338612",
+ "content_html": "Introduction: Why Do We Need Firebase Data Connect?
Let’s be real: Firebase has been a go-to for fast app launches for years, but what happens when your app's logic gets complex, your data relationships grow hairy, or you want join-heavy reports, analytics, and powerful search? Google heard the pain points:
- “I need relational queries and joins.”
- “Can I finally use full text search, not just basic keyword matching?”
- “Can my AI agents interact with my database, securely and conveniently?”
- “Can I swap between secure APIs and developer-friendly tools on the fly?”
You’re not alone. That’s exactly why Firebase Data Connect was born.
By the end of this article, you’ll understand:
- How Data Connect blends SQL (PostgreSQL) power with Firebase’s cloud-native ease.
- Where it beats Firestore—and when you should stick with Firestore instead.
- How to build secure APIs, type-safe SDKs, and connect your data to AI agents like Gemini.
- How to use full text search and vector search (semantic matching) like a pro.
- Practical steps to get started—even locally with emulators!
Meet Tyler Crowe & The Birth of Data Connect
Tyler Crowe isn’t just a Firebase PM—he’s been the brains behind Firebase Auth, AppCheck, and now Data Connect. In our season 5 podcast, Tyler explained:
"We built Data Connect under a single premise. You write the query, we do the rest."
The real motivation? Thousands of developers pining for the best of both worlds: Firebase’s developer joy… plus the relational might of SQL.
Want to go deeper into Tyler's journey? Listen to the full podcast episode.
What Is Firebase Data Connect?
In a nutshell: Data Connect is a fully managed, cloud-native SQL database service—built on Cloud SQL (hello, PostgreSQL!)—and deeply integrated into Firebase.
If you’ve ever wrangled cloud infrastructure, worried about scaling, or cursed at manual security configs, Data Connect is your new best friend.
Why is Data Connect Important?
- Relational Power: Complex queries, joins, and analytics—finally possible!
- No Infrastructure Headaches: Just click, deploy, and go. You never touch a server.
- Secure by Design: Auth, AppCheck, and access directives built in.
- AI-Ready: Schema and queries can be generated by AI agents like Gemini.
- Future Proof: Roadmap includes support for other data sources beyond Cloud SQL.
Highlights
Want a more hands-on intro? Check out our beginner’s guide to Firebase.
Data Connect vs Firestore: The Relational Revolution
Let’s break down a classic developer question:
“Should I stick with Firestore, or do I need Data Connect?”
Why This Matters
Firestore (or the classic Realtime Database) was built for real-time sync, offline-first experiences, and mobile scale. But… when your data needs get complex—think relational joins, analytics, extensions—Firestore just isn’t enough.
Data Connect Brings You:
- The ease of Firebase SDKs
- Pure SQL power (JOINs, aggregation, constraints)
- Deep API and security control
- Analytics and full text search at scale
Firestore is still perfect for:
- Real-time sync
- Offline-first mobile apps
- Simple, single-table queries
Not sure which is best for you? Check out our full breakdown: Data Connect vs Firestore.
Type-Safe SDKs and Secure API Endpoints
Here’s the developer dream: define a query, get a secure API endpoint—and a fully typed SDK that plugs right into your frontend.
Why Is Type Safety So Powerful?
Type-safe SDKs cut debugging hassles, let your IDE catch mistakes before runtime, and speed up development with auto-completion.
How Data Connect Does It
When you save a query/mutation, Data Connect generates an SDK for your chosen platform. That SDK ensures type safety, authentication, and converts your calls into secure, backend-executed SQL.
Example: Querying Movie Reviews in TypeScript
Unknown block type \"code\", specify a component for it in the `components.types` option
This fetches reviews securely, using the backend’s query definition and type validation.
AI Agents & Schema Generation with Gemini
Here’s one for the future: Let AI design your database. With Gemini, Data Connect enables schema and query generation straight from natural language.
Why Does This Matter?
Instead of fiddling with table structures, just describe your app in plain English and watch Gemini build a solid schema—saving you hours.
Example Workflow
- Describe Your App
- "I need a movie review app with reviews, movies, actors, and users."
- Gemini Generates SQL
Unknown block type \"code\", specify a component for it in the `components.types` option
- Tweak & Refine On the Fly
- Add constraints, relationships, or new features (“Let’s add a chat!”).
This isn’t just a parlor trick—schema quality is rock solid. Want to try it? Play with Gemini in this hands-on codelab.
Building Your First Data Connect App
Let’s roll up our sleeves. Setting up Data Connect is designed to be painless.
Why Building Should Be Instant
No one wants to spend hours on infrastructure just to test an idea. Data Connect delivers with fast onboarding, ephemeral databases for prototyping, and instant querying.
Step-by-Step Setup
- Start a New Service in Studio
Open Firebase Studio. - Design Schema with Gemini or Wizard
Let Gemini handle heavy lifting, or design schema manually. - Deploy to Cloud SQL
Choose ephemeral (for prototyping) or persistent DB. - Seed & Query Instantly
Input demo data, run live queries, modify as needed.
Get inspired by our movie review app walkthrough.
Browsing Data: Table vs JSON Views
Data Connect gives you classic SQL table views and old-school JSON blobs.
- Table View: Rows and columns (easy for relational reasoning)
- JSON View: Structured for mutation/debugging
Tip: Vector embeddings (for AI search) show as massive numeric fields—expand for details.
Lightning-Fast Full Text Search
Trust me, this feature is a game-changer. Powered by PostgreSQL, you can search text in ways that Firestore could only dream about.
Why Full Text Search?
- Users expect robust search (not just basic string matching)
- Multi-field queries (title + description, for example)
- Natural language and advanced operators (AND, OR, NOT)
How to Enable Full Text Search
Annotate fields in your schema:
Unknown block type \"code\", specify a component for it in the `components.types` option
Then craft your query:
Unknown block type \"code\", specify a component for it in the `components.types` option
Modes
- Plain: General terms
- Phrase: Exact phrase
- Query: Operators for advanced search (
love -potion gets results with “love” but not “potion”)
Check out how full text search works in detail.
Vector Search: Semantic Search for Real Apps
Let’s take your search to the next level—semantic matching with vector embeddings powered by Vertex AI.
Why Go Semantic?
Full text search is great, but what if users type synonyms or phrases? Vector search returns results that mean the same thing, not just match words.
How It Works
- Data (titles, descriptions) is fed to an embedding model (
text-embedding-gecko-005) - Embeddings live in a vector field (often 768 dimensions!)
- At query time, your search input is vectorized and compared for cosine similarity
Example: Vector Search Query
Unknown block type \"code\", specify a component for it in the `components.types` option
This finds “Love Potion” when you search “Love Tonic”—because meaning matters!
Defining Queries & Mutations: Making Your API
Here’s the “internals”: you define your queries and mutations (“operations”). Every operation is tracked with security, parameters, and API surface exposure.
Example: Add a Review Mutation
Unknown block type \"code\", specify a component for it in the `components.types` option
This gives frontend devs type safety and full API clarity.
Generated SDKs: Streamlining Frontend Integration
Once you’ve built your backend, integrating it is instant just hit “Download SDK” and roll into any modern frontend. Or have it build out your own connector.
Example:
Unknown block type \"code\", specify a component for it in the `components.types` option
- Fully typed SDKs for React, Next.js, Vue, Angular, and more!
- AI agents (like Gemini) plug in seamlessly.
Security: Auth, Row-Level Access, and AppCheck
Security isn’t just a checkbox—it’s part of every query and mutation in Data Connect.
Why Query-Level Security?
Instead of hand-writing complex security rules at the collection/table level, Data Connect lets you lock down each operation directly.
Key Directives:
@user: Requires authentication@public: Open access- Expression-based (e.g. admin role, claims)
Example: Secure Mutation Directive
Unknown block type \"code\", specify a component for it in the `components.types` option
- Row-level security: operations filter access using user IDs, roles, and more
- AppCheck compatibility—even for APIs like Stripe!
Local Development and Emulators
Yes—you can develop and test everything locally!
- Emulator spins up with PG Lite
- Seed and mutate data with demo apps in VS Code
- Test all queries, mutations, operations, and security
Start Data Connect emulator:
Unknown block type \"code\", specify a component for it in the `components.types` option
GraphQL Everywhere: Why It Matters
It’s official: Data Connect’s API is GraphQL-powered. Why should you care?
Why GraphQL?
- Ask for just what you need: No overfetching or underfetching
- Joins, fragments, reusable types: Compose powerful queries quickly
- Strong types: IDE/codegen support, easy refactoring
- Security at query layer: Fewer surprises
Example: Homepage Query With Fragments
Unknown block type \"code\", specify a component for it in the `components.types` option
Learning curve? Sure! But flexibility and power make it worthwhile.
Data Connect vs Firestore: How to Choose
Here’s Tyler Crowe’s advice:
“If you need relational joins, heavy analytics, or want to use Postgres extensions—pick Data Connect.
If you need real-time sync, offline support, or fast mobile-first stuff, Firestore is your bread and butter.”
Quick Guide
Firestore:
- Real-time sync, offline support
- Massive scale
- Simple queries
- Mobile-first
Data Connect:
- Complex queries, analytics
- Relational schemas
- Joins/aggregations
- Full text & vector search
- Web or cross-platform workloads
Still unsure? Ask Gemini right in Studio:
"Should I use Data Connect or Firestore for my app?"
What’s Next: Features Coming Soon
The Data Connect team is just getting started. Look out for:
- RAW SQL queries (not just views!)
- More advanced backend caching/performance tuning
- Custom MCP server support for agentic workflows
- Multi-data source (not limited to Cloud SQL)
- Deeper AI integrations
Stay updated via the CodingCat.dev blog.
Conclusion & Further Learning
There’s never been a better time to be a Firebase developer. Data Connect bridges the world of enterprise-grade SQL and developer-friendly, cloud-native workflows. Building secure apps, powerful queries, and integrating AI is finally within reach—whether you’re launching a startup or scaling to millions.
Key Takeaways:
- Data Connect brings SQL, security, and AI together in Firebase.
- You can use type-safe SDKs and GraphQL-powered APIs without fighting your infrastructure.
- Full text and vector search make your data "smarter" than ever.
- Security and access controls are baked in, not bolted on.
Subscribe
🚀 Subscribe to youtube.com/codingcatdev for weekly web dev, Firebase, and AI content!
Now, I’d love to hear from you:
What would you build with Firebase Data Connect? What’s your dream feature or integration?
Drop your ideas in the comments, tweet @CodingCatDev
Happy codingcatting for perfect peeps ✨
Subscriptions
Are you looking to boost your products reach? Have Alex create a video for you https://codingcat.dev/sponsorships
References & More Links
Note:
All code samples and workflows described here are for educational/demo purposes. Please consult the Firebase documentation for production best practices and latest updates.
This post is part of our deep dive series on Firebase. Major thanks to Tyler Crowe and the Google engineering team for moving web development into the AI-powered future!
",
+ "url": "https://codingcat.dev/podcast/firebase-sql-with-data-connect",
+ "title": "Firebase SQL with Data Connect",
+ "summary": "Firebase SQL with Data Connect how to use semantic searches with vector embeddings.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/85d81ee02a09ff89d5cbb7b3644e8793d98b5c13-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2025-10-28T19:48:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "f78ab69a-69a7-4e88-9496-22d1217bbd72",
+ "content_html": "Welcome back to CodingCat.dev! If you’ve ever found yourself neck-deep in the rabbit hole of identity, access management, and server security—especially in the brave new world of AI agents—today’s post is for you. I sat down with Dr. Tobin South, a security and AI expert, for a lively and insightful conversation that covers everything from fine-grained authentication and natural language access control (NLAC) to the nuances of Model Context Protocol (MCP) servers.
Whether you’re building your own MCP server or just curious about how identity management is evolving in AI-driven environments, this post will break it all down in simple, practical terms. And of course, we’ll share tons of real-world lessons, tips, and code to keep you safe, sane, and ahead of the curve.
“If you’re building an MCP server, you definitely need to check this out.” — Dr. Tobin South
Introduction: The Identity Management Problem in AI Agents
Have you ever handed over your admin credentials to an AI agent and prayed it wouldn’t accidentally nuke your production server? No? Just me? Well, as AI transforms the way we interact with infrastructure, applications, and even our inboxes, old-school identity and access management (IAM) tools are getting stretched to the limit.
In today's post, we’ll dig into:
- What fine-grained authentication looks like when AI agents drive the bus.
- How emerging protocols like MCP are reshaping best practices.
- The new problems (and hilariously dangerous pitfalls) you’ll encounter when serving up APIs for chatbots and generative models.
- Practical, real-world steps for building a secure MCP server today.
By the end of this post, you’ll have actionable advice on securing your MCP servers, managing agentic identities, and setting sane, practical access control—whether you’re at a startup or shipping software at scale.
Meet Dr. Tobin South: AI, Security, and the Academic Hustle
Dr. Tobin South is no stranger to the overlapping worlds of AI research, security, and enterprise software. After a wild ride through MIT’s AI scene (and a ChatGPT moment that changed everything), Tobin plunged into the depths of security—eventually landing at WorkOS, where he leads AI agent security, building MCP and IAM tooling for the world’s biggest AI labs.
“I spent a bunch of years thinking deeply about what AI security means... and there are really unique security challenges for agents.”
Tobin also runs a research lab at Stanford in partnership with Consumer Reports, focusing on consumer protection for the AI age. Think: safe transactional marketplaces, contextualized AI, and interoperable memory management.
- Academic meets industry: Practical solutions, experimental research, and policy influence.
- Stanford Consumer AI Lab: Where new ideas get dogfooded—and sometimes break stuff in spectacular fashion.
AI Security—The Agentic Future and Why It’s So Complex
Back in the day, security meant writing boring permission lists and hoping users wouldn’t share passwords. Now, with autonomous agents, you need identity infrastructure where an AI can “roam the web,” interact, and actually DO things for you—not just scrape content.
Key problems to solve:
- Delegated Authority: How do you ensure an AI agent only acts with your explicit permission?
- Safety Guarantees: What does “safe” mean when an agent can kill processes and buy tickets on your behalf?
- Fine-Grained Authentication: We’re moving beyond “all or nothing”—think scopes, context, and real-time oversight.
Dr. South’s Stanford research group throws crazy ideas at the wall (“what if agents could actually perform actions for you online?”) and asks the hard questions about identity, permission, and safe delegation.
Stanford Lab & Consumer Protection in the AI Age
It’s not just about writing safe code. What does consumer safety look like when you might soon be buying your next pair of pants through ChatGPT or Gemini? Stanford’s research lab runs experiments, advises students, and crafts real policy (like sections of the UN International AI Safety Report) on privacy and security.
How does an academic research lab work?
- Start with wild ideas—take them seriously.
- Ask students the off-the-wall questions (“what if agents roam the web?”).
- Conduct experiments to figure out what problems we need to solve.
- Feed findings into industry, policy, and standards.
Why it matters: As AI marketplaces, automation, and contextual memory get more complex, consumer safety requires new thinking—not just repackaging old IAM best practices.
How Standards Get Built: The OpenID Foundation and Fast-Moving MCP
Ever wondered how standards like OAuth, OpenID Connect, or MCP actually get written? According to Tobin, it’s equal parts collaboration, industry panic, and “LEGOs, not blueprints.” When the OpenID Foundation got curious about AI agents, Tobin helped launch a community group to pull together feedback from vendors, AI experts, and security pros.
- Dynamic world: MCP didn’t exist before October of last year.
- Fastest white paper ever: Industry is moving in real time, sometimes with “strongly held opinions in multiple directions.”
- MCP (Model Context Protocol): Enables standardized tool calling and state management for AI agents.
OAuth 2.1, Dynamic Client Registration, and What’s Next for MCP
Most security pros get nervous when words like "dynamic" and "client registration" pop up together. In the world of AI agents, it means your authorization server might need to let any agent connect—often pseudonymously. That sounds fun until you realize an agent could spawn a million applications in a single afternoon.
Recent changes:
- Client ID metadata—allows trusted agents (like Claude, ChatGPT) to declare themselves via DNS roots, so you don’t have to trust a random string.
- Dynamic Client Registration (DCR)—good for flexibility, but a big headache for security (“the wild west”).
Here's how it looks:
You get back a client_id and client_secret—but what’s to stop an agent from creating millions of these?
For background on DCR, read OAuth’s Dynamic Client Registration documentation.
The Security Challenges of Dynamic Tools and Agents
Imagine running VS Code and seeing a fresh OAuth client get created EVERY time you close and reopen your app. Or agents spawning millions of applications—each with their own keys and secrets. This is already happening in some test agents.
- Some agents create new applications each time
- Others reuse access tokens and clients
- Lack of standardization makes security unpredictable
Why standardization matters: As Tobin says,
“The wild west of technology is... very fun right up until the scale and the importance gets a bit too large.”
With AI, everything is happening at scale and speed we’ve never seen before. Even major players can’t keep up—hence the push for robust standards to ensure interoperability and safety.
Natural Language Access Control—NLAC, SORA, and the Evolution Beyond RBAC
Traditional access control models like RBAC (Role-Based Access Control), ABAC (Attribute-Based), and FGA (Fine-Grained Authorization) are starting to show their age—especially when AI agents interact via natural language. Enter NLAC—Natural Language Access Control—and new problems nobody saw coming.
Example: SORA App (AI Video Generation)
SORA lets you cameo friends in videos, turning on access so anyone can use your likeness. But the twist? You define what your AI deepfake can do using NATURAL LANGUAGE—just type it into a box.
“This is a crazy vision of the future of access controls on your own identity.”
Problems with NLAC:
- It’s not standardized.
- Scopes may need to be described in natural language.
- Enforcing control over NL is much harder than simple permission strings.
Agent-to-Agent Communication (A2A) — Natural Language, Power, and Worry
Say you want Salesforce AgentForce to talk to ServiceNow Agent—should they use a rigid API, or a more flexible natural language interface? Most agent-to-agent comms will happen over MCP, supporting arbitrary text-based requests.
Benefits:
- Unprecedented flexibility
- More powerful than strictly structured APIs
- Easier automation and integration
Drawback:
- Security is “terrifying”—hard to enforce precise control
- Restricting agents with scopes and permissions over NL remains challenging
Next-gen approaches may require sending and enforcing natural language scopes throughout the web. It gives users power, but opens new risks.
Fine-grained Permissions: NLAC, SSO, SKIM, and Context Management
We’re not likely to reinvent identity standards from scratch (“No, I don’t think we’re going to scan our eyeballs for World ID anytime soon”). Instead, Tobin argues we’ll adapt existing IAM protocols:
- OAuth 2.1, SSO, SKIM: Grabbing primitives from existing systems for provisioning and deprovisioning agents.
- Agentic Identity: “Halfway between a service account and a user.”
Problem: The complexity means OAuth standards (and MCP) will remain confusing and hard to use, but also robust.
The Real World: Setting Up an MCP Server the Right Way
Anyone can wrap an API behind an MCP server and throw an API key behind it. But that’s just scratching the surface—if you want safety, flexibility, and proper permissions, you need to go deeper.
Typical setup (v0):
- Wrap APIs behind MCP.
- Add API key or OAuth for access.
- Connect to Claude, ChatGPT, Gemini, etc.
- Roll with it.
But what’s wrong with this picture?
- Every user might supply an API key in unpredictable ways.
- Oauthing gives some identity, but how much access should you grant?
- Who controls which tools an agent can use, and when?
Here’s how Tobin suggests you level up your MCP server:
RBAC vs. Agent Trust
Don’t just map roles to permission sets. When an AI agent acts on behalf of an admin, it could have excessive privilege—like the Netlify CEO who accidentally took down the company’s core website!
Building Structured Tool Interfaces
Instead of exposing every API as its own tool, create general-purpose tools tailored to natural-language intents.
Designing Secure, Scalable API Access for AI Agents
Agents operate differently from humans. They can trigger API calls faster, in higher volume, and with less oversight. Standard dev-access patterns may be insufficient or dangerous.
- One API key per agent? Maybe for small use cases, but feels risky.
- OAuth tokens with scopes? Good, but how detailed and granular should scopes be?
- Dynamic tool access: Assigning access per user or per role.
“Your server text—the tool description—can completely change based on who connected to the server.”
Impersonation, Delegated Authority, and the On-Behalf-Of Problem
A common scenario: An executive assistant acts “on behalf of” the CEO, drafting or signing docs, but with more restrictions than the CEO herself. OAuth supports on-behalf-of flows and delegated authority, but actual adoption (especially in consumer use cases) is still spotty.
- Delegated Authority: Needed, but not widely supported.
- Consumer vs. Enterprise: Consumer platforms (Gemini, Claude, ChatGPT) own their agent ID; enterprises need robust audit, observability, and assurance.
Bottom-up adoption: As agents grow, repeated patterns will emerge, and organizations will need to balance flexibility, visibility, and compliance.
Agent Workflows, Orchestrations, and Real-World Security
Enter the orchestrators: Zapier, N8N, Google’s Agent SDK, OpenAI’s Agent SDK. Workflows let agents run asynchronously, acting on triggers and automating tasks across systems.
- Asynchronous agents: Have distinct identity and access challenges.
- Service accounts: In essence, but with dynamic, context-driven permissions.
- Human-in-the-loop still needed: Draft, review, and approve actions before sending.
This falls short of the holy grail—full workflow without constant approval—but strikes a balance between automation and oversight.
Fine-grained Access Control: FGA, NLAC, and Their Roles
Fine-Grained Access (FGA) lets you enforce dynamic, per-resource permissions—great for complex systems like Google Drive, less so for fast-moving, flexible AI tools. NLAC promises something even more adaptable, using natural language conditions and rules, but it’s hard to standardize.
- FGA: Powerful, but complex and heavy-weight for many agents.
- NLAC: Flexible, emerging, but not standardized.
Demo MCP Server Implementing FGA
Tobin built an MCP server where FGA is implemented INSIDE the server—tool descriptions and permissions are generated based on context, role, and conditions.
This approach is tricky, and NLAC might eventually overtake FGA in simplicity and flexibility.
What’s Next for MCP: Statelessness, Metadata, and Why the Future Is Fast
With the November 25th release of Model Context Protocol on the horizon, the most exciting development is statelessness in protocol design, making implementation and security easier for everyone.
“I’m very excited for a two-week rollout from a pure engineering perspective.”
Other improvements:
- Transport layer fixes
- Client ID metadata
- Better dev experience
- Responsibility in specs and standards
As AI agents go mainstream, and as protocols like MCP evolve at record speed, the standardization and incremental improvements will go a long way toward making this wild ride safe for everyone.
Curious to see all this in action, and hear more war stories from pioneers like Dr. Tobin South? Be sure to watch the full video on YouTube, subscribe to our channel, and join the conversation below.
What’s YOUR biggest challenge with access management for AI agents or MCP servers? Drop a comment, share your wins and “oops” moments, or let us know what you want to learn about next!
And don’t forget to follow us for more deep dives into the cutting edge of web dev, security, and AI!
External Must-Reads:
Stay curious, stay secure, and keep coding!
",
+ "url": "https://codingcat.dev/podcast/identity-access-management-for-agents-with-tobin-south",
+ "title": "Identity in AI Agents",
+ "summary": "Explore AI identity management with expert insights on MCP servers, agent security, OAuth, and future standards in AI. Unlock best practices and industry trends.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/b6824741d5542b93999d8f9be4ffef11e37dc52b-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2025-10-23T23:42:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "a3607438-2fa0-4660-be27-0b16bfb22b6c",
+ "content_html": "This is the place to learn how to bring your software ideas to life at lightning speed—without needing a fancy CS degree or burning the midnight oil on complex code. In this mega-long, ultra-detailed post, we’re diving deep into how no-code tools like Adalo and Bubble are changing the game for creators, hustlers, and even seasoned developers. You’ll even get some sample app builds, code snippets, demo flows, and insider tips you won’t find anywhere else.
Getting Started: Why No-Code?
Not a "technical" developer? That’s totally cool—and actually perfect for what we’re diving into. The whole point of no-code is to help anyone (seriously, anyone) take an idea—from that late-night lightbulb moment to a real, usable product—without needing to touch complicated code.
Here’s the big promise:
"Launch a product without having to code and without having a ton of money. Perfect, sounds like a winner to me." – Dan Hafner
No-code tools let you go faster, try out more versions, and focus on what matters: turning your logic into reality without the drag of traditional development cycles.
Meet Dan: The Journey into No-Code
Before we get hands-on, let's meet Dan Hafner, a builder who’s gone from political science, government gigs, and QA to full-blown no-code entrepreneurship.
A Not-So-Linear Path
Dan didn’t start out in a technical role, and honestly, that’s what makes his insights so valuable. From background investigations in DC to manual software QA for the Army, and eventually into remote business building, he combined a ton of different job skills—sales, QA, advertising—to land where he is today: the no-code world.
"I never planned on this, but it turned out to be something super cool. These tools helped me go from side hustle money to full-time business."
The flexibility of a remote, no-code business means freedom—something Dan needed while moving across the country with his wife, a NICU doctor.
Why No-Code Matters
The rise of tools like Adalo and Bubble is about more than just skipping code—it's about lowering the barriers:
- Don’t know how to code? No problem.
- Don’t have a ton of cash for a dev shop? Still no problem.
- Want to build something, not just dream? You’re in.
No-code levels the playing field for founders, hustlers, and creative thinkers everywhere.
Landing in No-Code Land: A Casual Origin Story
Let’s keep it real: most folks don't start out dreaming of software. Dan went through multiple “left turns”—from Poly Sci in college, a farm kid feeling out of place in D.C., to failing forward at a customer support job (barely keeping up with the tech, almost getting canned), before finally squeezing into a QA gig that opened the doors to software development.
"I almost lost that job, to be honest, because I just could not learn it fast enough. Eventually figured it out, got into quality assurance, and started working with web apps for the Army..."
He picked up Agile, Scrum, Jira, and all the regular software buzzwords, but skipped deep coding. That led him to realize—if you can think logically, you can build great stuff, even without a computer science background.
Adalo vs Bubble: The iPhone vs Android of No-Code
Here's the showdown! If you Google “no-code,” the first tool you’ll see is Bubble. But Dan’s fallen in love with Adalo for its simple UI and easy mobile publishing.
Bubble: Super advanced, tons of flexibility, lots of developer-like features, represents the "Android" side of things.
Adalo: Sleek, visually intuitive, the "Apple" of the no-code world.
Why Adalo Rules for Mobile
Dan notes that Adalo is especially good for folks wanting native mobile apps. You can easily export to Android/iOS; Bubble’s mobile support historically needed tricky wrappers but is improving all the time.
Live Demo: Building an AI MicroSaaS Generator in Adalo
It’s one thing to talk about no-code; it’s another to actually see it built. Dan takes us through building a microSaaS idea generator—the kind of tool that could be sold as a niche MVP, validation platform, or mini business.
Step 1: Get Started in Adalo
- Sign up
- Create new app: Go blank, call it:
AI MicroSaaS Generator - Pick branding, fonts, colors: Instantly applied everywhere
Step 2: Add Login & Signup Features
Adalo comes with pre-built authentication screens for users:
- Login
- Signup
- Homepage (land user after login)
Step 3: Design The Landing Page
Use Adalo’s drag-and-drop UI to add:
- Headers and welcome message
- Input field for idea submission
- “Try it out” button which triggers the AI validation
Step 4: Connect OpenAI for Instant Idea Validation
Adalo’s direct connection to OpenAI means you just need to drop in your API key.
- Go to Settings > API Keys
- Paste your OpenAI key
Want Google Gemini? Maps? They’re available, or you can connect any external service using Adalo’s built-in API connector.
Code Snippet: Basic API Call
Unknown block type \"code\", specify a component for it in the `components.types` option
"If you can type, click, and drag, you can build with Adalo."
Connecting AI: OpenAI, Google Gemini, and More
One of the real killer features in modern no-code platforms is easy AI integrations. You can literally plug in your API key and run sophisticated prompts directly from your app.
How Prompt Engineering Works
When setting up your microSaaS generator, you just jot out the instructions right in your custom action:
Unknown block type \"code\", specify a component for it in the `components.types` option
And you'll get back structured, readable feedback, directly from ChatGPT or Gemini.
Flexibility With Data Providers
While Adalo has direct support for OpenAI and Google Maps out of the box, you can hook up any external API—Firebase, Supabase, Xano, or your own backend—using the API connector.
Code Example: Connect External Database via API
Unknown block type \"code\", specify a component for it in the `components.types` option
The Component Marketplace: Shortcut City
Why build everything yourself? Adalo (and Bubble) offers component libraries / marketplaces where you can drag-and-drop:
- Apple/Google Sign In
- Audio Players
- Accordions, Tinder Swipe, custom lists
- Typewriter text (animated responses)
- Community/premium plugins
"These are community-driven, some are free, some paid. And developers can even make side cash selling components!"
Example: Animated Typewriter Text
Want those cool text reveal animations you see in ChatGPT clones?Just grab the Typewriter Text component from the marketplace, drag it onto your page, and bind the content to your AI response.
Unknown block type \"code\", specify a component for it in the `components.types` option
Handling Databases: Adalo’s Take & External Options
Data is king—so how does Adalo (or Bubble) deal with it?
- Adalo once capped collections and total records, but now scales (almost) infinitely for simple use cases.
- If you outgrow the built-in DB, you can hook into Supabase, Firebase, or XANO (HIPAA compliance, for medical apps!).
Example Collection Setup
microsaas_ideas (Title, Description, Owner/User)- Relationships: One user can have multiple ideas, but each idea belongs to one user.
Dan notes: Don’t worry about crazy joins or schemas if you’re small, but bulk CSV upload/export is super easy.
Code Snippet: Basic Data Model (Pseudocode)
Unknown block type \"code\", specify a component for it in the `components.types` option
Got tons of records? Use Supabase or Xano as a backend.
Building Out Authentication & User Roles
You want real users, protected routes, even paywalls or invite-only access? No problem.
Authentication Features (in Adalo)
- Native login/signup screens
- PIN-protected secondary walls for exclusive content
- Two-factor authentication (2FA)
- Role-based access (landlord, tenant, admin, regular user)
- Social logins: Google, Apple (drop-in components), Facebook/Twitter feeds via widgets
Code Snippet: Role-Based Access
Unknown block type \"code\", specify a component for it in the `components.types` option
Routing Logic
Set navigation logic so logged out users are sent to splash pages or onboarding flows; logged in users land on their homepage or dashboard.
Code Snippet: Simple Route Guard
Unknown block type \"code\", specify a component for it in the `components.types` option
"We built onboarding flows: profile pictures, bio info—easy as drag/drop. You can trigger flows based on user role selection at signup."
Publishing Your App: Web, Android, iOS, and Beyond
Here's where Adalo's "Apple approach" shines. When you’re ready to go live:
- Web Apps: Just toggle LIVE, attach your custom domain (CNAME), and hit "Publish."
- iOS/Android Apps: Export real native applications—no wrappers! Upload to App Store/Play Store.
"We build in our developer space, then transfer the finished app to the client’s Adalo account, or we offer hosting with hands-on updates and strategy support."
Example: Domain Setup
Just go to Settings → Domains → drop your CNAME, boom. For web apps, beta test instantly. For mobile apps, set up developer accounts (a service Dan charges for), and you’re off to the stores.
Bubble's Approach
Bubble is catching up, now rolling out native builders—but it’s still heavy on the browser/web side for now.
More No-Code Players: FlutterFlow, Supabase, Zapier, and HighLevel
No one tool rules the world—so here's a quick take on some extras.
FlutterFlow
- More "low-code" than "no-code"
- Slick UI potential, but you do need to know Firebase (and a bit of code)
- Great for developers wanting more control
Backend: Supabase & Xano
- Supabase: Postgres-based, great for advanced user/db needs
- Xano: HIPAA compliance for health apps, strong Adalo partnership
Glue: Zapier & Make (Integromat)
- Automate workflows between Adalo/Bubble & almost anything (Gmail, Slack, Stripe, etc.)
- Adalo has deep Zapier integration; Make is even more advanced for complex automations.
Code Snippet: Zapier Integration (Pseudocode)
Unknown block type \"code\", specify a component for it in the `components.types` option
Marketing Automation: Go High Level / Launchpad
Dan's team built a white-labeled HighLevel version—"Launchpad"—that plugs straight into Adalo. So, your users sign up, and you can zap their data right into marketing flows or drip campaigns.
Developer Struggles: Can You Really Touch Everything?
"I want access and the capability to change and touch everything." That’s the developer urge.
But no-code tools have gotten so good at opening up APIs and developer hooks that you can usually connect, extend, or integrate deep enough for most needs—without endless config files, webpack plumbing, or month-long sprints.
Traditional Dev vs No-Code Now
Old Way: Code every page, CRUD, auth, testing, deployment. Take forever.New Way: Drag, drop, connect APIs, publish instantly. Iterate fast.
"It’s the gold rush for creativity. APIs and integration mean you can connect almost anything. It’s exciting all over again."
Still want hand-coded pages? You can always mix your no-code app with custom Next.js/React flows, CMSs like Builder.IO, or plug JSON data into your own front-end.
Connecting the Dots: Workflows, Marketing, & Scaling
Launching an app isn’t enough. You need:
- Automated emails (Zapier)
- Funnels and landing pages (Go High Level, Launchpad)
- Payment processors, invoicing, CRM
- Bulk import/export of your user and app data
"There’s a no-code (or at least low-code) tool for any workflow now. You can go fast and not be boxed in by the old wall of 'I have to code everything myself.'"
Wrapping Up: Ready to Go No-Code?
We’re at a crossroads of no-code tooling and AI integration. Whether you’re a builder, entrepreneur, or classic developer, you can move way faster, explore wilder ideas, and build stuff instead of dreaming about it.
- The landscape is shifting fast—every week brings new platforms
- From microSaaS toolkits to full-blown social networks, you actually can go almost 90% of the way (and more)
- Scale when you need to; start simple, but always be able to add Firebase, Supabase, or raw code as needed
- AI-powered building lets you prototype, validate, and ship killer apps in days, not months
Big quote time:
"It’s the gold rush right now, man. Unbelievable. With AI and no-code coming into play, everything's fun all over again." – Alex (codingcad.dev podcast)
Get in Touch & Free Stuff!
Ready to go next level with no-code?
- Check out Dan’s site for resources, free guides, and a chat
- Dive into the CodingCat.dev library for more tutorials, podcasts, and how-to’s
- Connect with Dan Hafner on LinkedIn
- Free stuff, templates, and discount codes coming soon—watch CodingCat.dev for releases and bonus resources!
Sample Links From the Community:
",
+ "url": "https://codingcat.dev/podcast/build-a-custom-no-code-app-in-90-minutes",
+ "title": "Build a Custom No-Code App in 90 Minutes",
+ "summary": "Curious about no-code app building? Dive in as we explore Adalo vs Bubble, micro SaaS ideas, and real talk on launching products without code. Drop your thoughts below and share!",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/b7507d2e7862310280e622054777905351d5c214-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2025-10-12T15:27:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "28b2a0b5-07d1-4b23-a515-2cdc1c1e7731",
+ "content_html": "Welcome back to the CodingCat.dev blog! Today, we’re diving deep into a topic that’s near and dear to every busy developer’s heart: automation. If you’ve ever looked at your overflowing to-do list and wondered, “How can I get some of this done without losing my life to it?” — this one’s for you. We’re going behind the scenes with Joe, an automation and podcasting coach, who’s spent years refining processes, exploring tools, and developing frameworks that help him and his clients not only save time but also live the life they want.
This post pulls directly from a casual, fun, and super-detailed podcast interview packed with hands-on advice, links, frameworks, tools, and even a peek at Joe’s epic Stream Deck setup. So grab your coffee, get comfy, and let’s get automating.
Meet Joe: From Front-End Dev to Automation Guru
Most of us start somewhere — often with code, sometimes with burnout, and always with a drive to make things better. Joe’s journey is a classic developer story, with a twist:
- 20 years as a front-end developer
- Specialized in responsive web design (back when it was a new thing!)
- Podcasting coach and automation specialist
- Focused on helping people build “no-code” solutions
“I feel like I’m uniquely positioned here because I know how a computer thinks, and I try really hard to help people with no code solutions … not in the AI sense, but in the ‘let’s connect some tools and make it happen’ — no custom Python needed!”
Joe made the transition from a demanding agency life to self-employment — with the core goal of designing a lifestyle that balances work, family, and passion. And if you’re listening to this thinking, “I barely have time for dinner”— he’s been there too.
The Podcast Life: Escaping Burnout for Balance
Joe’s transition into podcasting wasn’t planned. It came from the realization that his skill set was perfectly aligned with the growing demand for audio content — and that podcasting offered exactly the kind of flexibility he wanted as a parent.
- Agency life = incompatible with being present for family
- Web development was fun, but podcasting became the passion
- Automation was the KEY to making it all work
There’s even a classic moment in the transcript where Joe describes a panic attack during a stressful year, and his three-year-old brings him water and reassures him. Talk about motivation for change!
“I made a promise to myself that I’m not going to put her in this position again. And so the next day I could work, I made a list of everything I did and then I put a check mark next to the things that only I could do. Everything else I automated or delegated to my virtual assistant.”
What Does “Automation” Really Mean?
Let’s get one thing straight: automation isn’t about “robot overlords” taking our jobs. In Joe’s world (and ours), it’s about finding shortcuts, reducing friction, and freeing your brain to do what matters.
Joe explains the true roots of “hacker” culture:
“The word hacker originally meant finding a shortcut — not breaking into computers, but figuring out how to do something in less time.”
That’s the vibe for this whole guide. No secret formulas, no pie-in-the-sky promises — just real, practical advice for getting stuff off your plate (whether by robots or real people).
Joe’s PER Framework for Automating Any Task
Ready for the playbook? Here’s Joe’s PER Framework — the dead-simple method for deciding what and how to automate.
P — Perform
- Actually do the task
- Record or document every step
- Note all the inputs and outputs
E — Evaluate
- Do you need to do this?
- Is this something only you can do?
- Are the inputs and outputs ALWAYS the same?
- If yes, could a computer do it?
- If no, could another person do it?
- Do you WANT to do it?
- Some stuff (e.g. sponsor outreach) might be best kept in your own hands.
R — Remove
- If a computer or another person can do it, offload it
- If it’s not necessary at all (dead weight), just drop it
“You don’t have to automate everything, everywhere, all at once. Just pick something that takes a lot of time, document everything you do, and start from there.”
Try this as a checklist:
Unknown block type \"code\", specify a component for it in the `components.types` option
Breaking Down the Podcast Engine
Curious what an automated podcast workflow looks like in real life? Let’s go step-by-step.
The Podcast Process
- Editing: Audio and video files delivered from the editor via Dropbox (named by episode number)
- Automations kick in:
- When the files hit Dropbox, a Zapier/Make flow triggers and updates Notion with episode status (“Ready for scheduling”)
- Virtual Assistant (VA) gets email notification
- VA Actions:
- Upload video to YouTube
- Copy show notes from Notion
- Upload audio to hosting (e.g. Transistor)
- Link up related YouTube fields and show notes
- Add chapters, schedule release
- Post on WordPress using custom block template
- Add transcript from CastMagic (automatically fetched on publish day)
- Upload video to Spotify for Creators for video podcasts
This way, the creator only needs to:
- Record the episode
- Approve final cut if necessary
- Everything else kicks off through automations and SOPs
“Once I’m done recording, I don’t even see it again until it’s in my RSS feed.”
Top Automation Tools
Let’s dig into the tool stack that Joe uses all day, every day. These are tested, recommended, and, as Joe puts it, selected because they reduce friction. Here’s what makes the cut:
The king of easy automation — connects 1000s of apps. If you want “set it and forget it” tasks between Google Sheets, Notion, Dropbox, YouTube, and more, this is probably your starting point.
Example: Automating Podcast Episode Status in Notion
Unknown block type \"code\", specify a component for it in the `components.types` option
But you can achieve this via Zapier’s built-in UI, mapping fields between services — no code required.
If you need more control, especially with custom APIs, webhooks, or “every little detail” — Make.com is more affordable but requires a steeper learning curve.
Example: Operations Counting
- Each step in a workflow = an operation.
- Watch your step count to avoid hitting limits if you have a high-volume process.
Connecting Google Services (More technical)
Unknown block type \"code\", specify a component for it in the `components.types` option
But this is all drag-and-drop in the Make.com UI.
Joe uses Apple Shortcuts for startup/shutdown routines — dictating daily checklists directly into Obsidian to clear his mind and capture tasks.
A Mac classic for auto managing files. Useful for organizing podcast downloads, renaming files, moving them to correct folders, and more.
Example: Hazel File Rule
Unknown block type \"code\", specify a component for it in the `components.types` option
No code needed — Hazel’s UI lets you set up “if this, then that” file automation rules.
This is where the magic happens for repetitive tasks, quick timers, scene switching, and more. Joe’s not kidding when he says he has three Stream Decks — each with shortcuts to automate common actions.
- Start time trackers
- Run Apple Shortcuts
- Control lighting
- Copy/paste with auto-replace (e.g., fix video URLs with domains)
- Switch podcast scenes
“All of this is to reduce friction in my day, right? … I want to look at the camera, I want to switch scenes. I don’t want to spend time dragging windows or finding links.”
Visual example:
Automate complex keyboard shortcuts and clipboard tasks.
Obsidian: Personal knowledge management — ideal for note-taking, journaling, and capturing daily routines. Runs Joe’s startup/shutdown scripts via Apple Shortcuts.
Notion: Project management, process docs, sponsorship/customer tracking, handover documents for VAs.
Automation in Action: Real-Life Examples
Joe’s approach is all about attacking friction, step by step. Here are some great examples:
File Management for Podcasts
Hazel watches the Riverside download folder, organizes files by role (Host/Guest), and puts them in a workflow folder — ready for editing, naming conventions applied. No more manual dragging and renaming!
Stream Deck = Productivity Powerhouse
- [ ] Start/Stop timers instantly for time tracking
- [ ] Switch scenes in ECAMM live for podcasting
- [ ] Copy markdown for YouTube descriptions
- [ ] Turn on/off recording lights (smart bulb)
Task Delegation with VAs
“When I perform a new task, I record a Loom video and tell my VA to watch it and write step-by-step instructions. She’ll send it to me for review, and once that’s right, she takes it over.”
Transcripts, video publishing, show notes, and embeds are all handled by SOPs shared in Notion — tracked by status, with notifications triggered by automations.
Apple Shortcuts + Obsidian: Mind-Clearing Routines
Joe dictated his shutdown routine via Apple Shortcuts while driving — allowing him to clear his mind and capture loose ends for tomorrow.
Unknown block type \"code\", specify a component for it in the `components.types` option
Choosing: Zapier vs. Make.com for Your Workflows
Let’s get honest about the two big automation platforms.
Zapier
Pros:
- Extremely user friendly
- Tons of integrations (especially with small/startup products)
- Partnerships (e.g., Google)
- Easy to share templates
Cons:
- More expensive if you need complex workflows
- Some limits on flexibility
Make.com
Pros:
- Much more affordable
- Fine-grained control and webhooks galore
- Developers can dial in logic or handle custom APIs
Cons:
- Steep learning curve — definitely more technical
- Some integrations require custom app setups in Google Cloud, etc.
Joe’s rule of thumb:
“For my clients, I always do this preamble — ‘I use Make.com for most of my automations, but you should use Zapier. Zapier is bigger, easier, and a lot of little details are abstracted away.’”
Example: Social Media Automation
Let’s say you use SocialBee for posting — Zapier will likely have a plug-and-play integration; Make.com might require you to build something with webhooks. Decide what’s worth your time (and money) up front!
Delegation and Teamwork: VAs, SOPs, and More
A HUGE piece of automation isn’t the robots — it’s people. The more you can standardize a process and hand it off, the more you can focus on what really matters.
How Joe Delegates Podcast Production
- Loom video or similar: Record task as you do it
- VA watches and transcribes the steps
- You review SOP for accuracy
- Repeat when process changes
And it’s not just about saving time — it’s about creating a system that survives burnout, absence, or growth.
“A lot of people say low-level VAs will cost you time in management. But if you hand off the SOP while doing the task yourself, that’s not a problem. Most of the time, it’ll be right.”
Handling Podcast Sponsorships and Relationships
Joe’s honest: “I hate CRMs.” But staying organized with sponsor outreach is still a must.
His System:
- Built his own tracking setup using Notion, Make, and Todoist
- Updates sponsor/tracking status in Notion
- Automation triggers a Todoist task for follow-up — because that’s where to-dos get solved
Links:
“Relationship building is the most important aspect for me. I do all my sponsor outreach — it keeps things personal, and ultimately I’m a personal brand.”
How to Get Started Today (A Step-by-Step Plan)
Feeling overwhelmed by all this? Here’s Joe’s advice for starting small and winning.
Quickstart Automation Plan
- Write it down
- Spend a day logging everything you do
- Note every repeated task
- Run the PER Framework
- Perform: Document every input, output, and step
- Evaluate: Could a computer handle this? Could another person?
- Remove: Drop what doesn’t matter or can be offloaded
- Pick your tool
- For ready-made integrations, use Zapier
- For custom tasks, Make.com
- For file management, Hazel
- For routines and mind-dumps, Apple Shortcuts + Obsidian
- Make it frictionless
- Set up a Stream Deck or Keyboard Maestro for your most annoying repeat actions
- Delegate wisely
- Record yourself doing the task and have your VA transcribe the steps
- Review SOP occasionally to keep things current
Iteration is key: Don’t feel like you need the perfect solution from day one. Start with a small automation or delegation, test, tweak, and improve.
“We’re not building skyscrapers — we can iterate. Try something; if it doesn’t work, try something else.”
Real-World Example: Podcast Guest Management
Want to automate guest scheduling and communications? Here’s a Zapier recipe:
- Trigger: Guest books a time via Calendly
- Action: Add row in Google Sheets with guest info
- Action: Send guest a personalized survey via email
- Action: Update guest status in Notion
- Action: Notify you or VA for follow-up
Zapier Code Step Example:
Unknown block type \"code\", specify a component for it in the `components.types` option
Set up via Zapier UI — no code needed, unless you want custom logic!
Resource Links and Community
Find more automation ideas, tools, and guides:
For more, check Joe’s Podcast and Sponsor Magnet for advanced strategies.
Final Thoughts: Build Your Own Map
Joe wraps up with a reminder that automation is about making your life easier. Whether you’re running a podcast, managing a startup, or just keeping your head above water, these frameworks, tools, and habits can help you create time for what matters.
Start simple:
- Track your tasks
- Evaluate what really needs a human touch
- Offload, automate, and reduce friction
And most importantly:
“Don’t overthink it. Try small things; tweak if it’s not working, toss if it really doesn’t fit. You’ll keep making it better for yourself.”
If you decide to go deeper, the CodingCat.dev team has tons of tutorials, sample code, and blog posts waiting for you. Who knows — once you start automating, you might never look back!
Got questions, want more tutorials, or have an automation win to share?
Sound off in the CodingCat.dev Discord — we’re all building the future, one shortcut at a time.
Blockquote
“You don’t go to a restaurant because you can’t cook — you go because you don’t want to have to. The same goes for automation. Sometimes, paying for Zapier is like paying for dinner — buy yourself the time back!”
Code Appendix: Favorite Snippets
If you’re a developer, here are a few jump-start code templates for common automations discussed:
End Note
A huge thanks to Joe for breaking down his process and showing us behind the curtain. Whether you’re starting out or scaling up, automation is about getting your time back so you can live your theme — not just your to-do list.
Feeling inspired? Time to pick your first task and start hacking!
",
+ "url": "https://codingcat.dev/podcast/how-to-automate-your-work-and-free-your-time",
+ "title": "How to Automate Your Work and Free Your Time",
+ "summary": "Discover how to automate your tasks and reclaim your time! Join Alex and Joe as they discuss podcasting, automation, and work-life balance strategies. ",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/ab921ffcf80d0ce4ea1dc35d6c24086fd6fb9968-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2025-10-08T18:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "d2d147a2-5954-4ee4-9548-222667035923",
+ "content_html": "Welcome back, perfect peeps! Today we’re diving deep into how to bring the power of AWS to your local machine using LocalStack. If you build cloud-native apps, dabble in side hustles as a solopreneur, or want to experiment with AWS services without racking up a bill, this is the post for you. You’ll learn what LocalStack is, why it’s awesome, and how to use it—from setting up buckets to working with Lambdas, CDK, CI/CD, and beyond.
Grab your favorite mug (mine’s a Netlify mug with way too much coffee in it), sit back, and let’s see how LocalStack can seriously change up your cloud development workflow.
Meet the Hosts: Catching Up
“I think it’s actually been about three years since you’ve been on the show, which is insane to me.”
If you’ve tuned in before, you’ll recognize your hosts from the DevRel (developer relations) world: Brian Reinaldi, previously at LaunchDarkly and now at LocalStack, and Alex, your guide here at CodingCat.dev.
Like so many of us, Brian juggles a full-time gig with a bunch of side projects (check out cfe.dev for his event lineup). Burnout happens, life gets busy, and conference schedules never slow down. But the passion for sharing cool tools survives! That’s why Brian’s here to give us the rundown on how LocalStack has changed his approach to both work and play.
What Is LocalStack?
If you’ve ever dipped your toes in AWS, you probably know the pain: spinning up resources is slow, can get expensive, and—let’s be honest—just plain scary when you’re learning. Enter LocalStack.
High-Level Overview
LocalStack is basically AWS on your laptop. You get a Docker image that emulates AWS services locally, so you can spin up buckets, run lambdas, use API Gateway, and much more—without ever touching your actual AWS account.
You don’t need to sign up for an AWS free tier. You don’t have to worry about forgetting to tear down an EC2 instance (and getting surprising bills). Instead:
- You experiment and build locally
- Everything is ephemeral
- You control costs (no “I left a GPU running all weekend” horror stories)
- Debugging, iteration, and CI/CD are all much, much faster
And you can reset and restart as often as you want.
Free vs Paid
LocalStack has two flavors:
- Open Source (Free)
- ~30 AWS services (S3, Lambda, API Gateway, CloudFront, etc.)
- Use as much as you want, forever
- Pro / Paid
- ~120 AWS services emulated (and counting)
- Premium features: more services, tools for debugging, step-through, chaos engineering, more
- This is how the LocalStack team eats (support them!)
Which Services Are Covered?
“We have like really close parity with AWS on certain services (like S3 & Lambda)... Some APIs are partial. But for core stuff, we’re very close.”
AWS itself has ~300 services, but most modern apps rely on a handful: S3, Lambda, API Gateway, DynamoDB, CloudFront. For these, LocalStack hits near parity with AWS. For edge-case services or those you almost never use, some features may lag behind.
Check out their full list of supported services to see if your stack is covered.
Main Features
Some of the headline goodies you’ll find in LocalStack:
- Run dozens of AWS services locally
- Step Debugging: Attach a debugger to your (Node.js) lambdas, set breakpoints
- Hot Reload: Save your code, instantly test the change, no manual deploys
- Chaos Engineering: Simulate API slowdowns, outages, or dropped packets to test how your app handles “the real world”
- Web-based UI: Inspect resources, invoke lambdas, and debug from your browser
- Thin Wrappers for CLI Tools: Keep your workflow—just redirect commands to “local”
“The real savings isn’t just the money. It’s the time you save as a developer. Tests that took 45 minutes now take just a few minutes.”
Setting Up LocalStack – Step By Step
Okay, let’s get hands-on. Here’s how to set up LocalStack on YOUR laptop, with tips straight from Brian and Alex.
The Docker Flow
LocalStack runs as a Docker container. So:
- Install Docker
- Pull LocalStack:
Unknown block type \"code\", specify a component for it in the `components.types` option
- Run LocalStack:
Unknown block type \"code\", specify a component for it in the `components.types` option
- By default, LocalStack runs with GUI/API access on port
4566. (Side note: multiple ports are used internally.) - Install the CLI wrapper (optional, but way easier):
Unknown block type \"code\", specify a component for it in the `components.types` option
Or grab their LocalStack CLI for more features.
Web App UI: Not Just for the Cloud
Here’s something wild: LocalStack’s web UI runs on your laptop, but acts just like a cloud console.
What’s actually happening:
- The web app connects to your local Docker instance
- You browse buckets, lambdas, APIs…the whole works
- Everything is still 100% local to YOUR machine
“People actually get confused. They open the web app and think, ‘Wait, is this running in the cloud?’ Nope! It’s all on your box. But with all the power of a cloud console.”
Learning AWS for Real: Project Walkthrough
CDK, CloudFormation & Infrastructure as Code
To get serious with AWS, you’ll want Infrastructure as Code. Brian’s example uses CDK (Cloud Development Kit)—a tool that lets you build AWS resources in TypeScript, Python, etc., then transpile down to CloudFormation.
- CDK Stack: Write resources like
new s3.Bucket() or new lambda.Function() in your preferred language. - Bootstrapping: CDK zips everything up, creates CloudFormation, and shoves it into S3 for deployment.
- Deploy: Actually builds resources locally—in our case, via LocalStack.
Workflow:
Unknown block type \"code\", specify a component for it in the `components.types` option
If you’re used to writing TF with Terraform, this is a super similar flow.
Makefiles for Convenience
Brian uses a Makefile to automate all his steps, like:
Unknown block type \"code\", specify a component for it in the `components.types` option
So he can just:
Unknown block type \"code\", specify a component for it in the `components.types` option
Easy button!
Example: S3 Static Site Hosting
Let’s walk through building a basic static site—think Netlify in the early days, just HTML/CSS/JS.
1. CDK Stack Definition
Here’s (roughly) what your TypeScript CDK stack might look like:
Unknown block type \"code\", specify a component for it in the `components.types` option
What’s going on?
- Defines an S3 bucket for static website hosting
- Enables public read, sets index/error documents, CORS, and SSL
- (Optionally) Uses S3 deployment to push static files into the bucket
Assets: Your /site-assets folder holds index.html, styles.css, JS, etc.
2. Deploy Locally
With LocalStack running:
Unknown block type \"code\", specify a component for it in the `components.types` option
3. Inspect in the Web UI
- Open the LocalStack web app
- Browse to the S3 bucket
- See your static assets:
index.html, JS, CSS, etc.
4. Access the Hosted Site
LocalStack gives you a special URL pointing to your local instance, like:
Unknown block type \"code\", specify a component for it in the `components.types` option
Open in your browser. You’ll see your static site, served by a local clone of AWS S3.
Blockquote
“I'm doing this all locally. This is running, if you see that URL, it's actually pointing at your local instance... Not in the cloud, running on my machine.”
5. Bypass Annoyances
Working on the real cloud often hits you with CORS errors or IAM permission headaches. LocalStack lets you:
- Disable CORS for easier development
- Bypass IAM (security) during dev, then enable to test before pushing to prod
Read more on local AWS S3 restrictions.
From S3 to Global: Adding CloudFront
Let’s make things more “real world.” You want to deploy your site globally (via CDN) instead of from a single S3 bucket.
What’s New?
- Add a CloudFront distribution in your CDK stack
- CloudFront sits in front of your S3 bucket
- Disables public access on S3 (only CloudFront has access)
Access the CDN-Backed Site
The app is almost identical visually, but now served through a global CDN—and still completely local. Sound slow? Actually, on LocalStack, these setups build in seconds, not minutes.
“A CloudFront distribution can be super slow to deploy to AWS... This is going to deploy in a matter of seconds with Lambda, CloudFront, and S3.”
Serverless Power: Adding Lambdas
Static sites are fun, but sometimes you want server-side logic. Enter AWS Lambda.
1. Basic Lambda Example (Node.js)
Lambda handler file (e.g., src/handler.js):
Unknown block type \"code\", specify a component for it in the `components.types` option
2. Add Lambda to Your CDK Stack
Unknown block type \"code\", specify a component for it in the `components.types` option
3. Expose via Lambda Function URL
You can create a public URL to hit your Lambda directly (for demos, not production!):
Unknown block type \"code\", specify a component for it in the `components.types` option
4. Deploy, Test, and Play
- Deploy with
make deploy-lambda - LocalStack instantly exposes your
functionUrl - Hit it in your browser or with
curl:
Unknown block type \"code\", specify a component for it in the `components.types` option
5. Web UI FTW
Invoke the lambda, add debug payloads, and get instant feedback via the web app. No more click-wait-refresh nightmares!
API Gateway: Real-World Full Stack
Now, let’s build a real API like you’d use for your single-page app’s backend.
1. Integrate Lambda With API Gateway
Instead of exposing your lambda directly, route it through API Gateway.
Unknown block type \"code\", specify a component for it in the `components.types` option
Now, when you hit /hello on the API endpoint, it triggers your lambda!
2. Deploy & Call
Unknown block type \"code\", specify a component for it in the `components.types` option
Get the API endpoint (from CDK outputs or the web UI):
Unknown block type \"code\", specify a component for it in the `components.types` option
Visit in the browser or use your front-end code to call this endpoint.
3. Put It All Together
- S3 hosts the static site
- CloudFront sits in front as a CDN
- API Gateway exposes endpoints
- Lambda runs your server code
- All running locally
“This is now calling somewhat getting closer to a real app... not just a function URL endpoint, but an API, hit by the front-end.”
Bonus: Other AWS Services
Want more? LocalStack (even in the free version) supports:
- DynamoDB: Run a NoSQL backend locally for your app or play with familiar Dynamo queries
- SSM (Systems Manager) / Secrets Manager: Store environment variables, secrets, and config
- IAM: Simulate auth rules (can be turned off/on to make life easier in dev)
You can use thin wrappers for AWS CLI, CloudFormation, Terraform, CDK, and more.
Example CLI wrapper usage:
Unknown block type \"code\", specify a component for it in the `components.types` option
“We even have a tool (Pro) called IAM Policy Stream that actually tells you what IAM policies you're missing for different services!”
Debugging, Hot Reload, and Chaos Engineering
Step Debugging
With Pro, add breakpoints to your serverless functions, use your favorite IDE, and step through lambdas—something most AWS users only dream about.
Hot Reload
Saving your lambda code triggers instant reload and redeploy. No more zip && upload && test loops.
Chaos Engineering
Want to make sure your app doesn’t break if AWS throws a wrench in the works? LocalStack lets you simulate:
- Slow network calls (latency injection)
- Dropped packets (simulate outages)
- AWS API failures
Great for modern observability/testing practices!
“You’d be surprised how often packets are lost or latency spikes in the real world. Now you can test your resilience before you ship!”
LocalStack in Your Workflow: CI/CD & Team Dev
Developer Workflow
Picture this: Two developers are working on a feature, each running LocalStack on their laptops.
Workflow:
- Each dev spins up LocalStack locally
- Modifies CDK or Terraform code
- Tests lambdas, endpoints, etc., with full hot reload and step debugging
- Commits to git
CI/CD Pipeline Integration
Teams often spin up test environments on AWS for every pull request (expensive and slow). With LocalStack:
- CI/CD pipeline pulls your code
- Spins up LocalStack Docker container
- Deploys your full app to local stack in the CI runner
- Runs integration and unit tests (all local, zero cloud costs)
- If everything passes, deploys to AWS
Benefits
- No more “who forgot to shutdown the test instance?” bills
- CI builds run faster (tests go from 45min to 2min in some cases)
- Easy rollbacks (just restart local stack)
- Fast local feedback = more experimentation
Speed Gains
“It’s all faster! Unless you’re paying Buco bucks for your AWS builds, nothing beats your own dev laptop for speed.”
What’s Next for LocalStack? Beyond AWS
LocalStack started as “AWS locally,” but their dream is multi-cloud on your laptop.
GCP, Azure, and More
- Snowflake Emulator: Already in pre-release (for data science workflow fans)
- Azure: On the roadmap, with early experimental bits already out there
- GCP: No timeline yet, but definitely a future goal
- Custom Connections: Hook up LocalStack to third-party providers (like Okta, Auth0, etc.) easily
“Big vision: Whatever you do on the cloud, you should be able to do locally in LocalStack—across AWS, Azure, and GCP.”
Why Multi-Cloud?
Companies rarely stick with one provider forever. Multi-cloud is on the rise, and being able to test any cloud workflow locally is going to be a must.
Final Thoughts & Resources
After using LocalStack, Brian’s main takeaway is speed and freedom. It’s not just about saving money—it’s making cloud development fun to experiment with again.
“It let me experiment and iterate without all the pain and delay. Now I’m more likely to try new things, mess with new APIs, because it’s so fast and safe.”
All the demos we saw today (including Lambda, S3, CloudFront, API Gateway, and even DynamoDB) are included in the free version, so you can start experimenting without spending a dime.
Resources & More Reading
For extra hands-on guides and new episodes:
Join the Conversation
Have questions? Want to share your horror stories (or victories) using LocalStack? Hit us up at @CodingCatDev or drop a comment below!
",
+ "url": "https://codingcat.dev/podcast/running-the-cloud-on-your-desktop-with-localstack",
+ "title": "Running the Cloud on Your Desktop with LocalStack",
+ "summary": "Tired of AWS running in the cloud, how about right on your desktop?",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/6835d7b8549dd59339a6e475e64b8ff079d64ab1-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2025-10-06T13:06:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "e4aefc68-fdf9-441a-962f-653d10a3f01a",
+ "content_html": "Welcome back, CodingCats! If you’re itching to untangle the furball that is app permissions, you’re in the right place. This is CodingCat.dev, where the dev snacks are always fresh, and today, we're serving a feast of practical authorization wizardry for your modern full stack projects.
In this mega post:
- We’ll break down why building your own Authz is a waste of precious dev time
- Dive deep into live SpiceDB Playground tricks
- Demo rich Google Docs–style ACL modeling
- Use code and schema to empower comment, edit, and view flows
- See LLMs, SpiceDB, and the cloud come together for AI-powered authorization Q&A
- Explore modern tooling, compliance, Edge caching, bulk permission checks, and scalable strategies
- Compare top platforms: Authzed, WorkOS, Okta FGA, and Cerbos
- Show how to leverage all this in React and NextJS—check out our Ultimate NextJS Guide too!
So grab snacks and a notepad—let’s tame NOT ONLY Authz code, but also your whole approach to permissions, once and for all.
“Stop building Authz. Start building value.”
—A rallying cry from today’s guest experts!
Why Authz? Why Now?
Let’s face it: Most apps go through the same growing pains with permissions:
- “Add a new feature? Time for days lost in auth spaghetti!”
- “Business pivot? Ouch, I’ve gotta refactor our fragile object ACL logic.”
- “How did Bob get access to this? Who has edit here?”
Authorization (authz) is the backbone of scale—think Dropbox, Google Docs, even your favorite project management apps. But building and maintaining a robust, flexible, and scalable authorization layer is hard and boring, and distracts your team from delivering features that matter.
If you’re still rolling custom permission tables and hand-coding complex checks, it’s time to rethink your approach.
Stop DIYing Permissions: What’s the Point?
Before we dig into code and tools, let’s be honest: Why do most developers roll their own Authz?
- “It’s simple… until it isn’t.”
- “My use-case is different.”
- “Third-party tools seem overkill for my MVP.”
It always starts simple:
Unknown block type \"code\", specify a component for it in the `components.types` option
But soon you have editors, commenters, nested folders, group sharing, org-wide rules, audit requirements, and suddenly your code looks like spaghetti mixed with cheese sauce and sad.
And the more “value” you build, the less you want to touch that fragile Authz tower!
That’s where platforms like SpiceDB and Authzed come in—they’re built on decades of learning by Google, Dropbox, Okta, and more. You get robust, scalable logic without tying your dev time in knots.
Meet the Playground: Real SpiceDB in Your Browser
Let’s start with something hands-on:
Imagine if you could instantly try out your actual Authz schema, live, in your browser.
That’s what the SpiceDB Playground gives you.
What’s unique here?
- Runs a real, compiled-to-WASM instance of SpiceDB in the browser
- NOT a fake rules engine or mock, but the same engine your production code could hit
- You can define objects, relationships, permissions—test scenarios instantly!
Let’s take a tour:
To get started, try this:
- Launch the playground (see SpiceDB's Playground or codingcat.dev)
- Check out sample schemas (like our Google Docs-style sharing below)
- Define objects:
user, document - Create relations: who can read? who can write?
- Add permissions, like
edit, view, comment - Test with sample data. Does Alice have view on Doc X?
Google Docs-Style Permissions: Modeling with SpiceDB
Google Docs set the standard for powerful, granular sharing—let’s see how you can build that using SpiceDB.
Say you want:
- Users
- Documents
- Relations like “reader”, “writer”, “commenter”
- Permissions derived from those roles
A simplified version of your schema might look like this:
Unknown block type \"code\", specify a component for it in the `components.types` option
What’s going on here?
writer and reader are relations (think: “Alice is a reader of Document1”)edit permission is whoever is a writerview permission is anyone who’s a reader or a writercomment covers both commenters and writers (but not plain readers)
When a user shares a doc
When Alice shares a doc with Bob:
- The system writes a “Bob is a reader of Document1” relationship into the DB
- When Bob hits the link, the system asks: “Does Bob have view permission on Document1?”
- SpiceDB answers YES/NO by evaluating schema and relationships live
“You don’t have to manually figure out which roles grant which permissions every time.
Just ask: Can Bob view this doc? Your schema does the heavy lifting.”
Step-By-Step: Schema, Relationships & Queries
Let’s break down a real workflow.
1. Define Types and Relations
Unknown block type \"code\", specify a component for it in the `components.types` option
2. Add a “Commenter” Role
But what if you want some users to just comment (not edit)?
Add:
Unknown block type \"code\", specify a component for it in the `components.types` option
Pro tip: View is now anyone who’s a reader, an editor, or a commenter.
3. Writing Relationships
In production, your app writes relationships as users interact:
Unknown block type \"code\", specify a component for it in the `components.types` option
Or in the playground’s “test relationships” area:
Unknown block type \"code\", specify a component for it in the `components.types` option
4. Querying: Does Sam have comment permission?
This is done via a simple API call:
Unknown block type \"code\", specify a component for it in the `components.types` option
Result: YES!
5. Test-Driven Permissions
Worried about a new schema breaking old assumptions?
- Use the playground to model and test before deploying.
- Add/check relationships and see if permission trees evaluate as expected.
Richer Permissions: “Commenter” and Beyond
Let’s go wild: Suppose you want a completely custom role, like “auditor” or “guest editor.”
Just add a relation and permission:
Unknown block type \"code\", specify a component for it in the `components.types` option
Your app can now add users to any role and derive permissions through combinations.
Set Operations
The + operator in SpiceDB schema is like a union of sets. Think:
“The set of everyone who can edit, combined with everyone who can comment…”
You get composite permissions as easily as combining sets.
Testing and TDD for Permissions
A killer feature of the playground is how it visualizes the logic tree for each permission check:
- View the full computation:
“Is Sam a commenter?” NO
“Does Sam have edit?” NO
...
Result: YES/NO - Try failing and passing scenarios, all visualized:
- Add new relationships: Did you just grant someone access by mistake?
- Change schema and instantly see if breakages appear.
Key lesson:
Model your perms before you code business logic!
Just like TDD for backend code, test-driven permissions lets you iterate quickly and avoid costly mistakes.
How Frontends (like React) Use Permission Results
So you have these fancy checks—how do you actually wire them up in your web app?
Typically:
- Your server wraps features/components in permission checks
- If the check returns YES, show the button/feature; if NO, don’t render
Example in React:
Unknown block type \"code\", specify a component for it in the `components.types` option
Question:
Should you hit SpiceDB over and over from the UI?
Answer: Maybe not—there are options.
- Bulk permission checks:
Ask for all relevant permissions in one round-trip. - Cache results at the edge (e.g., popular docs, org-wide roles).
- Keep latency in mind:
SpiceDB is blazingly fast—single to double-digit ms with proper config.
“With a 20ms round trip, your permission checks likely aren’t the bottleneck—focus on bigger page loads or DB calls.”
Going Beyond: Bulk Checks, Latency, and Edge Caching
As your app grows, don’t let authz checks slow you down.
Bulk Checks:
Need view, edit, and comment permissions for 10 docs and 1 user?
Use the bulk API to ask all in one shot.
Unknown block type \"code\", specify a component for it in the `components.types` option
Edge and Caching:
Permissions may change during a session—cache them, but be ready to invalidate if the user’s roles update.
Scaling to millions:
SpiceDB and Authzed can handle millions of rels and users, but be aware:
- Lookup APIs (who has access to X?)—optimized for big scales with features like Materialize
- For super-complex scenarios, talk to their team (or check out CodingCat.dev guides).
RBAC, ABAC, Policy Engines, and Where SpiceDB Fits
Let’s clear up jargon:
- RBAC: Role-Based Access Control (“editors”, “admins”)
- ABAC: Attribute-Based Access Control (permission based on user or resource attributes)
- Policy Enginers (like Cerbos): Define rules in policy code (often in YAML); can be powerful, but may require more wiring up
SpiceDB/Zanzibar-style:
Based on graphs of relationships—users, groups, objects, and composite permissions
Most modern “fine-grained auth” tools are inspired by Zanzibar*, Google’s system.*
Both SpiceDB and Authzed treat the original paper almost as gospel, providing the most “Zanzibar-like” open-source experience you can get outside Google.
Role Explosion? No Problem! User-Defined Roles
Suppose your admin dashboard says:
“Add a new role: [_________]”
That’s a user-defined role—not just “admin” or “editor,” but anything your business needs (say, “auditor,” “contributor,” or “regional manager”).
SpiceDB handles this with another level of indirection:
- You write relationships like
project:foo#role:auditor@user:alice - Define what that role can do in the schema
Example schema snippet:
Unknown block type \"code\", specify a component for it in the `components.types` option
Here’s what happens:
- Create a new role in the playground or via code
- Assign users to that role
- Grant permissions at both the direct and role levels
“You don’t need to hardcode every possible permission. Design for flexibility from Day 1.”
Automating Authz: The Rise of MCP and LLM-Driven Access
The future is not just writing schemas and APIs—it’s making Authz approachable even for people who don’t grok graphs.
Enter MCP (Model Context Protocol)
Originally from Anthropic, MCP is like the USB-C plug for AI → backend plugins:
- Exposes your live SpiceDB instance to LLMs or AI tools
- Now you can ask permission questions in English and get real answers against live data
For example:
“Who has access to project PiedPiper?”
Or,
“Can Jared resolve issue #17 in project FooBar?”
Here’s how you vibe code up an MCP server:
- Build the server with AI (see the transcript’s “vibe coding” story—AI cost <$3!)
- Attach to running SpiceDB instances (local or prod mirror)
- Plug MCP into Claude Desktop or other AI devtools
Key Use Cases:
- Answer “who has what permission” for compliance, audits, onboarding — no query-writing required!
- Generate test scenarios, write relationships, scaffold schemas using English
“AI is the new junior dev, answering complex Authz questions faster than you can grep logs.”
Compliance, Reporting, and “Who Has What?”
Auditors and managers don’t want to grep logs or learn Zanzibar. With MCP + SpiceDB, you can enable questions like:
“Find all users who can edit Project X”
“After deprecating this project, who needs to be notified?”
“Which users have ‘admin’ roles in this workspace?”
These are NOT simple lookups—your schema might have N levels of indirection, groups, nested roles, custom attribute blends. But with lookup APIs (and LLMs), you can get:
- Who can act (for a given permission)
- Why (role → group → resource chain)
- How was this permission granted? (visual tree)
Performance:
Even with millions of users, lookup queries can run in milliseconds, especially when using Materialize or batch APIs.
No-Dev Reporting:
Picture giving a compliance officer a prompt bar, not a query engine. “Show me all users with access to this doc.”
Scaffolding Test Scenarios—With Just English!
Ever spent more time setting up test data than building the feature?
With the MCP + SpiceDB stack, you can say:
“Set up a scenario where Jared has the Auditor role on Project PiedPiper.”
And it generates the right relationships:
Unknown block type \"code\", specify a component for it in the `components.types` option
Checks its work, verifies with permission checks, and you get instant feedback.
Caveats:
- Don’t run on prod without audit!
- LLM outputs are non-deterministic, but perfect for dev, test, and learning.
- Combine with your TDD flows for painless regressions.
Platform Showdown: Authzed vs WorkOS, Okta FGA, Cerbos
Authzed/SpiceDB
- Open, Zanzibar truthers — core model matches Google's approach
- Schema-first, composable, with console/playground
- Docs
- Great for wide use-cases, community-backed
WorkOS / Warrant / FGA
- Rebased on Warrant, now integrated into WorkOS’s identity platform
- Similar schema, but tightly coupled to WorkOS's broader SSO suite
- FGA: Fine Grained Authorization, OpenAPI, available as open source (see docs)
Okta FGA
- Enterprise-focused, integrates tightly with Okta ecosystem
- Okta’s FGA project
- Integrated policies with auth
Cerbos
- Policy-first, more ABAC
- YAML-based policy definitions
- Good for orgs who want to express rules programmatically (think: “senior engineer can approve up to $10,000”)
- See Cerbos
“While there’s no true portability or standard, if you model Authz as asking “does X have permission Y on Z?”, you can swap backends later if needed.”
How Granular Can You Get? Limits & Best Practices
You’re not limited to just “admin/editor/reader.”
You can model:
- Teams, orgs, time-bound roles
- “Guest” and “external” access
- Nested folders, inherited roles
- Sharing links
- Feature flags
- Fine-grained row-level security a la Supabase
BUT:
- More granularity = more data and more to manage
- Sometimes, the simplest approach suffices
"Sometimes people get lost in the weeds, over-engineering for use-cases they don’t have. Start simple—you can always evolve your schema."
Try it Yourself! Community, Open Source, and Next Steps
- Try the community edition of FusionAuth for auth, and open-source SpiceDB for Authz.
- Experiment with the playground (here)
- Model your real-world permission scenarios—Google Docs, Notion with comments, Trello-style boards, you name it.
- Don’t reinvent the wheel—plug, play, customize, and focus on building your app, not debugging access rules.
Hands-On Tutorials
Further Reading & Next-Level Learning
The CodingCat Takeaway
Building robust, scalable authorization isn’t just about plugging in a third-party tool—it’s about shifting your mindset:
- Define permissions as questions, not hardcoded booleans
- Model your reality, starting simple but scaling as needed
- Use open-source building blocks, invest your time in features your users love—not plumbing
Whether you run with SpiceDB, plug into Authzed, or explore the fusion of AI and access control, the path is the same: stop fighting with Authz, start shipping value.
Got questions?
Jump into the CodingCat Dev Community! Or hit us up on Discord for live coding sessions and schema reviews.
—
"You can have your cake and eat it too. Build the exact permissions model your product needs—without owning the gnarly complexity."
—Sam, Authzed/SpiceDB Guru
See you all next time for live coding and more dev snacks. Don’t miss our upcoming features on Authz with LLM workflows, NextJS RBAC hooks, and more—subscribe to CodingCat.dev and never miss a purr.
",
+ "url": "https://codingcat.dev/podcast/secure-ai-access-control-using-spicedb",
+ "title": "Secure AI Access Control Using: SpiceDB",
+ "summary": "Dive into hands-on SpiceDB demos, real-world permission modeling, and AI-powered scenario testing!",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/985c915f4445dce1e599ec70fea53b09689aefe2-960x540.png?rect=0,18,960,504&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2025-07-21T17:17:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "b9dfe3ba-bd69-4588-97fe-012b3a370f14",
+ "content_html": "Welcome back, fellow cats and coders! Whether you've landed here from the CodingCat.dev podcast or you're looking for the freshest snacks in dev land, you're in for a treat. Today, we're getting hands-on with modern authorization models, putting REBAC under the microscope, and spotlighting SpiceDB—the open source project that's changing how folks scale permissions for their apps. Plus, we’ll sprinkle in code, diagrams, and best practices from the field.
So, let’s paw at the mouse and dig into:
- Why traditional auth models hit limits as you scale
- What makes REBAC and SpiceDB so spicy (yep, Dune fans, we see you)
- The realities of plugging in centralized, flexible auth—from early MVPs to massive teams
- Real code, real stories, and real advice from devs in the trenches
Let’s get cozy and authentication-ready! 😺
Introduction: Auth is Different from Authz
Before we get too deep:
Authentication (Authn) is about confirming who you are.
Authorization (Authz) is about what you're allowed to do.
We keep hearing these terms thrown together, swapped around, and even merged in some products, but in reality, they're different worlds with different problems, tradeoffs, and risks. If you skip Authz or just tack it on, you’re basically throwing the doors open on your data. Yikes.
Why Should You Care?
- Growing apps means growing complexity
- Hacky “role” logic balloons fast (ever heard of role explosion?)
- Users expect Google Docs–level sharing, not Windows 95 levels of access
- Mistakes here cost trust, money, and security
If you’ve built an app and hit a wall with permissions, this post is for you. If you’re just getting started—awesome: you can skip some pain.
Meet the Experts: Sam, SpiceDB, and the CodingCat.dev Crew
Who’s Who?
I'm your trusty guide from CodingCat.dev, usually found herding cats and wrangling code. Today, I chatted with Sam—a Software Engineer at Authzed, creators of SpiceDB and all-around authority on fixing access management at scale.
Sam’s journey started hands-on, writing code and building the main open-source project. Now, he splits his time between direct engineering, community building, and showing teams a better way to do Authz. He's all about making complex topics approachable—just the way we like it.
“I've been at Authzed for over four years. Day to day, I'm still building, but recently I've shifted to helping more people get introduced to better access control.”
In Short: You’ll get a developer’s perspective with a heavy dose of practical advice and relatable “been there, fixed that” stories throughout.
The ABCs of Permission Models: RBAC, ABAC, PBAC, and REBAC Explained
There’s an alphabet soup of access control models. Each has its place, its strengths—and its pain points. Let’s break them down, developer to developer.
1. RBAC: Role-Based Access Control
This is the familiar one:
- Roles like
admin, editor, viewer - “If you’re an admin, do X. If you’re an editor, do Y.”
Good:
- Easy to grok and quick to implement
- Great for UIs and small teams
Bad:
- Doesn’t handle complexity (e.g. “View this doc if you’re on the X team and created it”)
- Roles multiply—fast (“role explosion”)
Example:
Unknown block type \"code\", specify a component for it in the `components.types` option
2. ABAC: Attribute-Based Access Control
Uses attributes, not just roles.
- Attributes = any data: department, region, time-of-day, etc.
- Rules can get more granular (“If user.department === 'finance' and resource.region === 'US'”)
Good:
- More flexible than RBAC
- Machines (not just humans) can use it
Bad:
- Policies can become unwieldy, hard to audit
- “Attribute explosion” is real
Common in AWS IAM!
Ever wrangled JSON policy statements? You’ve seen ABAC.
3. PBAC: Policy-Based Access Control
Policies define how access works. You still have attributes, but you wrap decision-making into reusable policies.
- Think: central policy engine ("Can user X perform Y on resource Z?")
Good:
- Intuitive if you like writing “policy as code”
- Used in Rails plugins and many frameworks
Bad:
- Policies can sprawl
- Tightly coupled to codebase unless you plan ahead
4. REBAC: Relationship-Based Access Control
The flexible, modern model. Everything is a relationship:
- Users
- Teams
- Resources
- Permissions
Think “graphs” of relationships (“Sam is in Team A; Team A can view Document B; therefore Sam can view Document B”).
"With REBAC, you can actually model the previous three access control approaches… It’s a very generic construct. You have objects, you have relationships between those objects, and you apply rules based on the connections."
Visual: Diagram showing user, team, resource, and their relationships—all leading to a permission decision.---
Enter REBAC: Relationships Rule Everything Around Permissions
Let’s say you want to represent:
- Alice is a member of
Engineering Engineering has access to Budget.xlsx
With REBAC, you create relationships:
Unknown block type \"code\", specify a component for it in the `components.types` option
Want to check if Alice can read Budget.xlsx? Just check if there’s a path through these relationships.
“If you can define the objects and relationships, you can almost model anything... [REBAC] generalizes to any permission scenario.”
Bonus:
REBAC lets you evolve. Model gets more complex? No problem—you add new relationships and types.
Visual: Flowchart of access via relationship path (user -> team -> resource).---
Scaling Authz: Lessons from Google Zanzibar
You ever hear about Google’s own permission system? No? Here’s the TL;DR:
What is Zanzibar?
- Internal system powering permissions for Google apps (Docs, Drive, Calendar, etc.)
- Centralizes authorization—one system, many models
- Handles a ridiculous amount of requests (think billions, fast, reliably)
The 2019 Zanzibar paper explains how they modeled relationships, scaled with consistency, solved “new enemy” (revocation) problems, and kept performance up.
“The reason Google built Zanzibar is they only wanted one system... for every application to use for unique permissions.”
Visual: Timeline of Google tech from research paper to open source—Kubernetes, Zanzibar, SpiceDB.---
SpiceDB: Open Source Auth for Modern Apps
If Zanzibar is the OG, SpiceDB is the open source flavor. Built on Zanzibar’s principles, it drops into any app stack.
What Is SpiceDB?
- An open source, “database for scalably storing and querying fine-grained authorization data”
- Not a generic graph DB, but a purpose-built permission engine
- Developer-friendly schema (less internal “secret sauce” needed)
- Handles arbitrary objects, subjects, and relationships
Visual: Side-by-side snippet of Zanzibar’s internal config vs. SpiceDB’s schema DSL.#### Example: Defining Permissions in SpiceDB
Zanzibar’s Way (hard to read)
Unknown block type \"code\", specify a component for it in the `components.types` option
SpiceDB’s Way (JavaScript-ish DSL)
Unknown block type \"code\", specify a component for it in the `components.types` option
How It Works In Practice
- Define a schema: what are your objects, relationships, and permissions?
- Write the relationships: who owns, who can read, who can edit, etc.?
- Query at runtime: “Can Alice do X to Y resource?”
“It’s kind of like a relational DB—SpiceDB has a schema for objects, relations, and permissions, and the data forms a graph.”
RBAC and JWTs Are Not Always Enough
Auth “Quick Wins” (And When They Hurt)
Most web apps start with JWTs (JSON Web Tokens) and RBAC:
- User logs in
- JWT encodes their role(s) or permissions
- App checks role when serving endpoints
Common pattern:
Unknown block type \"code\", specify a component for it in the `components.types` option
Sounds fine… until real life steps in.
Real-World Headaches
- Want per-report access? Now you need
read:report:1, read:report:2, etc.—hello, permissions explosion! - Teams and group sharing? JWT gets huge, and permission checks become spaghetti
- Someone’s role changes—now you have revocation lag (user still has their old JWT till it expires)
From the transcript:
“Multiply that by every user... now you have ‘n’ number of read reports. Obviously no one's gonna do that.”
It Gets Worse
- Add group-based permissions? You’ll have to change the JWT claims and the application code!
- Need to revoke instantly? JWTs are cached and can’t be revoked (that’s why you want short lifetimes).
Visual: Flow from login, JWT issued, user role changes, with lingering access due to long-lived JWT.---
Evolving Your Authorization Model as You Scale
Biggest question teams face: Should we start “the right way” from day one, or patch later?
Three Stages Most Teams Go Through
- MVP Mode: Quick and dirty; roles in the JWT, RBAC in the code.
- Growth Pains: Business logic and permissions intertwine—“custom authorization hell.”
- What Now? The company wants Google Docs–style sharing and needs to scale to new teams, regions, and resources… but the current system can’t keep up.
Quoting Our Guest:
“What if you didn’t have to take on so much tech debt? Some teams now just choose SpiceDB from day one—even if their permissions are easy, they know they’ll need to grow later.”
When Should You Move from RBAC/JWT to REBAC/SpiceDB?
- As soon as you want more than basic roles or per-resource checks
- When “who can see what” depends on relationships, teams, projects, or sharing
- If you want centralization, dynamic policy changes, or audits
Pro Tip:
Starting too simple means future rewrites. Investing in a flexible model early pays off.
How to Start Building with SpiceDB and Authzed
Getting Set Up
1. Model Your Permissions (Before Coding)
Use the Authzed Playground to:
- Define objects, relations, and permissions
- Test out real-world scenarios in your browser
- Tweak, evolve, and validate models without touching app code
Visual: Screenshot of the Authzed Playground interface, showing schema definition and relationship graph.#### 2. Integrate with Your App
Once you’ve got the model baked:
- In your app, write relationships as you create objects (e.g., “Sam owns Doc123”)
- Check permissions at key points (“Can Sam view Doc123?”)
- SpiceDB acts as the source of truth; your code just asks for a yes/no
Sample Schema
Unknown block type \"code\", specify a component for it in the `components.types` option
Adding Relationships
Assume we use the SpiceDB client library:
Unknown block type \"code\", specify a component for it in the `components.types` option
Checking Permissions
Unknown block type \"code\", specify a component for it in the `components.types` option
3. Build, Test, and Ship
- Build integration tests around permission checks
- Use the schema evolution tools (as your model grows)
- Rely on SpiceDB’s auditing, debugging, and instant propagation for safety
Visual: Diagram showing application, SpiceDB, and relationship-based permissions flow.---
Best Practices, Gotchas, and Next Steps
Tips from the Trenches
- Model Before You Code:
Use the playground, design outside your app—faster feedback, less churn. - Write Minimal App Logic:
Your app only writes relationships and does permission checks—NOT complex logic. - Separate Authn and Authz:
Authentication (JWT/providers) establishes identity; authorization is who can do what. Don’t muddle the two. - Revocations Are Hard (but SpiceDB Solves It):
The “new enemy problem” (e.g., fired users keeping access) is solved with SpiceDB’s linearized events—no more stale tokens. - Don’t Embed Permissions in JWTs (for dynamic checks):
It works for static roles, but not for resource-specific or frequently-changing access. - Start Simple but Scalable:
Even if your app is small, SpiceDB lets you start with RBAC and upgrade seamlessly when you need more.
Common Pitfalls
- Role Explosion:
Too many roles, too much code—switch to relationships sooner. - Confusing Authn and Authz:
Each has distinct responsibilities and risks. - Delaying Centralization:
Siloed permissions mean headaches; centralize early.
“You can authenticate someone, validate who they are, but that alone doesn't get you much. You need to do something with that identity—in other words, you need authorization.”
Visual: Venn diagram showing separation of Authn and Authz.---
Resources & Further Reading
Official Docs & Playground:
Technical Deep Dives:
Community & Open Source:
On CodingCat.dev:
Video Walkthroughs:
Wrapping Up: Build the App, Not the Boilerplate
Let’s cut to the chase: authorization is hard, but it doesn’t have to be a mess. From RBAC to REBAC, the field has matured—a ton. With tools like SpiceDB, you get future-proofing and a better dev experience.
“Let us give you friendly APIs so that you can focus on your stuff, and we'll handle the nuances and the complexity.”
Whether you're a one-person startup, an engineering lead at a growing team, or just curious about next-gen access control, you now have superpowers to solve these puzzles right.
To recap:
- Model outside your app, iterate, then plug in
- Avoid embedding fast-changing permissions in JWTs
- Let relationships—not roles—drive your logic when needed
- Centralize early, scale confidently
Celebrate your newfound permission prowess—and let’s keep building... with way less tech debt.
Got Questions? Want More Demos?
Check out the full video for hands-on demos and more cool stuff, or jump into the Authzed Playground and try your own scenarios.
Smash that follow, and drop us your questions at CodingCat.dev! Happy (authorized) coding!
Quick Reference: Sample SpiceDB Schema (Copy/Paste Ready)
Unknown block type \"code\", specify a component for it in the `components.types` option
Example: Permission Check in Node.js
Unknown block type \"code\", specify a component for it in the `components.types` option
If You Remember One Thing...
Start modeling your access before you need it. You’ll thank yourself later.
Catch y’all in the next episode—and keep those code snacks spicy!
Visual: CodingCat mascot giving a thumbs-up with a stack of code and a “permissions approved” stamp.
This post was created with insights from Sam at Authzed, the CodingCat.dev community, and real stories from the trenches. All opinions, code samples, and cats are our own.
",
+ "url": "https://codingcat.dev/podcast/scaling-permissions-with-spicedb-rebac-explained",
+ "title": "Stop Building Authz, Start Building Value: A Deep Dive into Authorization, REBAC, and SpiceDB",
+ "summary": "Dive into AuthZed, SpiceDB, and Permissions.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/2d5574e4f3ce25d07898a17bbeeb2bcfd7df8568-960x540.png?rect=0,18,960,504&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2025-07-21T16:55:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "4628e5fe-28d8-49c4-af37-15904dcf787a",
+ "content_html": "Auto Show: The Vision
What is Auto Show?
Autoshow is more than a tool—it's a revolution. Described as "automatic show notes for podcasts and videos," Anthony breaks down its functionality and potential impact:
"The idea was born out of a need for efficiency in my own podcast, FS Jam, where chapter titles and well-structured show notes were a dream, not a reality."
In our digitally-driven world, content creators constantly ask, "How can I make my workflow more efficient?" Auto Show offers one answer by automating the transcription and chapterization process. This tool is vital for anyone in devRel or content creation, significantly cutting down on manual labor.
Why Use Auto Show?
- Streamlined Workflow: Automatically generate chapter titles and timestamps.
- Ease of Repurposing Content: Simplifies turning long-form content into digestible bites.
- Advanced AI Integration: Leverages tools like OpenAI's Whisper for transcription accuracy and ChatGPT for intelligent outputs.
Here is a simple command-line interface (CLI) demo for processing a video:
Unknown block type \"code\", specify a component for it in the `components.types` option
This command showcases Auto Show's ability to handle YouTube videos by automating their processing.
AI and Auto Show: Diving into Technologies
Anthony shares the technological journey behind Auto Show, detailing the incorporation of AI to improve content creation. Here's a closer look at the tech stack:
Whisper AI
Whisper, developed by OpenAI, is a transcription tool integral to Auto Show's functionality. Anthony explains the importance of efficiency:
Unknown block type \"code\", specify a component for it in the `components.types` option
This command transcribes audio into text with impressive accuracy, forming the backbone of Auto Show’s transcript generation.
ChatGPT Integration
Utilizing ChatGPT, Auto Show crafts engaging and concise summaries of content. Here's a sample script snippet, illustrating its integration:
Unknown block type \"code\", specify a component for it in the `components.types` option
The tool's strength lies in its ability to provide contextually relevant summaries and titles, something invaluable for time-strapped content creators.
For further exploration of AI integration, check out our AI technologies post.
Building Auto Show: From CLI to Front End
Auto Show initially began as a command-line interface (CLI) project. However, as Anthony notes, the goal is to reach a wider audience by transforming it into an accessible, front-end application:
Unknown block type \"code\", specify a component for it in the `components.types` option
While the CLI version remains open source, Anthony's team is crafting a user-friendly interface for the paid version, catering to those who aren't as CLI-savvy.
Open Source Commitment
Despite commercial elements, the core logic behind Auto Show remains open-source, embodying Anthony’s belief in community-driven development.
Check out Auto Show on GitHub
Explore more open-source projects on our open-source page.
Future of Content Creation: AI in DevRel
As Anthony aptly states:
"If you are a dev rel, Auto Show will likely be very useful for you."
The role of AI in content creation isn't just transformative; it's becoming essential. As technologies like Whisper and language models advance, the capabilities of tools like Auto Show will only grow.
To learn more about AI's future in dev and content creation, explore our dev insights section.
Embeddings and Advanced Features
Delving deeper, Anthony touches on the potential of embeddings:
What are Embeddings?
Embeddings translate text into a mathematical space, enhancing search capabilities beyond a model's original training. Here's how they're utilized:
- Enhanced Search: Quickly pull relevant content from vast episode libraries.
- Content Access: Give users the ability to search and retrieve concise answers based on extensive archives.
Anthony's recipe for success with embeddings involves PostgreSQL and Prisma, leveraging their capabilities for robust storage and retrieval:
Unknown block type \"code\", specify a component for it in the `components.types` option
Conclusion: The Road Ahead for Auto Show
In conclusion, Auto Show represents a significant leap forward in the content creation landscape. As Anthony continues to refine and expand its capabilities, tools like Auto Show are poised to become indispensable to anyone invested in content creation and development relations.
Links
Jason's Blog on Buffalo sticks?
OpenAI Whisper
AutoShow Repo
",
+ "url": "https://codingcat.dev/podcast/automatic-show-notes-for-podcasts-and-videos",
+ "title": "Autoshow - Automatic Show Notes for Podcasts and Videos",
+ "summary": "Anthony is back, join us for a fun chat on coding, dev tools, and AI!",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/7ce952a3cc876ae1616a9b16bf979c195e93c2c7-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2025-03-05T21:26:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "8efab30e-08b7-408d-ac98-c51238069042",
+ "content_html": "Diving into AI: A Journey Through Tech with Raymond Camden
Welcome, Perfect Peeps, to the world of AI! As technology evolves, artificial intelligence might seem like an overwhelming realm to dive into, but by taking small, manageable steps, you can smoothly transition and grow with it. In today’s deep dive, we unravel the world of AI, explore how to get started using Gemini AI Studio, and look into the tech journey of developer evangelist Raymond Camden.
Getting Started with AI: Taking the First Step
AI has been a buzzword in the tech world, but often, it can feel intimidating for newcomers. The key to overcoming this apprehension is to start small. Platforms like AI Studio for Gemini offer a friendly environment for beginners to start their journey into AI. By iterating your knowledge and building on what you learn, AI can soon become a part of your everyday toolkit.
Meet Raymond Camden: From Star Wars to Coding
Raymond Camden, a household name for many in the development community, shares his fascinating journey into the world of technology. Growing up with a love for movies like Star Wars and Tron, Raymond found his inspiration early on. The mystical allure of films combined with the encouragement he received from family (including access to an Apple IIE) grew his curiosity into a career in tech.
"Star Wars taught me about stories, but Tron showed me the magic of tech."
After delving into BASIC programming and later Pascal, Raymond pursued computer science in college. His dreams oscillated between working in CGI for movies and diving into government work focusing on security. However, an epiphany regarding his strengths led him to pivot to a technical writing degree—perfect for combining his passion for tech and writing.
Overcoming Math Roadblocks
Not everyone in tech is a math wizard, and Raymond's story highlights a crucial point: it's okay to pivot. Facing challenges in higher-level math courses, he found his niche in technical writing where he could marry his passion for writing with technology. This pivot wasn't the end but a stepping stone in his lifelong tech journey.
The Evolution of Web Development
From hand-coding web pages in college in the early 90s to harnessing tools like Dreamweaver, Raymond witnessed firsthand the evolution of web development. His early experiences with web building laid the groundwork for his future endeavors as he merged his writing skills with the expanding internet landscape.
Unraveling a Career in Tech
Let's fast forward. Today, Raymond thrives in a landscape where tech tools have become more accessible, opening avenues for people from non-traditional backgrounds to enter the tech field.
The Joy of Enabling Others
Raymond highlights the importance and joy of using tools that allow non-technical individuals to navigate tech spaces. Platforms like Sanity IO and various CMS systems enable users to create beautiful, functional websites without diving deep into hardcore coding, showing the democratisation of technology.
AI for the Cautious Coder: Easing into Artificial Intelligence
Stepping into AI doesn't mean you need an extensive background. With resources like Google's Gemini API, even those at a beginner level can start crafting functional AI applications. The Gemini API stands out by offering free resources, making it an excellent entry point for those curious about AI without financial commitment concerns.
Understanding Multimodal AI
One of Gemini's standout features is its multimodal capability, which allows for the analysis of diverse data types—text, images, and audio—offering versatile applications. This capability extends Gemini's flexibility, and as Raymond explains, enables extraction of unique insights from complex data sets.
"My understanding of the multimodal nature is it's like having a universal translator for binary data—text, pictures, audio, and more."
Boosting Your Development: Tools and Strategies
For those who feel daunted by AI, adopting an assistive approach helps ease the transition. AI can be a powerful ally in content creation, such as writing, coding, and even assisting with complex decisions. The key is in utilizing AI as a tool to complement human creativity rather than replace it.
Aiding Code Creation
For those new to coding or transitioning to new languages, tools like Visual Studio Code Copilot and Zencoders offer AI assistance in generating, reviewing, and improving code. They take on the role of a virtual coding buddy, suggesting and completing lines of code based on context.
Unknown block type \"code\", specify a component for it in the `components.types` option
Raymond shares how these AI tools transformed his view from skepticism to recognition of their immense value.
Exploring AI Studio and Prompt Crafting
A common challenge in AI development is crafting effective prompts—the input given to an AI model to elicit a specific type of response. Raymond emphasizes the importance of fine-tuning prompts for precision and effectiveness, even more than the AI code itself.
AI Studio provides a sandbox environment to test and refine prompts before integrating them into applications. This step ensures the AI's responses align with user expectations and enhances functionality.
Closing Thoughts: The Future of AI and Creativity
Raymond brings us full circle by reflecting on the potential growth of AI tools and their place in the future of technology. With AI, individuals from all backgrounds can enhance creativity, streamline workflows, and delve into new avenues of exploration.
"As tech advances, so does our capacity to innovate, and AI is just the beginning."
Conclusion
This journey through AI with Raymond Camden shows that the intersection of creativity and technology is accessible to everyone. Whether you're a seasoned developer or just getting started, take each step at your own pace and allow tools like AI to enhance your journey.
For more on coding and innovation, be sure to explore CodingCat.dev for a collection of resources, podcasts, and insights tailored to tech enthusiasts like you.
This blog post captures a slice of the vast world of AI and technology, inspired by the transcript from the rich and engaging conversation with Raymond Camden. Explore, create, and innovate—your tech journey awaits!
",
+ "url": "https://codingcat.dev/podcast/ai-for-peeps-scared-of-ai",
+ "title": "AI for Peeps Scared of AI",
+ "summary": "Explore Ray Camden's tech journey & dive into AI for beginners. Discover the latest in AI, from coding basics to powerful tools like Google's Gemini.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/a535d004c847fc77dc1c8771c7c482ce65278234-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2025-02-26T05:01:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "59b0c713-79dc-45cc-b7ef-95479932611b",
+ "content_html": "AI-Powered Persuasion Engineering: Insights and Innovations with Ben Bressington
Welcome back to CodingCat.dev, where we're all about delivering the freshest bites of development wisdom. Brought to you by Cloudinary, enabling faster builds with AI-powered image and video APIs, and Trygolia, powering discovery with enhanced search capabilities. In today's post, we're diving deep into the world of AI-powered persuasion engineering. Let's explore the fascinating insights shared by our guest, Ben Bressington, as we navigate the intricate intersection of AI, technology, and human interaction.
The Road Less Traveled: Ben's Mobile Life Journey
"I decided two years ago to pack everything up, sell everything, and hit the road. I'm in an RV, on the side of a lake in Arkansas, enjoying life and running the business from wherever."
Ben Bressington's story is as much about personal transformation as it is about technological innovation. Living life on the move in an RV with only a Starlink or hotspot connection presents its own set of challenges and opportunities. This lifestyle not only allows for greater personal freedom but also introduces unique hurdles, from connectivity issues to the need for creative problem-solving.
This nomadic existence has also empowered Ben to leverage AI and technology to create connections across geographical boundaries, paralleling how AI enables two-way communication in digital spaces irrespective of physical location.
AI and Human Connection: A Double-Edged Sword
Living the road life isn't all Instagram filters and picture-perfect scenes. Ben addresses the less glamorous facets, such as the challenges of maintaining genuine connections in a highly digital world. As more of our interactions are mediated by screens, AI steps in both as a solution and a problem. It's offering tools for communication but simultaneously making natural conversations rare.
Here's a powerful thought from Ben:
"Everything will be alright in the end, and if it's not alright, it's not the end."
AI's Role in Bridging Communication Gaps
When AI acts as a facilitator of human interactions, such as in maintaining family bonds over long distances, it illustrates the revolutionary potential of tech to transform traditional communication.
- Virtual Family Time: Imagine a world where holographic glasses enable you to play chess with a parent who's miles away. AI doesn't just serve as a bridge but creates entirely new platforms for relationships.
Unknown block type \"code\", specify a component for it in the `components.types` option
The question remains: can AI enable us to go beyond surface-level conversations to truly understand each other across digital spaces?
The Evolution of Avatars and Personal AI Assistants
As AI technology progresses, it's fostering innovations like virtual personal assistants and avatars that can mimic human interactions.
Avatars: Beyond the Screen
Creating realistic avatars that replicate not just voice but mannerisms is becoming more feasible. These AI tools can analyze and adapt to user interactions, predicting needs and crafting personalized experiences.
"We have AI avatars helping us communicate effectively by understanding not just the words being used but their context and subtext."
- Emotion AI: Employs sophisticated machine learning algorithms to detect emotional nuances in text and speech.
Unknown block type \"code\", specify a component for it in the `components.types` option
With virtual avatars, the promise of interacting with a digital person becomes a reality, offering further personalization in customer service and communications.
The Changing Landscape of Development Roles
AI isn't just redesigning how we communicate—it's reshaping the job market, especially in tech and development roles. Developers must become proficient in integrating modern AI systems like RPA (Robotic Process Automation) and NLP models.
Automation and Integration: Key Skills for Developers
Ben highlights the unique opportunity for developers to enhance their skill sets through learning system integration and API management. Developing fluency in tools like n8n or Make.com could be the edge needed to stay relevant in a fast-evolving workspace.
- Example Use Case: Automating repetitive data entry tasks with
n8n:
Unknown block type \"code\", specify a component for it in the `components.types` option
Rethinking AI: From Threat to Tool
With roles evolving, so too must our relationship with AI—from fearing job replacement to seeing AI as a tool. As Ben suggests, it's about integration innovation:
"Developers now need to show how they're increasing ROI by integrating smart APIs and reducing friction in systems."
AI in Real-World Scenarios
From concept to practice, AI touches more facets of daily life than we may realize. The sophistication of AI applications in seemingly mundane areas like grocery delivery and security underscores its transformative potential.
Drones and Automated Deliveries
Ben shares his experience with drone-based delivery services in Arkansas, illustrating how geographical mapping and weather analysis integrate AI for efficiency.
- Path Optimization: Similar to optimizing code, drone paths are refined using AI to account for myriad variables, ensuring timely and efficient deliveries.
Security and Surveillance
Using AI for surveillance isn't new, but its sophistication is turning full-circle, with entire security teams replacing physical guards with AI systems analyzing video feeds.
Unknown block type \"code\", specify a component for it in the `components.types` option
Ken highlights:
"The first job that AI replaced was actually a security guard, through advanced video and monitoring technologies."
Final Thoughts: The Future of AI and Human Collaboration
As we stand on the edge of AI's potential, it's not the machines we should fear but our ability to adapt and integrate with them. The key takeaway from Ben's insights suggests a shift from passive participant to active innovator.
Embracing Change
To thrive in this evolving landscape, developers and non-developers alike must be open to continuous learning and adaptation. The value lies not just in what AI can do autonomously but in how we can guide it to enrich human experiences.
Call to Action
Let's continue this journey together at CodingCat.dev, tapping into the collective wisdom and exploring further into AI's expansive frontier.
Feel free to share your thoughts and comment below about how you're integrating AI into your work or life—let's build a future where human and machine collaboration is as seamless as possible.
Stay tuned for our next exploration into the constantly changing world of AI.
Unknown block type \"code\", specify a component for it in the `components.types` option
",
+ "url": "https://codingcat.dev/podcast/ai-powered-persuasion-engineering-with-ben-bressington",
+ "title": "AI-Powered Persuasion Engineering with Ben Bressington",
+ "summary": "Join Ben Bressington for insights into AI-powered persuasion, nomadic lifestyle tips, and how tech reshapes industries.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/849d6e5132ab7428a6aaef0e28b315d717389976-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2025-02-18T19:58:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "668e776c-0102-416a-9f83-78c024dae636",
+ "content_html": "Welcome fellow coders and indie hackers! Dive into an insightful exploration of SupaStarter and indie hacking with Jonathan Wilke. Grab a snack and buckle up for the freshest dev insights you won't want to miss.
Want to skip to the Discount code for supastarter: CODINGCATDEV
Introduction: A Glimpse into the Journey
Ever found yourself wondering about the indie hacking scene, or maybe pondering the best way to kickstart your next big SaaS project? You're in the right place. Jonathan Wilke, who many of you might know as an internet sensation for his hacking journey, joins us to share his invaluable experience. Jonathan's story began two and a half years ago when he ventured into building a SaaS starter kit that later paved the path for his first online product sale.
The Allure of Indie Hacking
What is Indie Hacking?
In the realm of tech and business, indie hacking is about starting a product from scratch and growing it independently without relying on external funding. Jonathan shares,
"The beauty of indie hacking is the freedom it brings. You can grow at your own pace without the pressure of external investment."
For many, including Jonathan, indie hacking was driven by the lack of initial capital but also the desire to maintain full creative and operational control.
Why Choose Indie Hacking?
Indie hacking isn't just about the product; it's about building something meaningful while balancing it with life’s demands. Many indie hackers, like Jonathan, start with side projects, allowing them to develop their ideas without the safety net of a full-time job. The journey is both thrilling and daunting but ultimately rewarding.
From Student to Developer: Jonathan's Story
Jonathan's path into development began at the age of 15. A natural progression led him through university, where he studied computational visualistics—a blend of computer science and graphic design—a perfect foundation for web development. As he narrated his tale of moving from a stable job to freelancing, it's clear that passion and drive fueled his transition into full-time indie hacking.
The Genesis of SupaStarter
Building a Solution
SupaStarter wasn't Jonathan's first project but became his first publicly available product that he sold online. It emerged out of necessity—a solution for freelance clients who required repetitive functionalities.
"I realized I was coding the same things repeatedly for different clients. SupaStarter was born from the idea of a reusable template."
Features of SupaStarter
With SupaStarter, you're not just purchasing a template; you're investing in a robust foundation for your SaaS project. Here are some standout features:
- Frontend and Backend Integration: From marketing pages to a complete SaaS interface, it covers all bases.
- Flexible Authentication: Offers a multitude of options including magic links and two-factor authentication.
- Payment Integration: Supports platforms like Stripe and Lemon Squeezy, with both one-time and subscription billing.
- Community and Support: Exclusive Discord server for guidance and a comprehensive documentation repository.
Deployment and Scalability
Launching a SaaS project can be daunting, but SupaStarter simplifies the process. Jonathan emphasizes the importance of laying down a scalable and production-ready base.
To illustrate, here’s how you might start setting up a new SupaStarter project, after you purchase it:
Unknown block type \"code\", specify a component for it in the `components.types` option
These initial steps get you moving swiftly, allowing more focus on customizing the SaaS to your unique needs.
Navigating the World of SaaS with SupaStarter
Deploying a SaaS with SupaStarter
Jonathan highlights the ease of launching a SaaS using SupaStarter, which is designed for both newcomers and seasoned developers to swiftly move from concept to deployment. His guiding principle is clear:
"Ensure you build on a scalable and production-ready foundation."
Scalability and Control
A significant aspect of SupaStarter is the adaptability and control it offers over all parts of the application. Jonathan ensures that full flexibility is maintained, whether it's switching between payment providers or integrating new features.
Expanding with SupaStarter: Insights from the Documentation
According to the SupaStarter documentation on Next.js, there are numerous functionalities and integrations available. Here are a few highlighted features:
- Dynamic Routing with Next.js: Leverage dynamic and nested routes with ease.
- GraphQL APIs: Implement efficient GraphQL APIs to enhance data handling capabilities.
- Server-Side Rendering (SSR): Utilize SSR for improving SEO and user experience by serving pre-rendered HTML.
The Power of AI in Development
Balancing Magic and Mastery
AI tools offer immense potential in accelerating development, but Jonathan points out a caveat:
"AI accelerates productivity but can disconnect developers from understanding their code."
It’s crucial for developers to balance AI assistance with a solid foundation in fundamental coding skills to ensure they're ready to troubleshoot and innovate.
Integrating AI with SupaStarter
SupaStarter exemplifies this balance by incorporating programmable media via Cloudinary and search functionalities through Algolia, thereby enhancing the user experience without compromising developer control.
Building with Confidence: Community and Support
One of the unique aspects of SupaStarter is the vibrant community and comprehensive support offered. Jonathan mentions that every customer gets access to exclusive resources, an aspect that significantly reduces the time to market for their projects.
Conclusion: Embracing the Indie Hacker's Spirit
Jonathan's journey encapsulates the essence of indie hacking—resilience, creativity, and freedom. SupaStarter is more than a product; it’s a testament to the power of independent entrepreneurship. As we wrap up, remember that every great product starts with a spark, much like Jonathan’s first sale after three weeks.
",
+ "url": "https://codingcat.dev/podcast/supastarter-and-indie-hacking-with-jonathan-wilke",
+ "title": "supastarter and Indie Hacking with Jonathan Wilke",
+ "summary": "Explore indie hacking, SaaS growth strategies, and innovative developer tools with Jonathan Wilke.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/6fab5422bfbf71ce8e85063b6c07f331d68c4fa3-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2025-01-30T02:01:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "6c788031-cace-4c70-bcf5-66d0dcda085f",
+ "content_html": "In today's web development landscape, efficient media management is crucial for enhancing user experience and performance. This blog post dives into the use of Cloudinary, a comprehensive media management platform, specifically focusing on its integration with Next.js via the Next Cloudinary library. Join us as we unravel the intricacies of image optimization, transformation, and delivery using Cloudinary's powerful capabilities.
Meet the Experts: Alex Patterson and Colby Fayc
Kicking off our session, we’re joined by Colby Fayc, the Director of Developer Experience Engineering at Cloudinary, and our host, Alex Patterson from Coding Cats. Colby is not only prominent in the developer community but also an avid content creator. You can check out his work on spacejelly.dev and his YouTube channel at Colbeayo.
"I've been a big fan of Next.js and Cloudinary, and it's something I really wanted to see come together." - Colby Fayc
Why Next Cloudinary?
Cloudinary offers two main components: a Digital Asset Management (DAM) service and a programmable media delivery section, which includes powerful transformation and optimization tools. These features are akin to a Photoshop API, allowing for dynamic cropping and resizing directly through code. However, there was no direct Next.js integration, prompting the creation of Next Cloudinary to fill that gap.
The Magic of Component-Based Development
One of the standout features of Cloudinary's integration with Next.js is its component-based approach, which aligns perfectly with the React ecosystem. This approach allows developers to seamlessly integrate complex image optimization processes into their applications with ease.
Unknown block type \"code\", specify a component for it in the `components.types` option
Above is a simple example that replaces Next.js's Image component while still enabling Cloudinary's robust transformations and optimizations through a Cloudinary URL.
Setting Up Your Environment
To get started with Next Cloudinary in a Next.js project, you’ll need to set up your environment variables and install the necessary packages. Make sure to define your CLOUDINARY_CLOUD_NAME in a .env.local file:
Unknown block type \"code\", specify a component for it in the `components.types` option
Install the package:
Unknown block type \"code\", specify a component for it in the `components.types` option
Building a Photo Gallery with Cloudinary and Next.js
Head over to ShadCN UI to scaffold a new Next.js application. This utility simplifies setting up your Next.js environment, allowing you to jump straight into building.
Unknown block type \"code\", specify a component for it in the `components.types` option
Navigate into your project directory and start building your components. We utilized ShadCN UI to quickly bootstrap our application, especially since it offers effortless integration with various component libraries.
Implementing Image Components
Once you have set up your Next.js app, use Cloudinary's CLDImage component for your image management:
Unknown block type \"code\", specify a component for it in the `components.types` option
Transformations and Optimizations
Cloudinary's unique selling point is its advanced transformation capabilities that don't just optimize, but also transform your images with operations like scaling, cropping, and even adding overlays or underlays.
Resizing and Cropping
Dynamic resizing and cropping are among the most crucial functions for maintaining performance across different devices. Use the sizes attribute to ensure images are responsive and only download the necessary pixels for a given device size.
Unknown block type \"code\", specify a component for it in the `components.types` option
This attribute ensures that your images are optimally resized for the container they are in, improving loading times on slower networks.
Background Replacement and Overlays
One of the more fascinating features is the ability to dynamically remove and replace backgrounds using AI-based transformations.
Unknown block type \"code\", specify a component for it in the `components.types` option
Beyond removing backgrounds, you can also overlay images or text, great for customizing thumbnails or personalizing user experiences.
Performance Analysis with Lighthouse
Once you've set up your image components and transformations, it's crucial to analyze your application's performance. Using tools like Google's Lighthouse can give insight into how well your images and overall media management are optimized. Typically, Cloudinary's on-the-fly transformations can significantly reduce perceived load times and resource usage.
Advanced Use Cases
Beyond basic transformations, Cloudinary allows for a wide range of advanced use cases, such as:
- Media Flows: Automate and streamline the process of managing your media assets.
- Video Management: Integrate Cloudinary's video features for more seamless video playback and analysis.
- Upload Widgets: Enhance user experience by allowing easy and customizable media uploads.
Integrations with CMS
If you're using a CMS like Sanity or Contentful, Cloudinary can integrate directly, offering powerful media management capabilities right from your content management interface. This integration ensures that your workflows remain smooth and efficient.
Conclusion
Cloudinary offers a powerful suite of tools that can dramatically enhance the way you handle media in your web applications. When combined with Next.js via the Next Cloudinary package, it empowers developers to create highly-optimized, media-rich applications with minimal overhead.
Whether you're a seasoned developer or just getting started, integrating Cloudinary with your Next.js projects will undoubtedly elevate your application's performance and user experience. For a hands-on experience and further exploration, check out the Next.js Cloudinary repository and dive into the world of image and video optimization.
Happy Coding!
",
+ "url": "https://codingcat.dev/podcast/stop-killing-your-largest-contentful-paint-score-with-images",
+ "title": "Stop Killing your Largest Contentful Paint score with images!",
+ "summary": "Join us as we explore Cloudinary's new components for Next.js! Learn how to optimize images, integrate Cloudinary, and boost performance.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/75dc7459b152687483012f9c2c34cd87ba39c3d1-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2025-01-24T12:35:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "c091d38b-4812-4922-a6f9-2e8ea7aa1dd1",
+ "content_html": "Welcome back, coding enthusiasts! If you've ever had Next.js up and running and found yourself in need of better monitoring on both the front and back end, you've landed on the right page. Today, we'll dive deep into adding Sentry to Next.js with insights from Luca, who works on the JavaScript SDKs at Sentry, focusing especially on the Next.js SDK.
Let's roll up our sleeves and get started!
Introduction to Sentry for Next.js
Sentry is a powerful tool for error tracking and performance monitoring. If you rely on Next.js for your web applications, integrating Sentry can provide detailed insights into both client-side and server-side issues.
What You'll Learn
Step 1: Setting Up Sentry
To get started with Sentry in your Next.js project, you'll need to install the Sentry SDK. Let's walk through the process.
Installation
First, initialize your project and use the command line wizard:
Unknown block type \"code\", specify a component for it in the `components.types` option
Now, install the Sentry package:
Unknown block type \"code\", specify a component for it in the `components.types` option
This installs the Sentry SDK and sets the stage for the configuration.
Initial Configuration
With Sentry installed, you'll need to configure it. Sentry will prompt you with several options to customize its functionality according to your needs.
### Handling Ad Blockers
One interesting challenge comes from Sentry occasionally being listed by ad blockers. Luca mentions that although it's a hurdle, their team is transparent about it, promising nothing shady occurs. Enabling the option to circumvent ad blockers ensures that you'll capture all necessary events.
Unknown block type \"code\", specify a component for it in the `components.types` option
Step 2: Configuring the SDK
Next, we'll delve into configuring the Sentry SDK. The wizard generates three primary configuration files for different runtime environments: client, server, and edge.
Client
Unknown block type \"code\", specify a component for it in the `components.types` option
Server
Unknown block type \"code\", specify a component for it in the `components.types` option
Edge
Unknown block type \"code\", specify a component for it in the `components.types` option
Instrumentation
Instrumentation is key, especially for monitoring applications. The wizard provides an instrumentation.ts file, a feature to register and monitor server behavior.
Unknown block type \"code\", specify a component for it in the `components.types` option
Navigating Sentry in Development
Let's take a spin through the Sentry interface, which offers a wealth of information on how your Next.js app is performing.
Performance Monitoring
Performance is critical. Sentry monitors API requests, rendering times, and more, providing a trace view that details loading time, resource fetching, and more.
For high-traffic sites receiving thousands of views a day, consider these best practices to ensure optimal performance.
Filtering Noise
Third-party code might throw errors that clutter your dashboard. Sentry allows you to filter these out.
Unknown block type \"code\", specify a component for it in the `components.types` option
Improving Replay Rates
Replays can quickly eat into your quota. Optimizing replay settings ensures you're gathering quality data without unnecessary noise.
Unknown block type \"code\", specify a component for it in the `components.types` option
Introduction
In this episode of Code with CodingCat, we delve into integrating Sentry with a Next.js application to monitor its performance and errors effectively. Joined by Luca, a developer who works on the Sentry JavaScript SDKs, we embark on a step-by-step exploration of setting up Sentry, configuring its features, and optimizing the monitoring process.
This session covers everything from the initial setup to advanced deployment strategies, offering valuable insights into error handling and performance tracking. Whether you're a novice to Sentry or looking to fine-tune your monitoring setup, this episode is packed with practical advice and expert tips.
Guest Information
Luca
Luca is a developer at Sentry, focusing on maintaining the Next.js SDK. He brings his wealth of experience to the episode, helping to unlock the full potential of Sentry's features for monitoring JavaScript applications.
Related Resources and Links
Episode Summary
Join us for an exciting exploration of integrating Sentry with a Next.js application. In this episode, Luca guides us through the setup process, starting with the initial installation of the Sentry SDK. He details the various configuration options available and explains how to tailor the setup to suit different needs, such as circumventing ad blockers, managing source maps, and adjusting log levels.
We also dive deep into performance monitoring and troubleshooting, with Luca providing expert insights into how Sentry tracks errors across front-end and back-end processes. This episode covers essential features like session replay, breadcrumbs, and the ability to filter out noise from third-party libraries.
Luca also discusses some advanced deployment strategies on platforms like Vercel and highlights the importance of optimizing for performance with real-time monitoring.
By the end of this episode, you'll be equipped with the knowledge to seamlessly integrate Sentry into your Next.js projects and harness its full capabilities to monitor, track, and optimize application performance effectively.
Key Takeaways
- Seamless integration of Sentry with Next.js for error tracking and performance monitoring.
- Detailed explanation of Sentry's configuration options to tailor the setup.
- Insights into troubleshooting common issues with Sentry and Next.js.
- Advanced deployment strategies and optimizations for real-time monitoring.
- The importance of filtering out noisy third-party errors for cleaner data.
Notable Quotes and Examples
"We can upload source maps to your project in Sentry, like associated with your application." — [Timestamp: 15:30]
"Everything that happens is instrumented. This feature is actually experimental, but the folks over at Vercel have been very helpful." — [Timestamp: 10:20]
"Session replay records your DOM and all the changes, and it's streamed to Sentry servers to stitch it back together." — [Timestamp: 45:14]
Detailed Episode Breakdown
00:00 - Introduction
Luca introduces Sentry and its capabilities with Next.js, setting the stage for a hands-on demonstration of the integration process.
01:57 - Installation Guide
Luca walks us through the installation of the Sentry SDK, emphasizing the importance of using the command line wizard to connect your account and select your project.
Unknown block type \"code\", specify a component for it in the `components.types` option
03:39 - Configuration Details
Luca elaborates on various SDK features, including circumventions for ad blockers, edge, and server setups. He explains how to wrap your Next config and discusses the significance of source maps.
06:08 - Performance Monitoring
The episode delves into the performance monitoring features offered by Sentry, with a focus on capturing performance measurements and understanding the user's journey via session replay.
"When I first saw this feature, my mind was blown because it was so crazy." — [Timestamp: 8:40]
14:16 - Troubleshooting Tips
Luca shares common troubleshooting tips, highlighting potential pitfalls with source maps and runtime tagging. He discusses ways to minimize errors from third-party libraries.
24:12 - Advanced Features
An exploration of advanced features such as Sentry's integration with Cron jobs on platforms like Vercel. Luca explains how to configure tracing and debugging options for maximum efficiency.
40:07 - Deployment Insights
The episode explains deployment strategies, particularly focusing on the seamless integration with Vercel’s infrastructure for continuous integration and deployment.
55:02 - Performance Optimization
A discussion on optimizing performance monitoring by leveraging web vitals and understanding long load times, offering valuable insights into maintaining high-performance standards.
Social Media Highlights
- "Discover how to integrate Sentry with your Next.js projects and enhance your monitoring abilities with Luca!"
- "Diving into Sentry's session replay feature was a revelation—seeing the user journey has never been easier!"
- "Learn how to optimize error tracking by filtering out noisy third-party errors with Sentry. Game changer!"
- "Deploying on Vercel with Sentry integration? Tune in for expert tips on streamlining your CI/CD pipeline!"
- "Understanding web vitals in real-time with Sentry's performance monitoring tool could save your next project!"
",
+ "url": "https://codingcat.dev/podcast/how-to-add-sentry-to-next-js-the-right-way",
+ "title": "How to add Sentry to Next.js the right way!",
+ "summary": "Discover how to set up Sentry with Next.js for seamless error monitoring and performance tracking.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/c8bdc4d88604cef005fd18d07d3867597cb4f2f5-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-10-16T19:12:28.380Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "94893625-c4bc-4adc-b59b-391fa13f2b5e",
+ "content_html": "Breaking Down ADHD
ADHD has various manifestations. Chris elaborates on the subtypes—hyperactive, inattentive, and combined. Each comes with its unique challenges.
- Hyperactive/Impulsive: Classic ADHD with lots of energy and impulsive behavior.
- Inattentive: More scattered, with trouble focusing but without the hyperactivity.
- Combined: A blend of both.
ADHD in the Real World: Coding and Focus
Developers with ADHD often find themselves in intense "hyperfocus" states, a double-edged sword that can lead to incredible productivity or overwhelming burnout.
Tips for Managing Focus
- Set Small Goals: Break tasks into smaller pieces to overcome that daunting inertia.
- Use Tools: An Apple Watch or similar device for reminders can be a game-changer.
- Embrace Imperfection: Sometimes, doing something "poorly" is better than not at all.
The Comorbidity Conundrum
ADHD doesn't exist in isolation. Many experience comorbidities like depression or anxiety, influencing how ADHD manifests. Alex shares his struggles with depression, emphasizing the importance of diagnosis and understanding.
- ADHD and Depression: The link often lies in undiagnosed ADHD causing feelings of inadequacy or overwhelm.
- Anxiety and ADHD: A common pairing that compounds focus issues.
Navigating Relationships and Work
ADHD can strain relationships, especially in high-pressure environments like coding. Learning to communicate needs and finding a supportive work culture is crucial.
Environment Matters
- Remote Work: Offers flexibility but demands self-structure.
- Office Work: Provides routine but can feel restrictive for ADHD minds.
ADHD's Superpower: Hyperfocus
While ADHD presents challenges, it also offers unique strengths. Hyperfocus allows developers to dive deep into projects, sometimes at the expense of everything else.
"When you're in hyperfocus, it's like 10x flow state. Nothing can rip you out of it." — Alex
Coping Strategies
- Manage Meetings: The disruption of focus can feel physically painful.
- Scheduled Breaks: Prevent burnout by setting dedicated downtime.
Medication: A Personal Choice
Chris and Alex discuss the nuanced decision to use medication. While not for everyone, it can be beneficial in managing ADHD symptoms.
- Stimulants: Like Concerta, offer focus but come with considerations like heart rate.
- Alternatives: Non-stimulant options are available for those sensitive to traditional meds.
ADHD in the Tech Community
Wess joins the conversation, sharing his experiences as a developer with both ADHD and autism. His insights shed light on how these conditions intersect.
Learning and Growing
Wess emphasizes the importance of continuous learning and adapting, using ADHD as a catalyst for exploration rather than a barrier.
## Building a Better Work Environment
Both Chris and Wess highlight the need for workplaces to understand ADHD. Here's how you can advocate for yourself or support coworkers:
- Open Communication: Disclose ADHD status and needs early.
- Flexible Scheduling: Find a routine that aligns with ADHD's ebb and flow.
- Task Management: Use checklists and detailed to-dos to maintain momentum.
Further Reading and Tools
By understanding ADHD's nuances and leveraging its strengths, we can create inclusive, supportive environments for all. Embrace your ADHD superpowers—your unique perspective is invaluable in coding.
"You're different, not worse. Find what works for you, and let yourself shine." — Chris
Related Resources and Links
Episode Summary
This episode navigates the world of ADHD, with a focus on developers dealing with attention struggles amidst coding demands. Chris shares the personal revelations and coping mechanisms he’s discovered on his ADHD journey.
From defining ADHD and its implications on everyday tasks to strategies for maintaining productivity, the discussion aims to shed light on the reality of working with ADHD. The episode also delves into the emotional and psychological aspects, exploring themes of identity and self-worth, making it a must-listen for those seeking connection and understanding in similar experiences.
Understanding the nuances of ADHD can empower those who have it, and this episode offers engaging perspectives and solutions to leverage ADHD as a potential asset rather than a limitation.
Key Takeaways
- ADHD can be a superpower, especially in creative fields.
- Proper diagnosis and understanding of ADHD are crucial.
- ADHD brains process information differently, which can lead to unique problem-solving skills.
- Hyperfocus in ADHD can result in incredible productivity when channeled correctly.
- Disclosing ADHD to employers can lead to a better work environment.
Notable Quotes and Examples
"I always knew I had ADHD. It's something I learned to work with, not against." (05:08)
"In the coding world, hyperfocus is like ten times the flow state." (20:28)
"You're not worse, just different. Embrace what makes you unique." (56:10)
"Finding a job culture that aligns with your ADHD can be game-changing." (29:07)
Detailed Episode Breakdown
00:00 Introduction
The hosts set the stage by discussing their focus on ADHD in the tech field. The lively conversation reflects real-life experiences and current events.
### 00:18 ADHD and Coding
Chris dives into his personal ADHD story, sharing how early experiences shaped his approach to learning and coding.
05:08 Understanding ADHD
Chris explains ADHD types and traits, emphasizing the significance of understanding them for better productivity and self-awareness.
13:03 Diagnosing ADHD
The episode explains the process of diagnosing ADHD and highlights differences in accessibility across various countries.
20:28 ADHD Hyperfocus
The discussion turns to hyperfocus, a common ADHD trait, and how it can be both a challenge and a boon in coding tasks.
### 29:07 Strategies for Success
Practical advice and anecdotes are shared around working effectively with ADHD, both in remote and office environments.
37:45 Mental Health Insights
Chris and guest Wess share mental health struggles and triumphs, offering insights into balancing emotional and professional life.
56:10 Different, Not Worse
The conversation wraps up with an empowering message: embrace ADHD as a strength, not a setback, in the personal and professional realms.
Social Media Highlights
- "ADHD in coding is not a barrier but an untapped potential. Discover your neurodiverse strengths today!"
- "Hyperfocus: When ADHD turns a good coding sprint into a creative masterpiece."
- "Finding an ADHD-friendly work culture is crucial. Don’t settle until you find your tribe."
- "Embrace ADHD as a superpower, and watch your productivity soar."
- "ADHD: Not a flaw, just a unique way of seeing the world—and that's okay."
Conclusion: Embrace Your Superpowers
ADHD is different, not worse. It's a unique way of seeing the world, bringing both challenges and incredible strengths. By understanding and harnessing ADHD, developers can turn what seems like a hurdle into a superpower.
",
+ "url": "https://codingcat.dev/podcast/unlocking-your-adhd-superpowers",
+ "title": "Unlocking your ADHD Superpowers",
+ "summary": "Discover the unique strengths of ADHD in coding! Join us as we explore superpowers, challenges, and tips for thriving with ADHD. ",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/596981689ce59386426df906fdb94d110fc66db2-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-10-10T15:28:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "c49ed931-7b57-4d27-a659-026d0c5c0e93",
+ "content_html": "Introduction to OpenSauced
CodingCat.dev: Welcome back to CodingCat.dev podcast. Today on the show, we've got Brian, also known as bdougie. What's up?
Brian: Hey, glad to be back!
CodingCat.dev: It's been a while. We last had you on in December of '22. With Hacktoberfest kicking off, it’s the perfect time to discuss open source. Let's start with a refresher for everyone. What’s new with OpenSauced since our last chat?
Brian: Sure! OpenSauced initially started as a way for contributors to track and manage their open-source contributions. It has evolved significantly since then. We raised a seed round, scaled up the team to ten members, and our focus is now on making more open-source projects successful through data-driven insights.
Origin and Evolution of OpenSauced
To give you a bit more context, OpenSauced is a project that found its roots in GitHub. I previously worked at Netlify, creating a CRM-like platform for open source contributions. It was designed to gather insights and PR data in one place, streamlining the process for contributors and maintainers alike.
After participating in Techstars and raising seed funding, our goal solidified: identify what makes open source projects successful and replicate those achievements through data analysis and centralized dashboards.
"The goal really is to identify what success in open source looks like. So trying to repeat those through data for projects, and then we have another opportunity for folks to look at their own personal profile and contributions."
Understanding Metrics in Open Source Projects
A critical aspect of OpenSauced’s platform is to provide metrics and analytics that help identify successful projects and contributors. Let's break down some of these insights:
Confidence Scores
We evaluate the "Confidence" score, which assesses the likelihood of someone contributing to a project. For instance, projects with high star and fork counts typically indicate higher engagement and confidence. With projects like Supabase showing that 40% of its stars and forks convert into contributions, it's evident that these metrics are integral for assessing community involvement and contribution patterns.
"Confidence is the ability for somebody to make a contribution to this project."
Case Study: TypeScript
TypeScript, backed by Microsoft, demonstrates different engagement levels. For TypeScript, only 16% of the community's stars and forks convert into contributions. This variance shows the challenges and complexities in contributing to more intricate projects like TypeScript.
Unknown block type \"image\", specify a component for it in the `components.types` option
Business Side of Open Source
Open-source projects often require backing from companies to succeed. Opensauced embarked on this journey by providing data for Hacktoberfest and raising seed rounds to further their efforts. This transition underscored the necessity of understanding business dynamics within the open-source ecosystem.
CodingCat.dev: Can you talk a bit about the seed round and what it took to make open source a viable business?
Brian: It's interesting. Initially, our goal was to make people successful in open source. At GitHub, I aimed to get developers to use the platform more effectively. The business side required us to identify specific metrics and stats that indicate project health and success. For example, partnering with companies like Vercel and Sanity helps us understand the ethos of open source and cater to varying needs.
Unknown block type \"image\", specify a component for it in the `components.types` option
The Role of Data in Open Source Success
Collecting and analyzing data from various repositories is crucial for identifying patterns of success in open source projects. Opensauced tracks metrics like contributions, issue creation/resolution, and PR reviews to create a comprehensive understanding of project health.
For instance, the introduction of the "OSCR" (Open Source Contributor Rating) provides a numerical evaluation of a contributor’s impact based on diverse activities. It balances confidence and sway (engagement through issues and PRs) to provide a non-gamifiable, comprehensive score.
Unknown block type \"image\", specify a component for it in the `components.types` option
"The Oscar is like a metric based on not only confidence but also quality...Sway is the ability of generating conversation within contributions."
Demo: The Opensauced Platform
Let's take a closer look at how the Opensauced platform functions:
1. Repository Insights: By examining repositories like Supabase, Opensauced tracks contributions and engagement rates. For example, despite high star counts, the engagement might vary, indicating different levels of community involvement. 2. Contributor Analysis: Identifying prominent contributors through metrics like engagement and contribution quality helps streamline the onboarding process for new contributors.
Addressing Challenges in Open Source Contribution
One notable challenge is that large projects like TypeScript often rely heavily on contributions from employees of the sponsoring company, like Microsoft. This internal contribution tendency skews the general participation metrics and makes it trickier for new contributors to navigate.
CodingCat.dev: What about contributors for large projects?
Brian: It's true. For projects like TypeScript, contributions are predominantly from employees. Identifying and engaging with external contributors becomes essential to maintain a diverse and active community.
Tackling the Gamification Issue
A significant focus for Opensauced is ensuring that the metrics and scores provided cannot be gamified. This authenticity ensures that the scores are a true reflection of actual contributions and engagement.
CodingCat.dev: How do you ensure that the scores can’t be gamed?
Brian: We try to construct a score that requires participation across various domains. Gamifying such a score is challenging as it demands consistent, meaningful activity across multiple areas.
Future of Opensauced: Vision and Goals
Looking forward, Opensauced aims to enhance its capabilities by leveraging AI and machine learning to provide even more granular insights into contributions and project success. This includes expanding features like the code OWNERS file automation and exploring broader integrations with enterprise solutions.
CodingCat.dev: Where do you see Opensauced in the next 2-3 years?
Brian: We're actively working on partnerships and embedding our product within larger ecosystems. Our vision is to orchestrate developer culture, ensuring smooth contributions and project management both in open-source and enterprise environments.
"We see ourselves having a footprint on how folks are transferring knowledge and orchestrating their culture."
Community and Contributor Growth
The future involves empowering contributors and maintaining strong community engagement. Tools like vectorized search for issues and discussions are designed to facilitate better project management and contributor onboarding.
Unknown block type \"image\", specify a component for it in the `components.types` option
Final Thoughts
Opensauced is driving the way forward in optimizing open source project management through data-driven insights and community-focused solutions. By addressing the unique challenges faced by contributors and maintainers, Opensauced helps create successful and sustainable open source ecosystems.
Whether you're an individual contributor looking to make your mark in open source or an enterprise seeking to streamline your open source involvement, Opensauced offers invaluable tools and insights. Visit OpenSauce today and see how you can make a difference.
Resources
Code Snippets and Examples
Unknown block type \"code\", specify a component for it in the `components.types` option
Unknown block type \"code\", specify a component for it in the `components.types` option
Quote Highlight
"Confidence is the ability for somebody to make a contribution to this project, and the way we've been marking this is like folks who star and fork. Like if you look at Supabase, I think it is like seventy thousand stars."
",
+ "url": "https://codingcat.dev/podcast/building-successful-open-source-projects",
+ "title": "Building Successful Open Source Projects",
+ "summary": "I chatted with Brian (aka bdougie) about Hacktoberfest, building OpenSauced, and making successful open-source projects.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/136256313ec652e9b5deb0fa9ef68c634300201c-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-09-25T20:39:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "09d69095-4287-4b69-9f02-3d2e873f358e",
+ "content_html": "Welcome back to the CodingCat.dev podcast! Today, we are thrilled to have Sathiesh Veera, an expert in infrastructure and development, share his amazing journey from his roots in India to becoming an innovator in the tech industry. Sathiesh's story is inspiring and packed with valuable insights on how infrastructure and development intertwine to create robust tech solutions.
From Traditional Jobs to Java Enthusiast
During his college years, Sathiesh had the opportunity to get placed through campus interviews, which led him to work for various companies. Unlike many, his journey wasn't just about getting a job; he developed a profound love for Java, a language he has worked with for over 15 years.
"I started my first job, and that's where I got into Java. I always loved Java and developed too much interest in it."
While many of us start our coding journey with languages like C and Java, Sathiesh's curiosity pushed him further into exploring more complex topics like data engineering and DevOps.
Transitioning to the United States
Around 2015, the wave of AWS and microservices began to take over the tech world. Sathiesh moved to the United States and transitioned his focus to microservices and infrastructure-related work.
Evolution to DevOps
Working closely with platform teams, Sathiesh found his interest pivoting towards DevOps. He enjoyed the blend of infrastructure and development, taking on roles that focused on deployment pipelines and improving efficiency.
"I was more focused on deployment pipelines, looking to automate testing and parallelizing activities to reduce unnecessary steps."
The Birth of CloudPac
Four years ago, Sathiesh and his friends Madhu Chavva and Tej Rapolu, came up with an idea born out of solving infrastructure problems for companies. They started CloudPac, focusing on creating a unified solution for fragmented cloud services.
"CloudPac aims to solve fragmented services by uniting compute, storage, networking, and security in one package."
Despite early setbacks, including challenges with pitching to YC and personal hurdles, the team persevered. They've learned from their mistakes and are optimistic about the future.
Infrastructure Driven Development (IDD)
Now, let's dive into the core topic of today's discussion: Infrastructure Driven Development (IDD). Unlike traditional coding practices where infrastructure is an afterthought, IDD emphasizes creating applications with a deep understanding of where the code will run.
Key Takeaways
- Awareness of Environment: Developers need to be aware of where their code will execute. This awareness impacts decisions on scalability, efficiency, and cost.
- Performance vs. Cost: The trade-off between the two is crucial. For instance, AWS Lambda offers a cost-effective solution but comes with constraints like cold starts.
- Microservices: Understanding the different compute options, from AWS Lambda to Kubernetes, and their respective pros and cons.
"A lot of companies face the challenge of making cost-efficient decisions initially and then dealing with the repercussions later."
Challenges and Solutions in Infrastructure
Throughout his career, Sathiesh has encountered numerous challenges, from inefficient deployment pipelines to cost overruns in cloud services. His approach has always been to balance functionality with infrastructure awareness.
Case Study: AWS Lambda and DynamoDB
When working with AWS Lambda and DynamoDB, Sathiesh's team observed significant cold start issues and connection limitations with their database.
"Each lambda instance created its own connection to the database, leading to contention issues."
To tackle this, they explored alternatives and optimized their approach to ensure better performance and cost-efficiency.
The Future of CloudPac
CloudPac continues to evolve, aiming to provide not just pre-built solutions but customizable stacks that consider various business needs. By leveraging LLM, they aim to offer tailored recommendations based on specific project requirements.
"The developer's role is evolving. Infrastructure awareness is no longer an option but a must-have."
",
+ "url": "https://codingcat.dev/podcast/infrastructure-driven-development",
+ "title": "Infrastructure Driven Development",
+ "summary": "Join us as Sathiesh Veera shares his journey from engineering in India to a career in Java development, DevOps, and founding a tech startup.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/9df9ecc7df62eeb57400e08c5e52d109020ced91-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-09-20T17:48:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "4edb915d-6b6c-4d9e-9692-0127da16ca1b",
+ "content_html": "Background of Corbin
- Early Education: Corbin shares his educational journey, including dropping out of high school due to personal challenges, and starting community college at a young age.
- Personal Struggles and Resilience: Corbin candidly discusses his battle with schizoaffective disorder and the impact of personal tragedies, emphasizing the human side of his journey into tech.
Early Career and Interests
- Internship and Early Job: Corbin talks about his early job at a charter school and his initial interest in design over programming.
- First Break in Web Development: A turning point came when a friend's project, which included his name, led to a job offer in web development despite his initial lack of experience in that field.
Career Development
- Progression in Web Development: He further elaborates on his career progression, from leading a front-end development team to discovering a passion for teaching while working at a boot camp.
- Consulting Roles: Corbin discusses his consulting roles with various companies like Astro and Clerk, where he primarily focused on developer education and documentation.
Current Role at Descript
- Job Functions: Corbin explains his current role at Descript, working on client platform teams and upgrading tools like React and Redux.
- Approach to AI: He shares his perspective on AI as just another API, emphasizing consistency in handling latency, reliability, and error management across different API styles.
Framework Guide Book Trilogy
- Introduction to Framework Guide: Corbin introduces his book series, "Framework Guide," which aims to teach the fundamentals of web development across multiple frameworks like React, Angular, and Vue.
- Books in the Series: The first book is out, focusing on componentization basics, while future books will cover ecosystem and internals, including in-depth framework construction.
Teaching Approach and Web Development Fundamentals
- Specialization vs. Generalization: Corbin's philosophy on learning involves rebuilding projects multiple times to understand the why behind frameworks, as opposed to just the how.
- Project-Based Learning: He advocates for a comprehensive understanding of project-based learning, ensuring that learners grasp necessary concepts such as internationalization, error handling, and accessibility.
Personal Preferences and Opinions on Frameworks
- Favorite Framework: Corbin shares his evolving preferences in frameworks, from Angular to React, then Vue, and highlights his excitement for emerging technologies like Angular signals.
- Complexity of Modern Web Development: He argues that the perceived increase in web complexity is more about rising user expectations rather than the inherent complexity of the software itself.
Final Thoughts and Next Steps
- Advice for Aspiring Developers: Corbin provides advice on the importance of cataloging what you learn, whether through blog posts, notes, or other means, to reference later in your career.
- Community and Open Source: He invites viewers to join his community at "Playful Programming" and to check out his book series for further learning and development.
Purrfect Picks
- Corbin's Pick: He recommends Qobuz for high-quality, DRM-free music, suitable for audiophiles who value superior audio experiences.
- Alex's Pick: Alex shares his discovery of Fabric.js, a flexible and lightweight JavaScript library for working with the HTML5 canvas, which he found to outperform other similar solutions.
Links
https://unicorn-utterances.com/collections/framework-field-guide
https://tanstack.com/form/latest
",
+ "url": "https://codingcat.dev/podcast/webdev-fundamentals-or-react-angular-and-vue",
+ "title": "WebDev Fundamentals | React, Angular, and Vue",
+ "summary": "Dive into web development fundamentals with Corbin. Explore frameworks, APIs, and developer education. Purrfect for new and seasoned developers looking to expand their knowledge.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/cc72a566723af57a17e0ef0b6e7b1214ffb70c60-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-07-24T20:21:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "120b48ee-cd22-429e-b36f-d0a4704565e4",
+ "content_html": "Introduction and Tech-Related Banter
- Technical Setup Issues: The video begins with the host experiencing some technical difficulties related to the pre-roll video.
- Guest Introduction: Vincent, the guest, is introduced. He recently changed jobs and shares some light-hearted banter with the host.
Vincent's Journey into Tech
- Career Path: Vincent explains his unique journey from aspiring to be an electrical engineer to becoming involved in software development. He cites his initial influence from a Minecraft streamer.
- Role Transition: He details his transition from electrical engineering to mobile QA, firmware development, product management, and finally technical writing and developer relations.
Challenges in Technical Writing
- Language Barriers: Vincent discusses the nuances of English as a second language and how it impacts his technical writing.
- Writing Style: He emphasizes the importance of clear, simple language in technical writing to ensure comprehensibility. Complicated language often makes documentation less accessible.
Importance of Documentation
- Product Discovery: The discussion includes how crucial it is for documentation to aid in the user journey from discovering the product to effectively using it.
- Feedback Loop: Vincent shares his practice of testing documentation clarity by walking through it with colleagues or new hires to identify pain points.
Analyzing Documentation Layouts
- Various Documentation Sites: The host and Vincent review different documentation sites (Cloudinary, Algolia, Appwrite, and Stripe) highlighting strengths and weaknesses.
- User Experience: They discuss how the organization and presentation of documentation significantly affect user retention and education.
Treating Documentation as a Product
- Investment in Documentation: Vincent suggests documenting like a product, highlighting the importance of making a business case for comprehensive documentation.
- User Journey in Docs: He explains that documentation should be tailored to closely follow the user journey, from first-time use to advanced usage scenarios.
Pain Points and Best Practices
- Design Collaboration: He advises integrating documentation writing within the product design phase to ensure that both documentation and product flow are aligned from the beginning.
- Measuring Effectiveness: Vincent talks about using analytics to track user engagement with documentation, identifying drop-off points, and iterating based on these insights.
Future of Documentation and AI
- AI Integration: The conversation veers into the future role of AI in generating and maintaining documentation. Vincent believes that while AI can assist, human-written documentation will always be required for niche, complex use cases.
- Role Transition: He envisions AI handling more straightforward documentation tasks with technical writers focusing on nuanced, detailed, and unique documentation needs.
Personal Insights and Preferences
- Preferred Tools: Vincent suggests starting with markdown-based documentation tools like Starlight from Astro for their flexibility and ease of migration.
- Tips for New Businesses: He recommends keeping documentation simple and generic initially to allow for future adjustments and scalability.
Documentation Tools
https://www.gitbook.com/
https://docusaurus.io/
https://starlight.astro.build/
https://vuepress.vuejs.org/
",
+ "url": "https://codingcat.dev/podcast/turning-documentation-into-a-product-best-practices-for-success",
+ "title": "Turning Documentation into a Product: Best Practices for Success",
+ "summary": "Explore best practices for turning documentation into a successful product with real-world insights. Elevate your docs by learning from industry experts!",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/0e2c63a5d529dd0faa9c7049f18299afec3b9c15-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-07-17T16:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "30be7590-3e6f-412f-bcd9-a5421ed57997",
+ "content_html": "Introduction and Welcome
- Introduction of Guests and Sponsors: The host welcomes the audience to the Coding Cat.dev podcast, sponsored by Cloudinary and Algolia, and introduces Thomas Johnson, CTO of Multiplayer.
- Brief Overview of Multiplayer: Thomas provides a one-minute overview of Multiplayer, explaining that it offers tools for teams working on distributed software to visualize system architecture and manage dependencies collaboratively.
Early Career and Inspiration in Tech
- Tom's Introduction to Programming: Thomas shares how he first got into technology as a child when his father brought home an Apple II Plus. He learned BASIC programming to create simple programs, which ignited his lifelong interest in software development.
- Academic and Career Journey: Thomas discusses his educational background, including graduate studies in robotics and AI, and his early professional experiences in speech recognition and distributed systems.
Pain Points in Early Software Development
- Challenge of Distributed Systems: Thomas highlights how early experiences with speech recognition in telecom introduced him to the complexities of distributed systems.
- Scaling and Technical Debt: Discussion on how startups often face challenges scaling their products and dealing with technical debt, based on Thomas’s observations from his consulting roles.
Multiplayer Features and Capabilities
- Introduction to Multiplayer: The host and Thomas delve into Multiplayer's features, including system architecture visualization, real-time collaboration, version control, and its upcoming Radar and Pulsar features.
- Manual Versus Automated Documentation: Comparison of traditional, manual system documentation methods to Multiplayer’s automated solutions using Radar for detecting system architecture without manual input.
Practical Demo of Multiplayer
- Before Multiplayer: Thomas shows how traditional tools like diagrams.net or swagger fall short in effectively documenting and communicating complex system architectures.
- Using Multiplayer: A demonstration of Multiplayer’s capabilities, such as auto layout, real-time collaboration, and connecting to GitHub repositories to pull accurate data. Thomas explains how their future feature, Pulsar, will allow users to build and deploy platforms from templates.
AI in System Design
- AI Assistance: Discussion on how AI can assist in system design within Multiplayer, from automating monotonous tasks to providing debugging aids. The AI will also help users with best practices and potentially generate initial code based on user inputs.
- Chat-Based Interaction: The potential for a chat interface to assist developers and even customers in interacting with and modifying system components.
Potential for Consulting and Partnerships
- Consulting Opportunities: Exploration of how Multiplayer could partner with consulting firms to offer system architecture expertise to their clients, providing integrated workspaces within Multiplayer for a seamless project handover.
- Broader Application: Emphasis on how the benefits of having a well-documented, collaborative platform can extend beyond initial development to ongoing maintenance and scaling.
Concluding Thoughts and Future Features
- Future Development: Thomas outlines upcoming features for Multiplayer, emphasizing how they aim to make system design easier and more collaborative. This includes enhancing the AI capabilities and deploying complete environments with third-party solutions.
- Closing Remarks and Picks: The host wraps up by sharing their recent entertainment picks, and Thomas recommends a book on drawing comics. They conclude by expressing enthusiasm for what Multiplayer is set to achieve.
",
+ "url": "https://codingcat.dev/podcast/ai-for-system-design-and-architecture-documentation",
+ "title": "AI for System Design and Architecture Documentation",
+ "summary": "Learn how AI transforms system design and architecture with insights from Multiplayer's CTO. Explore new tools and collaborative features for developers.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/b43bd666d412b682acb84db7ec9971753b1edf24-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-07-08T16:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "796783b3-91df-4657-9db0-316a77cf4cc1",
+ "content_html": "Introduction and Guest Background
- Guest Introduction: Roman Jacquez introduces himself, describing his background. Originally from the Dominican Republic, Roman shares his experience of moving to New York, studying computer science, and his initial interest in civil engineering before transitioning to software engineering.
Career Journey
- Early Career: Roman talks about his early days working in Lion Bridge as a desktop publisher and his passion for software engineering that led him to create internal tools.
- Philips Health System: Roman discusses his role at Philips Health System where he pushed for the adoption of Flutter within his team, leading to significant projects supported by the Bill and Melinda Gates Foundation.
Flutter and Google's Developer Ecosystem
- Google I/O and Flutter: Discussion revolves around the 2023 Google I/O event and key Flutter announcements. Roman highlights improvements to Flutter’s Impeller rendering engine and introduction to WASM (WebAssembly) support.
- Dart Language and Multi-Platform: Roman and Alex discuss advancements in the Dart language including macros. They also talk about Google's strategy in offering multiple development options such as Flutter and Kotlin Multi-Platform.
Firebase and Flutter Integration
- Firebase Data Connect: Roman shares his insights on Firebase Data Connect and his excitement over the integration of PostgreSQL and GraphQL, providing more versatility compared to the traditional NoSQL Firebase approach.
- Firebase Tools and Features: Emphasizes the range of Firebase tools like Remote Config, Firebase Authentication, and Cloud Messaging, and how these tools can be effectively integrated with Flutter to build comprehensive applications.
AI and Machine Learning Integration
- Gemini and Google AI: Roman discusses his experiments with Google's generative AI package and Vertex AI, explaining how these make implementing advanced AI functionalities accessible even for those with limited AI expertise.
- Multi-Modal Capabilities: Explains the potential of combining various media formats (text, video, images) using Gemini’s multi-modal capabilities, which allows for sophisticated applications closer to real-time utility and richer user experiences.
Community Involvement and Future Outlook
- Developer Community: Roman is actively involved in GDG (Google Developer Group) Lawrence, highlighting the importance of community in learning and sharing new technology advancements.
- Future Projects: Mentions upcoming initiatives and projects involving Flutter, Firebase, and AI. These include workshops and events aimed at fostering community and learning.
Entertainment and Personal Interests
- Personal Picks by Roman: Roman shares his recommendation for the TV series "Beef," emphasizing its gripping narrative.
- Technology Picks by Alex: Alex discusses Next.js updates, highlighting features in the upcoming version 15 and praising its potential benefits for web developers.
This structured summary highlights key themes and subtopics discussed, providing an overview that's easy to skim through.
",
+ "url": "https://codingcat.dev/podcast/google-i-o-flutter-updates-and-firebase",
+ "title": "Google I/O Flutter Updates and Firebase",
+ "summary": "Catch the latest updates on Flutter and Firebase from Google I/O 2024. Hear Roman Jacquez's insights and explore the exciting new features.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/bfa80d1c7afd30456c4d0d6e9819eda48b6826fa-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-07-05T16:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "ca69fea5-608a-4cee-9eba-8a2d915d7c84",
+ "content_html": "Introduction and Background of Guest
- Amanda's Background: Amanda discusses her educational journey, starting from her early exposure to computers due to her father's work with assembly and Fortran, to studying computer science in Brazil and the UK. She also shares her initial aspirations to become a professor before transitioning into a developer role.
- Balancing Career and Parenthood: Amanda talks about her experience as a first-time mom while working in developer relations, touching on the challenges and strategies she uses to balance both responsibilities.
Early Career and Education
- Early Exposure to Tech: Amanda's father played a significant role in her tech journey, exposing her to computers at a young age and encouraging her interest in tech.
- Educational Journey: Amanda elaborates on her academic path, including the challenges of studying math and moving from Brazil to the UK to complete her degree. She also discusses the multicultural environment at her Brazilian university.
Professional Development and Career Growth
- Transition to Dev Role: Amanda shares her transition from academia to a developer role, learning Ruby on Rails on the job, and her preference for JavaScript.
- Post-Graduation Studies: During the pandemic, Amanda pursued a post-graduation degree, which brought her a sense of fulfillment and advanced her knowledge in the field.
Community Engagement and Contributions
- Meetup Involvement: Upon moving to the UK, Amanda became actively involved in local meetups, eventually becoming an organizer and Women Techmakers ambassador.
- Hosting Events: Amanda organizes and hosts events such as "Women Techmakers Coffee and Code," fostering community engagement and knowledge sharing.
Working at Vonage
- Joining Vonage: Amanda joined Vonage during the pandemic and discusses the international and supportive nature of her team. She also highlights the various roles and projects she has been involved in.
- Learning and Growth: Amanda emphasizes the diverse opportunities for learning and growth at Vonage, including her work with network APIs and contributing to product development.
Using Firebase with Communication APIs
- Demo Overview: Amanda presents a demo on using Firebase with the Vonage Verify API for account recovery, showcasing a flow where users verify their identity via SMS to reset their password.
- Technical Details: The demo involves Firebase Functions, Firestore as the NoSQL database, and Vonage communication services to check SIM swap details and send verification codes.
Setting Up Firebase and Integration with Vonage
- Firebase Setup: Amanda explains the process of setting up Firebase, including creating a project, configuring Google Cloud resources, and setting up the Firestore database.
- Integrating Vonage APIs: Details on setting up a Vonage account, generating keys, and using the Verify API for sending one-time codes are provided. Amanda also discusses handling environment variables and testing the setup with Firebase emulators.
Content Creation and Community Contributions
- Blog Posts and Videos: Amanda shares various blog posts and videos she has created on topics like appointment schedulers, sending messages with Firebase, and migrating from Firebase generation one to two.
- Firebase Extensions: Amanda talks about contributing to Firebase extensions and the types of extensions available, such as multi-party video calls and message APIs from Vonage.
Future Events and Conferences
- Upcoming Conferences: Amanda expresses her excitement about attending Google IO Connect in Berlin, mentioning her preference for smaller, more interactive events.
- Community Engagement: She discusses the importance of accessibility and how attending past conferences has profoundly impacted her understanding and implementation of inclusive design in her projects.
Conclusion
- Community Involvement: Amanda invites viewers to join the Vonage developer community on Slack for further interaction and support.
- Final Thoughts: The session concludes with Amanda and Alex discussing the significance of professional networks, continuous learning, and the importance of community support in the tech industry.
",
+ "url": "https://codingcat.dev/podcast/using-firebase-with-communication-apis",
+ "title": "Using Firebase with Communication APIs",
+ "summary": "Explore how to integrate Firebase with Vonage APIs in our latest developer podcast. Learn about authentication, hosting, and more. ",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/3e572275c1b33cd918595f9513af587577179bea-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-07-03T16:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "ee3176c9-76b8-44cf-9a87-0fcac0aa8e53",
+ "content_html": "Introduction and Guest Introduction
- Welcome and Context: The hosts welcome viewers to the Coding Cat.dev podcast sponsored by Cloudinary and Algolia, noting fresh content post-Google I/O.
- Guest Introduction: Andrea, a developer relations engineer at Firebase, is introduced and provides an overview of her role.
Andrea's Career Path
- Educational Background: Andrea shares her educational background in Electrical Engineering and Computer Science with a focus on CS.
- Early Career: She details her early work experience at Cisco focusing on networking, and at Yahoo working on frontend development using React Native.
Transition to Google and Firebase
- Joining Google: Andrea discusses her joining Google five and a half years ago, starting with the Firebase console team.
- Role Evolution: She highlights her shift to different product teams within Firebase, focusing on remote config and predictions before moving into DevRel during COVID-19.
Developer Relations Engineer Role
- Role Responsibilities: Andrea explains what it means to be a developer relations engineer, from interacting with developers to relaying feedback to the product team.
- Day-to-Day Work: She describes the nature of her work, including working on content creation, mentoring, and organizing training events.
Google I/O and Firebase
- Google I/O Planning: The hosts discuss the stress and detailed planning that goes into preparing for Google I/O, from content creation to documentation.
- Examples of Work: Andrea provides specifics about her involvement, like mentoring engineers and reviewing documentation for new product releases.
Conferences and Presentations
- Conference Experience: Andrea explains the preparation involved in giving presentations at conferences and the difference between virtual and in-person experiences.
- Content Creation: Discusses the challenges and joys of creating new technical content, particularly in terms of introducing new features like Firebase Data Connect.
Firebase Data Connect
- Product Overview: An introduction to Firebase Data Connect, a new SQL database feature, which simplifies database queries through schema creation.
- Content Involvement: Andrea's role in working with the product team to create educational materials and blog posts about utilizing Data Connect.
Interacting with the Community
- Community Engagement: The importance of events like North America Connect for gathering feedback and engaging directly with Firebase users.
- Feedback Importance: Highlights the value of receiving direct user feedback to improve Firebase products.
Personal Enjoyment in DevRel
- Interactions and Feedback: Andrea emphasizes her passion for interacting with developers and other teams, finding it the most rewarding part of her role.
- Content Creation: She also enjoys the creative aspects of content creation, from writing to video production.
Challenges and AI in DevRel
- Workload and AI: Discusses the potential use of AI to assist in task management but stresses maintaining personal touch and creativity in content creation.
- Future Considerations: Reflects on how AI could help streamline certain aspects of her work without losing the human element.
Highlights of Google I/O Experience
- Personal Highlights: Andrea fondly recalls meeting her colleagues and other attendees in person, underscoring the value of in-person interactions.
- Event Excitement: Mentions specific moments of excitement, such as the unveiling of new Firebase features and team engagements.
",
+ "url": "https://codingcat.dev/podcast/what-is-a-firebase-developer-advocate",
+ "title": "What's A Firebase Developer Advocate",
+ "summary": "Discover the latest from Google IO and Firebase with insights from a DevRel engineer. Watch now for developer tips and community highlights to boost your projects!",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/dcb52c808f0cbc2644ae97c1fcfc4c4d841d342f-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-06-03T05:12:56.783Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "1bb4346d-eba0-41af-aecf-644bc7bcdefe",
+ "content_html": "Your Own Angular Cheat Sheet with the Angular Cookbook 2nd Edition
Welcome back, folks, to Codingcat.dev! In today's blog, we dive into an exciting resource for Angular developers: the Angular Cookbook, 2nd Edition by Mohammad. Whether you're a seasoned pro or just getting started with Angular, this book has you covered with practical recipes designed to solve real-world problems.
Let's explore Mohammad’s journey, the highlights of the cookbook, and the invaluable insights it brings to the Angular community.
Meet Mohammad
Before we get into the intricacies of the Angular Cookbook, let’s get to know Mohammad and his impressive journey from Pakistan to becoming a Google Developer Expert and author.
Early Days in Karachi
Mohammad hails from Karachi, Pakistan, a city known as the tech hub of the country. Although it has faced challenges over the years, Karachi remains a significant center of economic activity. Mohammad recounts:
"Karachi collects 35% of Pakistan's tax revenue and generates approximately 25% of the entire GDP."
Growing up in a small town within Karachi, Mohammad had to travel long hours for education. Despite the hardships, he maintained a passion for learning and focused particularly on coding subjects.
Journey into Development
Mohammad's foray into development began in 2013, when he joined a company as a software engineer, tasked with transitioning from Flash to HTML5. This was his introduction to Angular, and he was immediately captivated by its capabilities:
"When I went to the Angular site, I could just type something right in the site and I did not have to bind any event listeners on anything at all. But then magically, there was two-way data binding. I was fascinated by that."
Through dedication and contributions to open source projects, Mohammad quickly advanced in his career, eventually becoming a senior software engineer and leading small teams.
Transition to Sweden
In his quest for new challenges, Mohammad moved to Sweden to work with Klarna, a unicorn company specializing in online financial services. His time at Klarna involved extensive work with React and React Native, but his love for Angular never waned.
Becoming a Google Developer Expert (GDE)
Inspired and mentored by Mahoud, the second GDE from Pakistan, Mohammad embarked on a journey to build his portfolio and contribute to the community. His notable achievements include creating the NGX Device Detector, a library widely used in the Angular ecosystem.
"End of creating this thing called NGX Device Detector, which is right now one of the very famous libraries for Angular. We recently crossed, I think, 10 million downloads."
In 2018, Mohammad's hard work paid off when he received the prestigious GDE title, marking a significant milestone in his career.
The Angular Cookbook 2nd Edition
Fast forward to today, Mohammad has authored the Angular Cookbook 2nd Edition, a comprehensive guide designed to help Angular developers tackle common challenges with practical solutions.
Why the Angular Cookbook?
The idea for the cookbook stemmed from the need to bridge the gap between complex documentation and real-world applications. PACT reached out to Mohammad with the idea, and he put together an outline that addressed common problems faced by developers. The goal was to create a resource that follows the pattern of:
- Identifying the Problem
- Implementing the Solution
- Explaining How It Works
"The idea was to give something that follows the pattern of how I think a lot of developers learn. That is by implementing something and seeing and figuring out if this works or not, rather than a lot of text."
Writing Process and Teamwork
Creating the cookbook was no small feat. Mohammad collaborated with editors and technical reviewers to ensure the highest quality. Here’s a look into the process:
- Writing the Chapter: Mohammad wrote each chapter, detailing the problem, solution, and implementation specifics.
- Technical Review: A reviewer tested the code to ensure it worked as described.
- Grammar and Style Check: Editors refined the text for clarity and accuracy.
"One of the things that actually spilled over even after publishing was that just yesterday or the day before yesterday, I've upgraded all the logos of Angular to the new one."
Standout Features of the Cookbook
The Angular Cookbook covers a vast array of topics, from basic Angular principles to advanced performance optimizations. Here are some standout recipes and features:
Defer Blocks
One of the game-changing recipes in the cookbook is Using Defer Blocks for Lazy Loading. This technique significantly reduces the initial load time of applications by loading components only when necessary.
Unknown block type \"code\", specify a component for it in the `components.types` option
"Now we got the drag and drop component right here. And until that appears, you also saw a bit of a tiny loader there. This is while the component is being loaded."
Progressive Web Applications (PWAs)
The cookbook delves into creating PWAs with Angular, complete with a fun dice-rolling game example. This recipe covers service workers, caching strategies, and offline capabilities.
Unknown block type \"code\", specify a component for it in the `components.types` option
Form Arrays
Another practical recipe is working with Form Arrays in Angular. This section teaches how to dynamically create, add, and remove form controls.
Unknown block type \"code\", specify a component for it in the `components.types` option
Performance Optimizations
The cookbook also offers invaluable tips for optimizing Angular applications, such as using Web Workers for heavy computations.
Unknown block type \"code\", specify a component for it in the `components.types` option
"One of the things that actually spilled over even after publishing was that just yesterday or the day before yesterday, I've upgraded all the logos of Angular to the new one"
Conclusion
The Angular Cookbook 2nd Edition by Mohammad is more than just a book; it’s a toolkit for developers looking to harness the full power of Angular. From lazy loading with Defer Blocks to creating robust PWAs and optimizing performance with Web Workers, this cookbook provides practical, hands-on recipes for every Angular enthusiast.
Why You Should Get the Angular Cookbook
If you're looking to:
- Enhance your Angular skills with real-world examples.
- Solve common problems effortlessly.
- Optimize your applications for better performance.
- Stay updated with the latest Angular features and best practices.
Then this cookbook is a must-have! Get your copy today and start cooking up some Angular magic.
For more insightful articles and resources, check out Codingcat.dev. And if you have any questions or would like to share your experience with the Angular Cookbook, feel free to leave a comment below!
Happy coding! 🎉
",
+ "url": "https://codingcat.dev/podcast/angular-cookbook",
+ "title": "Angular Cookbook",
+ "summary": "Discover advanced Angular tips and tricks with Mohammad from the Angular Cookbook 2nd Edition. Watch now and elevate your front-end development skills!",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/41616e585d8fa52a194ff752da8bb35b9eaf778c-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-06-03T04:42:37.257Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "59b06426-2b94-4b31-ab7a-f18659c121e5",
+ "content_html": "A Friday Chat with Chris Sev: From Snow Shovels to Dev Advocacy and AI Tools
Welcome back, perfect peeps, to another fantastic episode of the Codingcat.dev podcast! Today, we're sitting down with the ever-amazing Chris Sev. So grab your favorite dev snacks, sit back, and let's dive into the world of developer advocacy, AI tools, and much more, thanks to our sponsor Cloudinary, the go-to for AI-powered image and video APIs.
Kicking Off the Weekend Vibes
We got started with a casual Friday vibe, something Chris and I both seemed to enjoy. The conversation flowed easily, touching on everything from recent snowfall adventures to the challenges of shoveling driveways. A perfect reminder that even devs have to deal with the mundane.
"I just shoveled for like 2 hours yesterday, getting around 17 inches of snow off my driveway. It's like my rite of passage to the area."
I'm up in Michigan where snowblowers are the norm, but it's been a tame winter, so not much action on that front.
Journey into Web Development
For those who might not be familiar, Chris shared his origin story in web development. Amazingly, it all began at the tender age of 14 with a bootleg copy of Dreamweaver.
"I started with a WYSIWYG drag-and-drop type editor, and I was clicking over to the HTML. It was trash, but I just started fixing it."
From those humble beginnings, Chris moved on to freelancing, creating websites for neighbors, and eventually starting a site that most of us know and love—Scotch.io.
The Rise and Acquisition of Scotch.io
Chris's journey with Scotch.io is nothing short of impressive. The site grew to 4 million pageviews per month and was eventually acquired by DigitalOcean. This phase of his career was a significant learning experience, teaching him everything from content creation to business acquisition.
Creating Tutorials with a Fresh Twist
One of the unique aspects of Scotch.io was its informal and engaging tutorials, making learning fun and approachable.
"We took pride in, like, not putting a suit on. We made it fun and fresh."
This approach resonated with a massive audience and became a fundamental part of Chris's journey into developer advocacy.
Developer Advocacy and Beyond
After the acquisition, Chris transitioned into the role of a Developer Advocate, a position he finds incredibly rewarding.
"It's like a free pass. You can flip and do anything as well."
He finds joy in interacting with customers, helping product teams, writing docs, and making videos. This flexible role allowed him to leverage his extensive experience in web development and content creation.
The Birth of Videotap: An AI-Driven Tool for Content Creators
Chris's latest venture, Videotap, is a perfect blend of his expertise in content creation and AI technology. Videotap aims to repurpose videos into engaging marketing content using AI. It generates social media clips, blog posts, recaps, and more.
"Videotap takes all your videos, hours spent making them, and uses AI to generate social media clips, blog posts, recaps, and everything you need for marketing."
Building Videotap’s Tech Stack
The tech stack for Videotap includes Laravel, AWS, OpenAI, and AssemblyAI. Laravel was chosen for its productivity, especially when combined with its Livewire packaging.
The AI aspects primarily involve making prompts and calls to OpenAI or Anthropic, depending on the need, and AssemblyAI for transcriptions.
"Assembly gives us timestamps per word, which is crucial for building out clips and highlighting words."
Real-World Usage and Feedback
As a user of Videotap myself, I can attest to its usefulness. From breaking down long podcasts into engaging snippets to generating comprehensive blog posts, it’s become an indispensable tool for my content creation workflow.
"It's about making our lives easier. Instead of spending hours, you get your content ready with just a few clicks."
However, as with any evolving tool, there are always features and improvements to look forward to.
Integrating Seamlessly with Existing Tools
One key insight Chris shared is the importance of integration. By enabling Videotap to work seamlessly with platforms like YouTube, WordPress, and social media schedulers, it becomes a central hub for content creators.
"I want to build a workflow where everything is integrated. Upload, generate, and then one-click publish to YouTube or your CMS."
The Human Element in AI Tools
Despite the advancements in AI, Chris acknowledges the irreplaceable value of human creativity.
"Even with AI tools, there's a need to keep that human touch to maintain originality and creativity.”
The goal is to use AI to handle the mundane aspects, freeing up creators to focus on what they do best—create.
Looking Ahead
The journey of Videotap is just beginning, and Chris is excited about the future. With a growing family and a new home, he’s ready to dive back into improving and expanding Videotap.
"Now that we're settled, I'm stoked to focus on the next big updates for Videotap."
Wrapping It Up
From shoveling snow to building AI-driven content tools, Chris Sev continues to be a trailblazer in the dev world. Whether you're a content creator, a developer advocate, or just a fan of web development, there's a lot to learn and be inspired by in Chris's journey.
Stay tuned for more updates and keep creating!
Links & Resources
Code Snippets
Below are some useful code snippets shared during our chat:
Sample Laravel Setup
Unknown block type \"code\", specify a component for it in the `components.types` option
Utilizing OpenAI API
Unknown block type \"code\", specify a component for it in the `components.types` option
Integrating AssemblyAI for Transcriptions
Unknown block type \"code\", specify a component for it in the `components.types` option
Stay Connected
Follow Chris on Twitter @chris__sev to stay updated on all his latest projects and insights into the world of web development and AI.
"I just want to help content creators make the best use of their time and creativity."
Here's to the beautiful blend of code, creativity, and AI. Until next time, keep coding and stay perfect!
Note: This blog post was transcribed and edited using AI tools, such as Videotap, aiming to enhance and streamline the content creation process.
Links
https://tallstack.dev/
https://www.assemblyai.com/
",
+ "url": "https://codingcat.dev/podcast/video-tap-automate-your-video-marketing-with-chris-sev",
+ "title": "Video Tap: Automate Your Video Marketing with Chris Sev",
+ "summary": "Join us as Chris Sev shares his dev journey, insights on building Scotch IO, and his exciting new AI tool, Video Tap. ",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/d1d07eed9126f3861849491374fa6b22662bd16c-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-06-03T04:35:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "b61bf55d-ab3d-4164-8a35-f5f7eb770536",
+ "content_html": "Firebase Security Rules are a powerful feature that allows you to control access to your app's data in Firebase Realtime Database, Cloud Firestore, and Cloud Storage. By defining these rules, you can ensure that only authorized users can read, write, and update your data. This helps to protect your app from unauthorized access and malicious attacks.
Why Use Firebase Security Rules?
There are several reasons why you should use Firebase Security Rules in your app:
- Data Security: Security Rules act as a first line of defense against unauthorized access to your app's data. You can define who can access and modify your data, which helps to prevent data breaches and leaks.
- Privacy Compliance: Security Rules can help you to comply with privacy regulations such as GDPR and CCPA. By controlling access to user data, you can ensure that you are only collecting and storing data that is necessary for your app to function.
- Prevent Accidental Data Loss: Security Rules can help to prevent accidental data loss by restricting write access to certain parts of your database.
- Improved Performance: Security Rules can improve the performance of your app by reducing the amount of data that needs to be transferred between clients and the server.
Getting Started with Firebase Security Rules
Firebase Security Rules are written in a declarative language that is similar to JSON. This makes them easy to learn and understand, even for developers who are not familiar with security concepts.
To get started with Security Rules, you will need to create a security rules file. This file can be created in the Firebase console or in your code editor.
The basic structure of a Security Rules file is as follows:
Unknown block type \"code\", specify a component for it in the `components.types` option
This rule states that all users must be authenticated in order to read data from the database. Additionally, all write requests are denied.
Understanding the Rules Language
The Security Rules language consists of a number of keywords and operators that can be used to define access control rules. Here are some of the most common keywords and operators:
- auth: This keyword refers to the currently authenticated user.
- request.auth: This keyword contains information about the currently authenticated user, such as their user ID and any custom claims that have been associated with their token.
- resource: This keyword refers to the specific data path that is being accessed.
- root: This keyword refers to the root of the database.
- match: This keyword is used to create wildcard patterns that can be used to match multiple data paths.
- get: This keyword allows read access to a specific data path.
- set: This keyword allows write access to a specific data path.
- update: This keyword allows users to update specific fields within a data path.
- delete: This keyword allows users to delete data from the database.
Common Security Rules Examples
Here are some common examples of Security Rules that you can use in your app:
- Allow all authenticated users to read all data:
Unknown block type \"code\", specify a component for it in the `components.types` option
- Allow only authenticated users to read data, but only allow administrators to write data:
Unknown block type \"code\", specify a component for it in the `components.types` option
- Allow users to read their own data and only allow administrators to write data:
Unknown block type \"code\", specify a component for it in the `components.types` option
- Allow users to read and write to a specific data path:
Unknown block type \"code\", specify a component for it in the `components.types` option
Advanced Security Rules Features
Firebase Security Rules also support a number of advanced features that can be used to create more complex access control rules. Here are some of the most common advanced features:
- Variables: You can define variables in your Security Rules file to store data that can be reused throughout your rules.
- Functions: You can define custom functions in your Security Rules file to perform complex logic.
- Database triggers: You can create database triggers that will be fired when certain events occur in the database,
",
+ "url": "https://codingcat.dev/podcast/firebase-security-rules-effortless-control-over-your-app-s-data",
+ "title": "Firebase Security Rules: Effortless control over your app's data.",
+ "summary": "Firebase Security Rules are a powerful feature that allows you to control access to your app's data in Firebase Realtime Database, Cloud Firestore, and Cloud Storage.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/3d5fa42c489cd7f673ce2c4aa96db4ef5a31aa72-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-04-06T04:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "ZKdo3vLUqcb4sYreiCPA9U",
+ "content_html": "The State of Flutter in 2024: A Comprehensive Look
Flutter, Google's open-source framework for building beautiful, native-looking apps across mobile, web, desktop, and embedded devices, has taken the app development world by storm. Since its release in 2015, Flutter has garnered immense popularity due to its:
- Expressive and beautiful UI: Flutter empowers developers to design and build visually stunning, custom UIs with rich animations and smooth scrolling, all thanks to its layered architecture and widgets.
- Cross-platform development: With a single codebase, Flutter enables developers to create apps for various platforms like Android, iOS, web, desktop (Windows, macOS, Linux), and embedded devices, significantly reducing development time and cost.
- Hot reload: This feature allows developers to see the changes they make to the code reflected in the app in real-time, without the need to recompile or restart the app, leading to a faster development cycle.
- Dart, a productive language: Flutter utilizes Dart, a modern object-oriented programming language known for its simplicity, learnability, and development speed. Dart's features like ahead-of-time (AOT) compilation and garbage collection contribute to the overall performance and efficiency of Flutter apps.
A Glimpse into the Future: The Flutter Roadmap for 2024
The Flutter team is constantly working on improving the framework and expanding its capabilities. Their roadmap for 2024 outlines several key areas of focus:
- Quality and performance: The team is committed to making Flutter even more stable, reliable, and performant. This includes fixing bugs, improving the framework's core libraries, and optimizing rendering and memory usage.
- Multiple views: Flutter now supports multiple views on mobile and web platforms. This means developers can create apps with different views, such as a list view and a detail view, or a split-screen layout, enhancing the user experience and enabling the creation of more complex and interactive applications.
- Tooling and AI: The Flutter team is actively working on improving the tooling and AI support for Flutter development. This includes making it easier to develop and test Flutter apps, providing better code completion and suggestions, and leveraging AI to assist with debugging and code generation.
Beyond the Roadmap: Flutter's Continued Growth and Impact
While the roadmap highlights the official areas of focus, several other trends and advancements are shaping the current state of Flutter in 2024:
- Growing community and ecosystem: The Flutter community has grown exponentially in recent years, with a vast number of developers, designers, and enthusiasts contributing to the framework's growth. This vibrant community fosters knowledge sharing, collaboration, and the creation of a rich ecosystem of plugins, packages, and resources that empower developers to build even more sophisticated apps.
- Enterprise adoption: Flutter is increasingly being adopted by large enterprises for building mission-critical applications. Its ability to deliver high-performance, cross-platform apps with a single codebase is proving valuable for businesses looking to streamline development processes and reach wider audiences.
- Focus on emerging technologies: The Flutter team is actively exploring and integrating emerging technologies like foldable devices, augmented reality (AR), and virtual reality (VR) into the framework. This ensures that Flutter remains at the forefront of app development and empowers developers to create innovative and immersive experiences.
Key Considerations for Choosing Flutter in 2024
Here are some key factors to consider when deciding whether to use Flutter for your next app development project in 2024:
- Project requirements: If your project requires a visually appealing, cross-platform app with a fast development cycle, Flutter is an excellent choice.
- Development team expertise: If your development team has experience with Dart or similar object-oriented languages, picking up Flutter will be a smooth transition.
- Project budget and timeline: While Flutter can potentially reduce development costs and time due to its cross-platform nature and hot reload feature, it's crucial to carefully evaluate project-specific requirements and resource availability.
Conclusion: A Promising Future for Flutter
In conclusion, Flutter's position in the app development landscape remains strong in 2024. With its focus on quality, performance, new features, and a thriving community, Flutter empowers developers to build beautiful, cross-platform apps efficiently. As the framework continues to evolve and integrate emerging technologies, we can expect to see even more innovative and impactful applications built with Flutter in the years to come.
Additional Resources:
- Flutter official website: https://flutter.dev/
- Flutter roadmap: https://github.com/topics/flutter-roadmap
- FLOSS - https://twit.tv/shows/floss-weekly
- Fuchsia OS - https://fuchsia.dev/
- Flutter Roadmap - https://github.com/flutter/flutter/wiki/Roadmap
- FLOSS Restart - https://hackaday.com/2024/01/09/floss-weekly-episode-765-that-ship-sailed-and-sank/
",
+ "url": "https://codingcat.dev/podcast/4-9-flutter-2024",
+ "title": "Flutter 2024",
+ "summary": "Build beautiful, native experiences across platforms from a single codebase.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/9b0c2fdb08ada42536b9bc7c851dd490a3fbbba8-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-03-06T05:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "8510028d-dec1-40c7-bdbe-bed50dbc1f3d",
+ "content_html": "Introduction and Sponsor Acknowledgment
- Introduction and Welcome: The host introduces the Codingcat.dev podcast and welcomes Saon, who is featured in the episode.
- Sponsor Message: A brief mention of the podcast sponsor, Cloudinary, highlighting their AI-powered image and video APIs.
Saon’s Journey into Technology
- Non-Technical Beginnings: Saon shares her experience working in marketing and sales roles in a startup environment and her fascination with the technical side of product development.
- Transition to Coding: She discusses her self-taught coding journey, decision to quit her job to focus on coding full-time, and eventual enrollment at Flatiron School’s boot camp.
Boot Camp Experience
- Overview of Flatiron School: Saon provides an overview of her immersive education at Flatiron School, emphasizing the importance of community and support in her learning experience.
- Challenges and Rewards: She highlights both the difficulties of learning to code and the exhilaration of building functional products, emphasizing the net positive experience despite hardships.
Building Code Newbie Community
- Twitter Chats to Podcast Transformation: Saon describes how she started building communities through #CodeNewbie Twitter chats and eventually transitioned to podcasting to delve deeper into individual stories.
- Sustainability and Growth: She shares the evolution of Code Newbie, including the introduction of various initiatives like meetups, newsletters, and conferences, culminating in the acquisition by Dev.to in 2019.
Educational Background and Diverse Interests
- Academic Journey: Saon discusses her time at the University of Maryland, initially pursuing pre-med and later obtaining degrees in English and Psychology.
- Pivot to Tech and MBA: She explains her decision to transition from medicine to technology, driven by her interest in coding, and her eventual pursuit of an MBA to deepen her understanding of the business side of tech startups.
Integrating Code Newbie into Dev.to
- Acquisition Experience: Saon talks about her relationship with Ben Hopbper of Dev.to and the smooth transition of Code Newbie community initiatives into the Dev.to ecosystem.
- Ongoing Projects: She reflects on the sustained success of Code Newbie post-acquisition, highlighting the community’s ongoing activities and newly launched projects.
Not A Designer Initiative
- Concept and Purpose: Saon introduces her newsletter, Not A Designer, aimed at providing design insights to developers while documenting her own journey of learning design.
- Tools and Platforms: She discusses the use of Beehive for newsletter distribution and the benefits of its features in managing and growing her audience.
Discussion on Entrepreneurship and Lessons Learned
- Failed Attempts and Learning: Saon recounts her experiences with various failed product ideas, emphasizing the importance of emotional resilience and persistence in entrepreneurship.
- Pivoting Strategies: She reflects on the valuable feedback from investors and mentors, leading her to pivot her startup ideas and eventually focus on Disco and new initiatives like Big Cash Money community.
Perfect Picks Segment
- Saon’s Book Pick: Saon recommends "The Happiness Project" and discusses its impact on her approach to finding daily joy and happiness.
- Host’s Game Pick: The host shares his experience playing "It Takes Two" with his son, highlighting its unique cooperative gameplay and ability to bring them closer together.
",
+ "url": "https://codingcat.dev/podcast/not-a-designer-design-bites-for-devs",
+ "title": "Not a Designer: Design bites for devs",
+ "summary": "In the tech world, developers often find themselves stepping into the realm of design, a space that can feel both unfamiliar and daunting. Recognizing this challenge, Saron Yitbarek has crafted \"Not a Designer\" Design Bites, a series specifically tailored for developers who find themselves in need of design knowledge.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/91264acdb1751425fa717b1c76958687de9fc8aa-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-02-29T05:25:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "ZKdo3vLUqcb4sYreiCPARs",
+ "content_html": "In the fast-paced world of design, creating a seamless and efficient user experience is crucial. Design systems have emerged as a powerful solution, marrying art and science to ensure consistency and cohesiveness. In this episode, we explore the complexities of successful design systems and how they bridge the gap between product and consistency.
1. Atomic Design
Atomic design is a methodology with a corresponding book created by our guest, Brad Frost. It involves breaking down user interfaces into fundamental building blocks, enabling the creation of more complex components. Serving as a mental modal for crafting The five hierarchical levels of atomic design — atoms, molecules, organisms, templates, and pages — build upon each other, enabling the formation of more sophisticated elements. By adopting this methodology, organizations can seamlessly scale their products while ensuring a consistent approach throughout the development process. As the importance of implementing design systems becomes increasingly recognized, additional complexities emerge for organizations to navigate.
2. Design System Complexities
As organizations grasp the importance of design systems, new complexities surface. The challenges of maintaining consistency and scalability are often underestimated. The conventional perspective on a design system oversimplifies the connection between the design system and the product or products it impacts. The digital product landscape has evolved significantly and as organizations scale, the complexity of design systems increases.
Consistency and Standardization
The significance of maintaining consistency within user interfaces cannot be overstated. This involves adhering to a set of design principles and visual elements to ensure a cohesive and intuitive user experience. Additionally, standardizing components is essential for achieving a unified look and feel across different aspects of a product or platform.
Collaboration and Communication
Efficient collaboration and communication between design and development teams are crucial for successful product implementations. Bridging the gap between these disciplines enhances understanding, fostering a streamlined development process.
Scalability and Efficiency
Design systems serve as powerful tools for scalability in product development. Implementing efficient design systems streamlines workflows, saving time and resources. Balancing the challenge of scaling design systems to meet a growing organization's needs is critical for long-term success. Successful design systems deliberately move at a slower pace than the products they support, acknowledging that quality cannot be rushed. Build only what is needed and avoid complexity for the sake of complexity. While products often prioritize speed to meet business goals, design systems, as critical infrastructure, require a more deliberate and thoughtful approach to design and technical aspects.
Unpacking Complexity
Navigating these intricate challenges, organizations witness the expansion of design systems' responsibilities, tooling matures, processes and organizational structures evolve. A design system ecosystem can become a complex web of dependencies within a modern organization.
3. The Layer Cake Approach
The layer cake approach is a more accurate representation of the relationship between design systems and products. The design system ecosystem is a complex web of dependencies within a modern organization. Unpacking the layers of this ecosystem is essential for understanding the intricacies of design systems.
- The design system ecosystem
- Core Design System: At the foundation lies the core design system—a comprehensive set of design principles, guidelines, and components. This serves as the bedrock, providing a standardized framework for the entire organization.
- Technology-Specific Implementation: Building on the core, this layer involves the translation of design principles into technology-specific implementations. It encompasses the adaptation of design components to fit the technological landscape, ensuring seamless integration.
- Recipes: Recipes represent predefined solutions or patterns that encapsulate best practices. These are ready-made solutions derived from the core design system, offering efficiency and consistency in design and development.
- Smart Components: Smart components are dynamic elements that intelligently adapt to different contexts within products. They leverage the design system's principles and technology-specific implementations to enhance flexibility and responsiveness.
- Product: At the top layer sits the product—the tangible manifestation of the design system in action. Products draw from the layers below, incorporating design principles, technology-specific implementations, recipes, and smart components.
It's crucial to note that these layers are not universally applicable to every organization. Instead, this visual representation illustrates a mature enterprise design system, influenced by real-world experiences with some of the world's largest companies. Each layer adds depth, and organizations may find value in implementing only a fraction of the depicted layers. Flexibility is key, allowing organizations to tailor their design system to their specific needs and growth trajectories.
4. Looking Ahead to a global Design System
A Global Design System would improve the quality and accessibility of the world’s web experiences, save the world’s web designers and developers millions of hours, and make better use of our collective human potential. - Brad Frost, A Global Design System Addressing the existing problem of incompatible design systems, a global design system is proposed as a solution. The aim is to improve the quality and accessibility of web experiences, save time for designers and developers, and harness collective human potential. The proposal involves creating an open-source system governed by a community of designers and developers, emphasizing collaboration on a global scale.
The Problem
Despite the widespread adoption of design systems, compatibility issues persist, creating friction for designers and developers. The proposal advocates for the creation of a global design system to enhance the quality, accessibility, and usability of web experiences.
The Proposal
Advocating for a united effort to create a global design system, the proposal suggests making the system open source and governed by a community of designers and developers. The goal is to dramatically improve the quality and accessibility of web experiences, showcasing the potential of worldwide collaboration.
- Centralize common UI components
- Layer on top of HTML
- Accessible and front-end best practices baked in
- Unstyled and easily themeable
- Intuitive and easy to use
- Interoperable with other systems
- Internationalized and localized
- Composable and extensible
Where
Establishing the infrastructure for the global design system involves elements such as a code repository, code packages, a design library, and comprehensive documentation on a reference site.
How
The process of building a global design system includes initiating conversations, forming a working group, conducting research, planning the implementation, designing and building, releasing, iterating based on feedback, governing the system, and maintaining support for integration into the HTML specification.
Who
The initiative is open for contributions from individuals and is not owned by a specific company or organization. It is sponsored by various entities and supported by the community, including organizations like Open UI, W3C, and the Design Tokens Community Group.
5. Conclusion
Design systems are powerful tools for achieving consistency and scalability in product development. As organizations grow, the complexities of design systems increase, and the challenges of maintaining consistency and scalability are often underestimated. The layer cake approach provides a more accurate representation of the relationship between design systems and products, illustrating the intricate web of dependencies within a modern organization. A good baseline is to start with a solid foundation, using the layer cake approach, and scaling a design system as an organization grows. Start small and only add in complexity when needed. The proposal for a global design system aims to address the existing problem of incompatible design systems, advocating for a united effort to create a global design system. The goal is to dramatically improve the quality and accessibility of web experiences, showcasing the potential of worldwide collaboration.
",
+ "url": "https://codingcat.dev/podcast/4-8-design-systems-brad-frost",
+ "title": "Design Systems: From Atomic Design to a Global Solution",
+ "summary": "Dive into the world of creating and maintaining successful design systems, unpack the layer cake approach, and explore the potential future of a global design system.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/e30ed0fae3f33f21034b0235d98ebe493c7e63df-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-02-28T05:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "bc72d5c6-0286-4853-a62e-20662ba4ea51",
+ "content_html": "Summary
- 🎤 Introduction to Snyk: The video starts with an introduction to Snyk and its capabilities in secure coding. It highlights the importance of secure development practices and how Snyk aids in identifying and fixing vulnerabilities in code and open source dependencies.
- 🌐 Guest Introduction: Ryan Clark is introduced, sharing his background and experience in software development, including his work at Disney and Microsoft. His journey to becoming a developer advocate is discussed.
- 💻 Experience at Disney: Ryan shares insights from his time at Disney, emphasizing the importance of secure coding and how he got started with application security.
- 🚀 Transition to Microsoft: Discussion on Ryan's transition to Microsoft and his role in developer advocacy. He talks about the opportunities and challenges he faced while promoting secure coding practices.
- 🐱💻 Working with Snyk: The video dives into Ryan's current role at Snyk, discussing how Snyk integrates with various development environments and the benefits it offers to developers.
- 📈 Snyk's Capabilities: Detailed explanation of Snyk’s features, including its ability to scan for vulnerabilities in code, open source dependencies, and container configurations. The video showcases how Snyk can be integrated into CI/CD pipelines.
- 🧑🏫 Educational Aspect: The educational aspect of developer advocacy is highlighted, focusing on how developers can learn and improve their security practices through tools like Snyk.
- 🛠 Live Demo: A live demo of Snyk’s integration with Visual Studio Code is shown. The process of identifying and fixing vulnerabilities in a project using Snyk’s tools is demonstrated.
- 🔧 Practical Tips: Practical tips on using Snyk effectively, including setting up GitHub integrations and using Snyk CLI for deeper analysis.
Why Snyk
Secure coding is a critical aspect of software development that ensures applications are protected from vulnerabilities and attacks. Snyk is a powerful tool that helps developers identify and fix security issues in their code, open source dependencies, and container configurations. In this article, we will explore the key features of Snyk and how it aids in secure coding practices.
Understanding Snyk
Snyk is a developer-first security platform that seamlessly integrates with various development environments. It scans for vulnerabilities in code, open source libraries, and container images, providing actionable insights to developers. Snyk supports a wide range of programming languages, including JavaScript, Python, Java, and more.
Key Features of Snyk
- Vulnerability Scanning: Snyk scans your code and dependencies for known vulnerabilities. It provides detailed information on the nature of each vulnerability and how it can be exploited.
- Integration with Development Tools: Snyk integrates with popular development tools such as Visual Studio Code, GitHub, GitLab, and CI/CD pipelines. This allows developers to identify and fix vulnerabilities during the development process.
- Automatic Fixes: Snyk offers automated fixes for many vulnerabilities. It can open pull requests with the necessary changes to update vulnerable dependencies.
- Continuous Monitoring: Snyk continuously monitors your projects for new vulnerabilities, ensuring that you are always aware of potential security issues.
Practical Tips for Using Snyk
- Integrate Early: Integrate Snyk into your development environment early in the development process to catch vulnerabilities before they make it to production.
- Use CLI Tools: Snyk’s CLI tools provide deeper analysis and can be integrated into your CI/CD pipelines for automated security checks.
- Educate Your Team: Promote security awareness within your development team. Use Snyk’s educational resources to stay updated on the latest security practices.
- Regular Audits: Regularly audit your projects with Snyk to ensure that dependencies are up-to-date and free from vulnerabilities.
Conclusion
Snyk is an invaluable tool for developers who want to ensure the security of their applications. By integrating Snyk into your development workflow, you can identify and fix vulnerabilities early, reducing the risk of security breaches. Continuous learning and proactive security measures are essential for maintaining secure software development practices.
",
+ "url": "https://codingcat.dev/podcast/secure-coding-beyond-the-surface-with-snyk",
+ "title": "Secure Coding - Beyond the Surface with Snyk",
+ "summary": "Secure your code with Snyk. Explore coding best practices and vulnerabilities in this informative video. Join us for a deep dive. Watch, comment, and share!",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/6127d25fa882d60d49af6fb0a4cc236103b9652d-1280x720.jpg?rect=0,24,1280,672&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-02-26T17:59:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "ZKdo3vLUqcb4sYreiCP8ok",
+ "content_html": "Unleashing React's Potential for Data-Intensive Applications
Welcome back, Perfect Peeps! We're embarking on another coding adventure with our fuzzy feline friend, Coding Cat. Today's focus: Refine - an open-source framework supercharging React apps tailored for the enterprise.
We'll be diving deep into Refine's capabilities from the creator himself, building a real-world application, and unraveling the ice-covered crosses of React development. So grab your warmest sweaters and let's get cracking!
Enter Refine: A Framework for the Data-Driven Era
Refine, as introduced by Botwan, the tech lead at Refine, is a powerhouse for building data-intensive applications. It's specifically designed to streamline the creation of:
- Internal tools
- Admin panels
- Dashboards
It's an open-source masterpiece honed for the React framework. And it's supported by a community that's growing more robust by the day with:
- 20K GitHub stars
- 15K developers actively using Refine
- Over 10,000 Refine-based apps already deployed in production
The Backstory: A VC-backed Y Combinator Alumnus
Refine's backstory is as fascinating as the framework itself. It all started from a direct need within Botwan's team - the need for an improved approach to craft enterprise React apps that could handle escalating complexity with finesse.
And yes, Refine is a Y Combinator alumnus, a stamp of approval that speaks volumes in the startup arena. It was this combination of an urgent internal driver, robust VC backing, and a nod from YC that catalyzed Refine's growth beyond its initial iteration.
"What began as an internal tool has now transformed into a community-shared resource, thriving in the open-source spirit."
Refine's Headless Philosophy: Mix-and-Match Your UI
One of Refine's boldest moves was to go headless. This decision doesn't imply a ghostly setup - rather, it epitomizes versatility and quality.
Moving away from a one-size-fits-all model, Refine enables developers to blend their preferred UI frameworks - be it Material UI, Ant Design, Tailwind, or anything else - with its robust backend capabilities.
So Refine breaks free of the constraints of low-code platforms that often transform into hassles as complexity mounts. The visionary team behind Refine has experienced the shortcomings of such tools firsthand.
They've built something that fuses low-code speed without compromising long-term flexibility or manageability.
Getting Hands-On: From Zero to React Hero
Alright, time to get our paws dirty with some real code! Refine is a joy for live coding tutorials, which is exactly why we're gathered here today.
We'll not just talk about Refine but actually build with it - kickstarting a simple app to demonstrate Refine's fluidity for spinning up React solutions.
Let's initialize our Refine journey with their CLI:
Kickstart your app with Refine's CLI
Unknown block type \"code\", specify a component for it in the `components.types` option
It's delightful to see a new app come alive, and Refine's CLI artistry adds a dash of whimsy to the otherwise mundane terminal window.
However, Refine's true magic lies in how effortlessly it handles routing, data flows, and backend integrations - be it GraphQL or REST.
Here's a snippet showing Refine's GraphQL data handling:
Unknown block type \"code\", specify a component for it in the `components.types` option
With ease, you're now managing data via a polished UI. All thanks to Refine's fluid integration between your UI library and its refined (pun intended) core.
Authorization in a Snap: Authenticating with Ease
And what about the ever-critical realm of authentication? Not to worry - Refine's got you covered here too with a simplified approach:
Unknown block type \"code\", specify a component for it in the `components.types` option
With barely a dozen lines of code, we've patched authentication into our app! Of course, this omits necessary complexity for real-world security - but it exemplifies the speed at which Refine can incorporate auth.
The Conclusion: Refine's Versatile Promise
Refine is a postcard from the future of React development - showcasing the open-source community's drive to solve problems and share solutions.
The transcript from our Coding Cat session serves as a testament to how versatility, integration, and simplicity are more than just buzzwords for Refine's developers. They're the cornerstones.
So dive deep into React's capabilities with the confidence that Refine won't let you get lost in tangled webs. As we sign-off from our feline-hosted coding party, remember - Refine stands as an invitation to build, innovate, and openly share solutions in the collaborative spirit of advancement.
Happy coding, Perfect Peeps! May your React apps flourish under Refine's wings.
Explore Coding Cat for more on Refine's magic and other web dev marvels.
",
+ "url": "https://codingcat.dev/podcast/cwcc-1_refine",
+ "title": "Refine the open-source Retool for Enterprise",
+ "summary": "Build Internal Tools and Web Apps with Blazing Speed.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/bd1ce599334db95c5d1809b6b015624f422ddf2a-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-02-21T05:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "ZKdo3vLUqcb4sYreiCPAb4",
+ "content_html": "LocalStack provides a comprehensive platform for developing and testing AWS applications locally, offering a significant reduction in development time and AWS costs. It emulates over 80 AWS services, allowing developers to work efficiently without internet access or AWS cloud resources. The tool is designed for seamless integration into existing workflows, supporting favorite cloud development tools and enabling faster deployment and testing. It's widely appreciated in the developer community for enhancing productivity and simplifying cloud application development. For more detailed information, please visit LocalStack's website.
Unknown block type \"image\", specify a component for it in the `components.types` option
Unknown block type \"image\", specify a component for it in the `components.types` option
Unknown block type \"image\", specify a component for it in the `components.types` option
Unknown block type \"image\", specify a component for it in the `components.types` option
Unknown block type \"image\", specify a component for it in the `components.types` option
Unknown block type \"image\", specify a component for it in the `components.types` option
Unknown block type \"image\", specify a component for it in the `components.types` option
Unknown block type \"image\", specify a component for it in the `components.types` option
Unknown block type \"image\", specify a component for it in the `components.types` option
Unknown block type \"image\", specify a component for it in the `components.types` option
Unknown block type \"image\", specify a component for it in the `components.types` option
Unknown block type \"image\", specify a component for it in the `components.types` option
Unknown block type \"image\", specify a component for it in the `components.types` option
Unknown block type \"image\", specify a component for it in the `components.types` option
Unknown block type \"image\", specify a component for it in the `components.types` option
",
+ "url": "https://codingcat.dev/podcast/4-7-harness-the-power-of-localstack",
+ "title": "Master Offline AWS Development: Harness the Power of LocalStack!",
+ "summary": "Effortlessly Develop AWS Applications Offline with LocalStack: Your Ultimate Guide!",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/080cc7d50b421ea224c29b5e74d82f458b7fd785-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-02-20T05:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "ZKdo3vLUqcb4sYreiCP9PW",
+ "content_html": "GitHub Repo: https://github.com/CodingCatDev/amplify-gen2-nextjs-code-with-codingcatdev
Welcome back to Code with CodingCat.dev, where we dive into the ever-evolving world of coding with a casual vibe and some purr-fectly thought-out code. Today, we're joined by Eric, a developer advocate from the AWS Amplify team, to unpack the game-changing AWS Amplify Gen2 for building full-stack web and mobile apps. If you're navigating the high seas of the coding cosmos, looking for ways to build more with less and streamline your workflows, get comfy, and let's unravel the mysteries of Amplify Gen2 together.
Reconnecting at re:Invent
It's always a blast catching up at events like AWS re:Invent, where the community's passion for engineering and advocacy converges. It was especially thrilling for Eric, who, transitioning from UI engineering to developer advocacy, marched into re:Invent with a speaker badge and an eager heart. Post-re:Invent, the wind-down was a chance to deep dive into projects, create fresh content, and especially explore the new frontiers of Amplify Gen2.
Hobbies, Football, and Taylor Swift
Speaking of depth, besides the tech realm, there's the unbeatable mashup of football and music that stirs our guest Eric's soul. As a devoted San Francisco 49ers fan, the postseason was a rollercoaster ride paired with cheer for global icon Taylor Swift. Every game is a story of triumph and heartbreak, much like our journey as developers, where resilience is the name of the game.
What is AWS Amplify Gen2?
Amplify Gen2 is all about rethinking app creation. Those familiar with Amplify's CLI and Studio would appreciate the shift toward more code-centric workflows. It's about creating the right abstractions that spice up the developer experience. Take a leap into a .notation and TypeScript-first approach that straightforwardly ties your app to databases and more.
The Power of TypeScript and Dot Notation
With Amplify Gen2, TypeScript takes center stage, offering streamlined syntax and the intelligence of auto-complete and error-checking to ensure your backend talks smoothly with AppSync and the managed GraphQL service.
Unknown block type \"code\", specify a component for it in the `components.types` option
Embrace the richness of TypeScript and craft your backend like a maestro orchestrating a symphony.
Live Coding Session: Meal Planning App
Let’s roll up our virtual sleeves and jump into a live coding session. We’ll embark on crafting a meal planning app. The goal is to create a simple CRUD app with authenticated user access. We start from scratch, initializing a next app and methodically setting up our backend with Amplify Gen2.
Setting Up the Schema
Our meal planning app will revolve around a schema with items like breakfast, lunch, and dinner alongside their calorie counts. With Gen2, typing out the schema is a breeze, with authenticity checks and comprehensive data modeling features integrated seamlessly.
Unknown block type \"code\", specify a component for it in the `components.types` option
Hooks, States, and Server Actions
We delve into creating reactive components using hooks, managing states for meal entries, and carrying out server actions like submissions and deletions. Our components evolve with the interplay of client and server-side logic, leaving us with a fully functional, albeit basic, meal planner by the end of the session.
Debugging on the Fly
We're all too familiar with the occasional hiccups that pop up when diving into code. Our session is no stranger to these, as we troubleshoot real-time, lending authenticity to the experience. We navigate through issues like unresponsive delete buttons and state retention on refresh, all part of the raw charm of live coding.
Unknown block type \"image\", specify a component for it in the `components.types` option
The Might of Amplify
In our app, Amplify Gen2 flexes its muscles by streamlining DB actions, from data entry to deletions, all the while ensuring security with user authentication. As we rap up the meal planning app, its simplistic CRUD functionality stands testament to Amplify's prowess.
Wrapping Up and Deployment
Our coding dance comes full circle as we discuss pushing our local wonders to the cloud. With AWS Amplify, deploying is as simple as linking your GitHub repo and letting Amplify's continuous deployment take the wheel.
Console Wonders
Take a moment to appreciate the ephemeral environments Amplify conjures with each sandbox, a temporary realm where your project breathes before you decide its fate - to flourish in the production environment or vanish into the digital aether.
What's Next for AWS Amplify Gen2?
As we bid goodbye, remember that this is just the beginning for Amplify Gen2. The horizon is ablaze with potential features like storage and functions, taking the tool's capabilities to new zeniths. Until Amplify sheds its preview cloak, keep experimenting, staying tuned for updates, and, most importantly, keep coding!
If you've been hooked by our coding adventure and are itching for more deep dives into the coding universe with a laid-back touch, check out our plethora of posts, tutorials, and nifty tips over at CodingCat.dev. Until next time, stay curious and code on!
",
+ "url": "https://codingcat.dev/podcast/cwcc-1-2-aws-amplify",
+ "title": "Next.js Amplified: Full-Stack Web Apps on AWS Amplify Gen2",
+ "summary": "AWS Amplify using Gen2, a code first approach",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/be4ab3b7d1d31d0354fa6889511f4e172adcb40f-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-02-14T05:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "ZKdo3vLUqcb4sYreiCPAkG",
+ "content_html": "Top 10 Launches
- Fly Postgres: Managed Postgres offering by Supabase and Fly.io (benefits, key features)
- Supabase Grafana: Open-source observability suite (what it tracks, benefits for developers)
- pg_graphql supports Postgres Functions: Expanded capabilities for GraphQL users
- Python Libraries reach stable: Improved stability and features for Python developers
- Aggregate Functions in PostgREST: Enhanced data manipulation capabilities
- Supavisor 1.0: Scalable connection pooler for Postgres (performance improvements)
- Edge Functions with Node & NPM support: Broader developer options for serverless functions
- Leaked Password Protection: Security integration with Have I Been Pwned
- Supabase Branching: Streamlined development workflows
- Postgres Read Replicas: Improved scalability and disaster recovery
",
+ "url": "https://codingcat.dev/podcast/4-6-supabase-release-week-x-new-features",
+ "title": "Supabase New Release",
+ "summary": "Supabase dropped a lot of new features during their X launch, I review these with Jon.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/2d988a2d4124c96fc9da1c73fd32e6f3f25dacda-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-02-13T05:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "ZKdo3vLUqcb4sYreiCP978",
+ "content_html": "<script> import OpenIn from '$lib/components/content/OpenIn.svelte' </script> <OpenIn url="https://github.com/CodingCatDev/astro-starlight-code-with-codingcatdev" />
Starlight, a documentation theme from Astro, simplifies creating accessible, high-performance websites. Key features include:
- Content Support: Starlight is compatible with Markdown, MDX, or Markdoc formats, facilitating quick website creation.
- Customization Options: It offers dark and light color themes, full internationalization support for multilingual content, and built-in site search functionality.
- Component Flexibility: Starlight supports built-in components and also allows integration of components from various frameworks like React, Preact, Svelte, Vue, Solid, Lit, and Alpine.
- Customization Tools: The platform includes a color theme editor for easy customization, offering options for inspiration, pre-built themes, or brand-specific adjustments.
- Usability: Designed for both small weekend projects and mature products, Starlight aims to provide a high-performance, accessible solution for documentation needs.
For more detailed information and to get started with Starlight, visit their Getting Started page.
",
+ "url": "https://codingcat.dev/podcast/cwcc-1-building-docs-with-starlight-and-astro",
+ "title": "Building docs with Starlight and Astro",
+ "summary": "Chris teaches Alex how to use Starlight for docs.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/098510938a91e58b35d879525fa3171828adbfac-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-02-07T05:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "ZKdo3vLUqcb4sYreiCPAIg",
+ "content_html": "- Introduction of Project Crystal in NX: Project Crystal is a new feature in NX, revolutionizing how NX is used. It enhances the functionality of NX plugins, making them more like VS Code or WebStorm extensions, providing subtle but helpful features for developers.
- Automated Target Inference and Simplified Configuration: Project Crystal automatically infers targets within a project, significantly reducing the size of project configuration files. This simplification eases adoption, especially in large-scale enterprise environments, and keeps configurations in sync with actual config files, treating them as a single source of truth.
- Enhanced Developer Experience with NX Plugins: The update introduces enhanced developer tools, such as automatic configuration of caching inputs and outputs, and streamlined integration with existing CLI tools like VLI and Remix CLI. This aims to improve productivity and transparency in the development process.
- Easier Adoption and Migration Path: Project Crystal facilitates easier integration into existing setups, like PMPM mono-repositories, by allowing seamless addition of NX and its plugins. This provides a smoother migration path for projects transitioning to an NX plugin-based workspace.
- Advanced Testing and Distribution Features: The update introduces advanced testing capabilities, such as fine-grained file-level test targeting and the ability to distribute tests across multiple machines. This can significantly reduce testing times and costs, especially in larger environments, and is complemented by NX Cloud features like NX agents.
",
+ "url": "https://codingcat.dev/podcast/4-8-nx-project-crystal",
+ "title": "Nx - Project Crystal",
+ "summary": "Imagine a world where integrating Nx plugins into your workflow is as seamless as adding extensions to VSCode. That's the vision of Nx Project Crystal!",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/dd397fb605327c30582d305c5e39af48dadcc4b5-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-02-07T05:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "ZKdo3vLUqcb4sYreiCP9GK",
+ "content_html": "GitHub Repo: https://github.com/CodingCatDev/svelte-view-transitions-pokemon
SvelteKit View Transitions: A Guide to Enhancing User Experience
SvelteKit View transitions are a powerful tool that can be used to improve the user experience of your SvelteKit applications. They can make your app feel more polished and professional, and they can also help to guide users through your app's content.
This blog post will discuss what view transitions are, how they can be used, and how to set up and use the sveltekit-view-transition library.
What are view transitions?
View transitions are animations that occur when a user navigates between different views in your app. They can be simple fades or more complex animations that involve multiple elements.
Why use view transitions?
There are several reasons why you might want to use view transitions in your SvelteKit app:
- Improve the user experience: View transitions can make your app feel more polished and professional. They can also help to guide users through your app's content by making it clear when a new view is loading.
- Reduce perceived load times: By using view transitions, you can mask the loading time of new views, making your app feel faster and more responsive.
- Create a more engaging experience: View transitions can add a touch of personality to your app and make it more engaging for users.
How to use view transitions in SvelteKit
There are two main ways to use view transitions in SvelteKit:
- Using the sveltekit-view-transition library: The sveltekit-view-transition library is a popular library that makes it easy to add view transitions to your SvelteKit app. The library provides a number of pre-built transitions, and you can also create your own custom transitions.
- Using Svelte's built-in transition API: Svelte also has a built-in transition API that you can use to create view transitions. The transition API is more flexible than the sveltekit-view-transition library, but it also requires more code.
Setting up the sveltekit-view-transition library
To set up the sveltekit-view-transition library, you can add the following to your package.json file:
Unknown block type \"code\", specify a component for it in the `components.types` option
Once you have installed the library, you can import it into your app.js file:
Unknown block type \"code\", specify a component for it in the `components.types` option
Using view transitions in your components
You can use view transitions in your Svelte components by using the <ViewTransition> component. The <ViewTransition> component takes a number of props, including:
mode: The type of transition to use. The sveltekit-view-transition library provides a number of pre-built transitions, such as fade, slide, and zoom.duration: The duration of the transition in milliseconds.easing: The easing function to use for the transition.
Here is an example of how to use the <ViewTransition> component:
Unknown block type \"code\", specify a component for it in the `components.types` option
This code will fade in the MyComponent component when it is mounted.
Conclusion
View transitions can be a powerful tool for improving the user experience of your SvelteKit applications. By using view transitions, you can make your app feel more polished, professional, and engaging.
I hope this blog post has given you a good introduction to view transitions in SvelteKit. If you have any questions, please feel free to leave a comment below.
",
+ "url": "https://codingcat.dev/podcast/cwcc-1-3-svelte-view-transitions",
+ "title": "Simplyfing SvelteKit View Transitions with Paolo Ricciuti",
+ "summary": "sveltekit-view-transitions offers a straightforward approach for implementing these transitions without complexity, aiming to enhance the user interface with smooth and visually appealing changes between views.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/34f2211036f7229d80aca4446a3af20e7be99c9a-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-02-05T05:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "ZKdo3vLUqcb4sYreiCPAtS",
+ "content_html": "Questions
- What are the key benefits of using Raycast?
- How does Raycast's raycasting technology differ from other launchers?
- What are some of the most popular features of Raycast?
- How can I use Raycast to launch apps and games faster?
- How can I use Raycast to manage my app library?
- How can I customize the look and feel of Raycast?
- Are there any keyboard shortcuts for Raycast?
- How can I use Raycast to search for apps and games?
- How can I create custom shortcuts for apps and games in Raycast?
- What are some of the best tips and tricks for using Raycast?
Bonus question:
- What are the future plans for Raycast?
Raycast is a launcher for macOS that was released in 2021. It is designed to be fast, easy to use, and customizable. Raycast uses a raycasting technology to launch apps and games faster than other launchers. Raycast also has a number of other features, such as the ability to search for apps and games by name, keyword, or category, the ability to create custom shortcuts for apps and games, and the ability to manage your app library.
Raycast is available for free download from the Mac App Store. However, there is also a paid version of Raycast that includes additional features, such as the ability to use Raycast with multiple users, the ability to create custom themes for Raycast, and the ability to use Raycast with Alfred workflows.
Here are some of the key benefits of using Raycast:
- Speed: Raycast is the fastest launcher on the market. It uses a revolutionary new raycasting technology that allows it to launch apps and games faster than any other launcher.
- Ease of use: Raycast is incredibly easy to use. Simply type the name of the app or game that you want to launch and Raycast will find it and launch it instantly.
- Features: Raycast is packed with features, including:
- The ability to launch apps and games from your keyboard
- The ability to search for apps and games by name, keyword, or category
- The ability to create custom shortcuts for apps and games
- The ability to manage your app library
- The ability to customize the look and feel of Raycast
Here are some of the most popular features of Raycast
- Universal search: Raycast can search for apps, files, and folders on your Mac. You can also use Raycast to search for information online, such as the weather, news, and sports scores.
- Quick actions: Raycast allows you to create custom actions for apps and games. This allows you to launch apps and games with a single keystroke or mouse click.
- Extensions: Raycast has a wide range of extensions available, which allow you to add new features and functionality to Raycast. For example, there are extensions for launching apps and games from other launchers, such as Alfred and Spotlight, and extensions for managing your to-do list and tasks.
Here are some tips and tricks for using Raycast
- Use keyboard shortcuts: Raycast has a number of keyboard shortcuts that can help you to launch apps and games faster. For example, you can press
⌘+Space to open Raycast, and you can press Enter to launch the selected app or game. - Use extensions: The Raycast extension directory has a wide range of extensions available, which can add new features and functionality to Raycast. For example, there are extensions for launching apps and games from other launchers, such as Alfred and Spotlight, and extensions for managing your to-do list and tasks.
- Customize Raycast: Raycast is highly customizable. You can change the look and feel of Raycast, add new shortcuts, and create custom actions. You can also create custom themes for Raycast.
Useful links
- Raycast website: https://www.raycast.com/
- Raycast Mac App Store page: https://www.raycast.com/
- Raycast documentation: https://developers.raycast.com/
- Raycast community forum: https://www.raycast.com/community
- Raycast Twitter account: https://www.raycast.com/tonka3000/twitter
- Raycast YouTube channel: https://www.youtube.com/c/Raycast
",
+ "url": "https://codingcat.dev/podcast/4-4-raycast",
+ "title": "Raycast a launcher faster than F1!",
+ "summary": "Raycast: A lightning-fast launcher that makes your Mac more productive.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/1cf673a085537be26c6ea5e0e0089fe8188bfbe8-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-01-31T05:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "ZKdo3vLUqcb4sYreiCPB2e",
+ "content_html": "In this podcast we get to learn more about Taylor's journey in recruiting. Learn more about how Taylor now helps job seekers find a job faster and engineering leaders scale and hire more efficiently at Gun.io
- Introduction of Guest and Topic: Taylor Desseyn joins the CodingCat.dev podcast, hosted by Cloudinary, to discuss career development and personal growth. The show starts with Taylor commenting on the podcast's professional introduction and expressing a desire to improve his own podcast intros.
- Taylor's Diverse Roles and Music Background: Taylor describes his multifaceted role involving community building, product development, and marketing. He shares an anecdote from his youth about joining a band in 11th grade, leading to experiences like playing at notable Nashville venues and not capitalizing on networking opportunities.
- Transition to Recruiting and Tech Industry: After moving to Nashville and facing challenges in the music scene, Taylor shifts to recruiting, developing a passion for tech recruiting. He describes his journey in this field, including working on challenging projects like recruiting for Yahoo and learning about tech industry dynamics.
- Evolution of Career and Content Creation: Taylor discusses the evolution of his career, including a significant shift towards content creation in 2020. He reflects on his past in traditional recruiting and his transition to using digital platforms for engaging and sharing information.
- Networking Strategies and Advice for Job Seekers: The podcast concludes with Taylor offering insights into effective networking, especially in the tech industry. He emphasizes the importance of intentional conversations for career advancement and gives tips for job seekers, such as targeting specific companies and preparing thoroughly for interviews with major tech firms.
What is Gun.io?
Gun.io is a platform that offers access to top-tier technical talent, specializing in connecting businesses with skilled software engineers. Their process is streamlined to ensure a quick match between companies and vetted developers, who are selected based on their proven track record in the industry. The platform offers flexibility in engagements and emphasizes a quick hiring process, typically within a few days. Gun.io stands out for its global reach, having a network of developers skilled in popular programming languages and frameworks across various continents. They provide continuous support to ensure successful collaborations. For more details, you can visit their website at Gun.io.
What can Gun.io help with?
In today's digital economy, businesses increasingly rely on freelance developers to augment their teams and tackle specialized projects. Freelance platforms offer a streamlined way to connect with talented developers from around the world. But with so many options, how do you choose the right platform for your needs? This blog post will dive into the world of freelance developer platforms, offering insights to help you make informed decisions.
Understanding Freelance Developer Platforms
- What are they? Freelance developer platforms are online marketplaces that connect businesses with skilled software developers, designers, and other technical professionals. These platforms offer various features, including talent search, project management tools, secure payment systems, and often some form of quality control.
- Benefits:
- Access to Global Talent Pool: Platforms open doors to developers worldwide, not just those in your local area.
- Scalability: You can quickly find developers to match your project's specific skills and experience requirements.
- Cost-effectiveness: Freelance developers often charge less than traditional development agencies.
- Reduced Administrative Overhead: Platforms handle tasks like invoicing, payments, and sometimes even contract management.
Key Considerations When Choosing a Platform
- Talent Quality: How does the platform vet developers? Look for platforms with rigorous screening processes, reviews, and skill assessments. Some, like Gun.io, have senior developers making the evaluations.
- Specialization: Does the platform focus on specific areas of development (e.g., web development, mobile app development, data science)? Choose a platform aligned with your project's needs.
- Pricing Models: How do platforms charge? Options include hourly rates, fixed-price projects, or monthly subscriptions. Consider your budget and project scope.
- Communication and Collaboration Tools: Effective project management depends on clear communication. Evaluate the platform's built-in tools for messaging, file sharing, and task tracking.
- Support: Does the platform offer customer support for both businesses and developers? This can be crucial if issues arise during the project.
Popular Freelance Developer Platforms
- Gun.io: Known for its rigorous vetting process and focus on matching high-quality developers with businesses.
- Upwork: One of the largest freelance platforms, offering a wide range of talent and pricing options.
- Toptal: Specializes in connecting businesses with top-tier freelance developers.
- Fiverr: Offers a variety of freelance services, including development, at varying price points.
Tips for Success on Freelance Developer Platforms**
- Clearly Define Project Scope: Precise project requirements ensure you find the right developer and avoid misunderstandings.
- Set Realistic Expectations: Be upfront about your budget and timeline.
- Communicate Regularly: Open communication with your developer is crucial for project success.
- Start Small (If Possible): Test the waters by starting with a smaller project before committing to a larger engagement.
",
+ "url": "https://codingcat.dev/podcast/4-3-leveling-up-your-career-with-tdesseyn",
+ "title": "Leveling Up Your Career: Unleashing Your Full Potential!",
+ "summary": "Embark on a transformative journey of self-discovery and skill enhancement in 'Leveling Up Your Career,' where you'll unlock the secrets to unleashing your full potential and catapulting your professional success to new heights.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/80a9c7f1e7bcd5a2d24c707626afb433eb96f03b-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-01-24T05:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "LzgcvqoNSwz5R8loykbNx1",
+ "content_html": "Introduction
Compassionate Coding is a new approach to software development that prioritizes the humans involved in and affected by technology.
1. Background
Founding Story
After a decade of job-hopping around software engineering and technical leadership roles, April Wensel found herself burnout and looking for answers. Her personal growth journey led her to read books on compassion that resonated powerfully. In an effort to help other developers and the improve the industry as a whole, she founded Compassionate Coding in 2016 to bring more compassion, mindfulness, and social responsibility to software development.
Mission and Vision
The mission of Compassionate Coding is to bring more humanity into the software development process by connecting directly with developers through emotional intelligence training programs for teams and individuals.
2. The Compassionate Coding Philosophy
Self
Self-compassion means not beating ourselves up when we struggle. We can still pursue excellence while being compassionate with ourselves.
Collaborators
Compassion for our collaborators means empathizing with people who have different viewpoints. It also requires giving feedback in constructive ways.
Users
Compassion for users means creating good UX; it also means considering how technology affects users' mental health and overall well-being.
Society
In the widest sense, Compassionate Coding is about considering the how technology affects society and our planet.
3. Compassionate Coding the Company
Learning Resources
Compassionate Coding offers free blog posts and videos.
Community Building
The Compassionate Coding community stays connected through the newsletter.
Events and Workshops
April frequently speaks at events and on podcasts, and she offers customized training programs for companies. Common topics include emotional intelligence for engineers, communicating with assertive compassion, remote collaboration, and giving and receiving feedback.
4. Testimonials and Success Stories
Past Clients
Compassionate Coding provides training at software companies all over the globe, including some well-known social media, gaming, and search companies.
Participant Feedback
"I thought it was brilliant: pedagogically sound through its short exercises with different groups, very clear focus on takeaways, steering clear of being overwhelming, and a lot of fun to do. Keep up the good work!"
"I found this to be one of the most useful trainings on interpersonal relationships I have ever experienced.”
“Tangible ideas and evidence about EQ! Really liked how logical it was—very helpful.”
5. How You Can Get Involved
Sign up for the newsletter
There's a free Compassionate Coding newsletter.
Bring in a training for your team
Find out more about our team training options.
",
+ "url": "https://codingcat.dev/podcast/4-2-compassionate-coding",
+ "title": "Compassionate Coding: Emotional Intelligence for Engineers",
+ "summary": "Compassionate Coding is an approach to software development that combines compassion, mindfulness, and ethics to empower companies and individuals to create collaborative development environments to deliver more socially conscious products.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/9dfe1e4e41c08378e929d24032cd4920ae9be1f1-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-01-19T05:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "ZKdo3vLUqcb4sYreiCPBBq",
+ "content_html": "Navigating the Complexities of TypeScript: A Comprehensive Beginner's Guide
In the realm of programming languages, TypeScript stands out as a powerful tool that enhances the capabilities of JavaScript by introducing static typing. This feature promises to improve code reliability and readability, but mastering TypeScript can be a daunting task for beginners. This comprehensive guide aims to address the challenges of learning TypeScript and provide a structured approach to mastering its fundamentals.
Understanding the Complexity of TypeScript
The primary challenge in learning TypeScript lies in its departure from the dynamic nature of JavaScript. Programmers accustomed to JavaScript's flexible type system may find the strict type requirements of TypeScript initially overwhelming. This transition often leads to confusion and frustration, particularly when dealing with type errors and complex type annotations.
Furthermore, the abundance of TypeScript features, such as interfaces, generics, and type aliases, can be overwhelming for newcomers. Grasping the intricacies of these features and their proper application requires a deep understanding of TypeScript's core concepts and their practical implications.
Embracing the Power of Array and Tuple Types
Arrays and Tuples are fundamental data structures in programming, and understanding their usage in TypeScript is crucial. Arrays store collections of elements of the same type, while Tuples store fixed-size collections of elements with specific types.
Unknown block type \"code\", specify a component for it in the `components.types` option
Exploring the any, void, null, and undefined Types
Any type represents values of unknown or dynamically determined types. It's useful when dealing with external data sources or situations where type information is unavailable. The void type indicates the absence of a return value.
Unknown block type \"code\", specify a component for it in the `components.types` option
Harnessing the Power of Object Types
Object types define the structure of objects, specifying the properties and their associated types. This feature promotes type safety and enhances code maintainability.
Unknown block type \"code\", specify a component for it in the `components.types` option
Leveraging Optional and Utility Types
Optional types allow properties to be optional, providing flexibility in object structure. Utility types provide predefined functionalities for manipulating and modifying types.
Unknown block type \"code\", specify a component for it in the `components.types` option
Working with Interfaces, Generics, and Type Aliases
Interfaces define the structure of objects, ensuring consistent data structures across the application. Generics enable the creation of reusable components that work with different data types. Type aliases provide convenient names for complex type expressions.
Unknown block type \"code\", specify a component for it in the `components.types` option
Overcoming the Challenges of TypeScript
To effectively overcome the challenges of learning TypeScript, several strategies can be employed:
- Embrace a Structured Learning Approach: Utilize comprehensive TypeScript courses and tutorials, following along with exercises and carefully reviewing explanations.
- Practice Regularly: Consistent practice is crucial for solidifying TypeScript concepts. Engage in regular coding exercises and apply the newly acquired knowledge to personal projects.
- Seek Clarification When Needed: Don't hesitate to seek clarification when encountering difficulties. Consult online resources, engage in discussions with experienced TypeScript developers, and utilize online communities for support.
- Focus on Understanding Concepts: Prioritize comprehending the underlying concepts rather than merely memorizing syntax. This approach fosters a deeper understanding and facilitates the application of TypeScript in various scenarios.
- Be Patient and Persistent: Learning TypeScript requires patience and persistence. Embrace the challenges as opportunities for growth and celebrate each milestone along the way.
Conclusion
While TypeScript presents a learning curve, its benefits in terms of code robustness, maintainability, and reliability are undeniable. By adopting a structured learning approach, employing effective strategies, and utilizing comprehensive resources, beginners can navigate the challenges of TypeScript and harness its power to create high-quality software applications. Remember, consistent practice, a focus on understanding concepts,
",
+ "url": "https://codingcat.dev/podcast/4-1-typescript-is-freaking-hard",
+ "title": "TypeScript is freaking hard!",
+ "summary": "Join us as we delve into the fundamentals of TypeScript, a superset of JavaScript that elevates code quality and maintainability through static typing, in this comprehensive beginner's course.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/e39b2780e402821b829bfa52d358a0e7e1440080-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2024-01-01T05:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "ZKdo3vLUqcb4sYreiCP8fY",
+ "content_html": "Repo: https://github.com/CodingCatDev/cwcc-sass-supabase-nuxt-algolia
Creating a SaaS platform using Supabase, Nuxt, and Algolia is an exciting project that combines powerful technologies for a robust end product. Let's walk through the steps to set this up:
Setting up a Nuxt Project
First, you need to set up a Nuxt project. Nuxt is a powerful Vue.js framework that simplifies web development. You can create a new Nuxt.js project using the create-nuxt-app.
Unknown block type \"code\", specify a component for it in the `components.types` option
Follow the prompts to set up your project. For this example, we are using Supabase for our backend, so you can skip choosing an Axios module or any backend framework.
Installing Necessary Packages
To integrate Supabase with your Nuxt project, you need to install the Supabase JavaScript client.
Unknown block type \"code\", specify a component for it in the `components.types` option
Integrating Supabase with Nuxt
Connecting Nuxt to Supabase
Now, you will need to initialize Supabase in your Nuxt project. This can be done using Nuxt Modules.
Example: Authentication in Nuxt with Supabase
Below is a basic example of how you might handle user authentication in your application.
login.vue
Unknown block type \"code\", specify a component for it in the `components.types` option
Integrating Algolia with Nuxt
Connecting Nuxt to Aloglia
Now, you will need to initialize Algolia in your Nuxt project. This can be done using Nuxt Modules.
Example: Adding Search
This is a simple but very powerful feature to quickly add search into your frontend application.
algolia.vue
Unknown block type \"code\", specify a component for it in the `components.types` option
Add all features of a SaaS
Check the full video above to see how you can add Supabase and Algolia into a Nuxt framework and build an amazing application in a weekend. Dive in even further by building out your own custom functions with Supabase to push your database directly to Algolia.
",
+ "url": "https://codingcat.dev/podcast/cwcc-2023-12-20-building-your-own-sass",
+ "title": "Building your Own SaaS",
+ "summary": "Tim teaches Alex how to make your own SaaS using Supabase, Algolia and Nuxt.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/722f9b9e39ba3f6fae594361052ea606757ddd82-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2023-12-18T05:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "ZKdo3vLUqcb4sYreiCP8xw",
+ "content_html": "Introduction
Welcome to a live coding session where we build a React Native mobile app from scratch using Expo! My name is Alex and I'm joined by special guest Simon Grimm, an expert in React Native and mobile development.
In this extensive blog post, we will:
- Set up a React Native development environment with Expo
- Create UI components like buttons, images, links, scroll views etc.
- Implement routing and navigation between screens
- Fetch data and display it in the UI
- Open the camera and image library to select photos
- Deploy the app to mobile devices using Expo Go
So let's get started!
Full Solution
Setting up the Development Environment
First, we need to set up a React Native environment on our machines using Expo.
Unknown block type \"code\", specify a component for it in the `components.types` option
This scaffolds a new React Native project for us called cat-app.
Inside the project, we have an App.js file along with other config files. This App.js renders the entry component for our app.
We can now start the Expo development server:
Unknown block type \"code\", specify a component for it in the `components.types` option
And voila! The app opens up in the iOS Simulator. Later, we'll also install the Expo Go app on an actual mobile device.
Unknown block type \"image\", specify a component for it in the `components.types` option
Creating the Home Screen
Let's start building out the Home screen:
Displaying the Cat Images
We want to fetch some cat images from an API and display them on the Home screen.
First, we fetch the cat images:
Unknown block type \"code\", specify a component for it in the `components.types` option
Then we store them in React state and display them in a ScrollView:
Unknown block type \"code\", specify a component for it in the `components.types` option
This will display the cat images in a nice scrollable view.
We also added a "Load Cats" button to trigger fetching images:
Unknown block type \"code\", specify a component for it in the `components.types` option
Adding Navigation Between Screens
We want to navigate to a Details screen when a cat image is tapped.
Expo Router makes navigation really easy. We simply wrap the Image component in a Link to link to the Details screen:
Unknown block type \"code\", specify a component for it in the `components.types` option
Then we create a details.js screen that loads the specific cat details:
Unknown block type \"code\", specify a component for it in the `components.types` option
When you tap a cat image now, it seamlessly navigates to the Details screen with the tapped cat image loaded!
Opening the Camera with Expo
Finally, let's allow users to take or select photos using the camera. Expo makes accessing native device capabilities like camera super easy.
We just install the expo-image-picker package:
Unknown block type \"code\", specify a component for it in the `components.types` option
And import it in our Modal screen:
Unknown block type \"code\", specify a component for it in the `components.types` option
That's it! We can now open up the phone's image library, select images and handle them in our React Native code.
Conclusion
In this post, we:
- Created a React Native app with Expo CLI
- Built out Home, Details and Modal screens
- Added routing and navigation with Expo Router
- Fetched and displayed data from an API
- Opened up the camera picker using Expo APIs
As you can see, Expo simplifies React Native development tremendously allowing us to focus on the app code rather than native platform intricacies.
I hope you enjoyed this post and got an idea of building mobile apps with React Native and Expo! Let me know in Discord if you have any other questions.
",
+ "url": "https://codingcat.dev/podcast/cwcc-1-expo-react-native",
+ "title": "Expo from a Web Developers View",
+ "summary": "Dive into the seamless world of cross-platform app development as we unravel the magic of React Native with Expo, combining power and simplicity for an unparalleled mobile experience.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/343e2649e927e4e44c23609ae4ebaca2ad2b05d0-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2023-12-18T05:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "ZKdo3vLUqcb4sYreiCPBL2",
+ "content_html": "All the swags
Dont worry you have landed in the right place, for our event we will be giving away lots of free stuff this year!
The peeps from StickerMule have hooked us up with lots of stickers to give away to all of our awesome members so don't forget click on their logo above and make some swag for your own side projects!
You can fill out the below form and Alex will send you some stickers!!
Unknown block type \"image\", specify a component for it in the `components.types` option
<SwagForm />
Rough Outline of our talk
January:
Netlify buys Gatsby. Astro 2 Takes Flight. React Native vibe check. February - March:
Deno's Dilemma. The Ng-Renaissance Fair. New Year's Reso-Vue-tions (Vue goals and objectives for 2023). April - May:
Dart version 3 with WebAssembly compilation preview. Angular's bold new look. Modern.js and Node 20 releases. June - July:
React Server Components in the wild. The Officially Unofficial Svelte 4 Launch Party™. I Will Rem-Ember.js You. August - September:
Astro amps it up. Jamstack is dead, long live Jamstack!. Oops, Svelte did it again. October - November:
Angular goes meta. React Forgettiboutittt. WinterJS is coming. December:
Astro launches v4. The Ang-éissance. Real talk about The Edge™.
Unknown block type \"twitter\", specify a component for it in the `components.types` option
",
+ "url": "https://codingcat.dev/podcast/3-holiday-special",
+ "title": "CodingCat.dev Podcast - Holiday Special Live December 20 9a ET",
+ "summary": "Reviewing things that happened in 2023 and generally just having a fun holiday episode",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/778341ddfe02498b535de1cbad23abcb61fcaef5-960x540.png?rect=0,18,960,504&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2023-12-17T05:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "ZKdo3vLUqcb4sYreiCPD4I",
+ "content_html": "In the dynamic realm of software development, open source projects have emerged as a driving force behind innovation and collaboration. OpenSauced, a pioneering platform, empowers developers to seamlessly navigate the open source landscape, fostering meaningful contributions and enriching their professional journeys. Or is it just about pizza 🤷♂️, I have no idea that is why BekahHW is here to explain it all to us!
Unlocking the Power of Open Source Contributions
OpenSauced stands as a beacon for developers seeking to engage in the world of open source contributions. Its user-friendly interface and intuitive tools guide developers through the process of identifying suitable projects, making meaningful contributions, and showcasing their expertise to the world.
Identifying Hot Repositories
The platform meticulously curates a list of trending open source repositories, enabling developers to discover projects that align with their skills and interests. This curated selection eliminates the time-consuming task of manually searching for relevant projects, allowing developers to focus on making impactful contributions.
Generating Shareable Highlights
OpenSauced empowers developers to amplify their contributions by generating shareable badges that can be proudly displayed on resumes, portfolios, and social media profiles. These badges serve as a testament to a developer's commitment to open source, enhancing their visibility among potential employers and collaborators.
Connecting with Open Source Companies
OpenSauced bridges the gap between developers and companies that actively embrace open source practices. It maintains a comprehensive database of open source-centric organizations, providing developers with direct access to employment opportunities and fostering meaningful connections within the industry.
Features that Empower Developers
OpenSauced's arsenal of features extends beyond project discovery and contribution support. It offers a plethora of tools that empower developers to learn, grow, and connect with like-minded individuals.
A Knowledge Hub for Open Source Enthusiasts
The platform's blog serves as a repository of insightful articles and tutorials, covering a wide spectrum of open source topics. From beginner-friendly guides to in-depth technical discussions, the blog caters to developers of all skill levels, fostering continuous learning and enabling them to stay abreast of the latest open source trends.
A Vibrant Community Forum
OpenSauced cultivates a thriving community of developers through its interactive forum. This virtual space provides a platform for developers to exchange ideas, seek guidance, and collaborate on projects, fostering a sense of camaraderie and shared passion for open source. All wrapped up in a nice GitHub stromboli.
A Gateway to Open Source Job Opportunities
OpenSauced consolidates open source job postings from various sources, offering developers a centralized platform to explore potential career paths. This comprehensive job board streamlines the job search process, enabling developers to seamlessly identify positions that align with their skills and aspirations.
Transforming the Developer Landscape
OpenSauced has revolutionized the way developers engage with open source, empowering them to make meaningful contributions, enhance their skills, and expand their professional networks. Its impact extends far beyond individual developers, shaping the very landscape of open source collaboration.
Fostering Innovation through Diversified Contributions
By empowering a wider range of developers to contribute to open source projects, OpenSauced has diversified the pool of contributors, bringing fresh perspectives and innovative ideas to the table. This diversity of thought and expertise has accelerated the pace of innovation within open source projects, leading to groundbreaking advancements in software development.
Strengthening the Open Source Community
OpenSauced has fostered a stronger sense of community among open source developers. By providing a centralized platform for collaboration, learning, and networking, the platform has bridged geographical and cultural divides, fostering a sense of shared purpose and camaraderie among open source enthusiasts.
Shaping the Future of Open Source
OpenSauced's commitment to empowering developers and nurturing open source communities positions it as a driving force shaping the future of software development. By fostering a culture of collaboration and continuous learning, OpenSauced is poised to propel open source to even greater heights, driving innovation and shaping the software landscape for years to come.
Conclusion
OpenSauced stands as a testament to the transformative power of open source collaboration. By empowering developers to make meaningful contributions, fostering a vibrant community, and providing access to a wealth of resources, OpenSauced has democratized open source participation, paving the way for a future where innovation and collaboration thrive.
",
+ "url": "https://codingcat.dev/podcast/3-25-your-fast-track-to-open-source",
+ "title": "OpenSauced: Your fast track to open source",
+ "summary": "OpenSauced is a platform that helps developers find, contribute to, and learn about open source projects.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/6dad0f7e032c1029f6e94198ec0df9883bae49f4-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2023-12-13T05:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "LzgcvqoNSwz5R8loykbN6l",
+ "content_html": "<script> import OpenIn from '$lib/components/content/OpenIn.svelte' </script> <OpenIn url="https://github.com/CodingCatDev/code-with-codingcat-meltui" />
Melt UI is an open-source collection of component builders designed for creating user interfaces with the Svelte framework. It stands out for its accessibility and customization features, enabling developers to build user interfaces that are both accessible and uniquely styled. Here are some key aspects of Melt UI:
- Builder API: Unlike traditional component libraries, Melt UI offers builders instead of components. These builders are functions that generate a collection of properties that can be assigned to any element or component, providing great flexibility.
- Accessibility: Accessibility is a primary focus of Melt UI. It follows WAI-ARIA design patterns, ensuring that UI components are inclusive and user-friendly. This includes attention to aria attributes, role management, focus handling, and keyboard navigation.
- Style Customization: Melt UI comes without predefined styles, allowing developers to apply their own styling to integrate seamlessly with their application's design system. This makes it compatible with various styling approaches like vanilla CSS, CSS preprocessors, or CSS-in-JS libraries.
- Open and Extensible: The architecture of Melt UI is open and flexible, allowing for customization and extension of components. This supports adding event listeners and props to tailor components to specific needs.
- Simplified Development Experience (DX): Melt UI aims to simplify the development workflow. Its components are uncontrolled by default, relieving developers from managing local states. However, there's also support for controlled components for those who prefer it.
- TypeScript and SvelteKit Support: Melt UI provides a fully typed API, promoting a consistent and familiar experience across components. It is also built with server-side rendering (SSR) in mind, making it suitable for SvelteKit.
- Community-Driven: Being an open-source project, Melt UI is developed and maintained by a community of contributors.
Melt UI is still in its early stages, so developers might expect breaking changes in minor releases until a stable version is released.
For more detailed information and examples, you can visit the Melt UI website and its GitHub repository.
",
+ "url": "https://codingcat.dev/podcast/cwcc-1-2-meltui",
+ "title": "Open Source Svelte Magic: Melting Minds through Melt UI!",
+ "summary": "Thomas teaches Brittney and Alex how to use Melt and how Runes can be used to improve builders.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/b54283206d1510a777eee9a02ef2eef0c1731ef2-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2023-12-11T05:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "ZKdo3vLUqcb4sYreiCPDDU",
+ "content_html": "In the rapidly evolving realm of artificial intelligence, large language models (LLMs) have emerged as powerful tools for natural language processing and generation. These models, trained on massive datasets of text and code, have demonstrated remarkable capabilities in tasks such as machine translation, text summarization, and creative writing. However, the development and utilization of LLMs raise significant privacy concerns, particularly with regard to the handling of sensitive personal information.
Skyflow's Privacy Vault offers a groundbreaking solution to address these concerns, enabling organizations to build and deploy LLMs while upholding the highest standards of data privacy and security. This innovative privacy vault provides a secure environment for sensitive data, ensuring that it remains protected throughout the entire LLM lifecycle, from data collection and preparation to model training and deployment.
Safeguarding Sensitive Data Throughout the LLM Lifecycle
The LLM lifecycle encompasses various stages, each presenting unique data privacy challenges. Skyflow's Privacy Vault effectively addresses these challenges, ensuring that sensitive data is safeguarded at every step.
- Data Collection and Preparation: During data collection, Skyflow's Privacy Vault allows organizations to identify and redact sensitive data before it is used for training or inference. This process helps prevent the inadvertent exposure of personally identifiable information (PII) or other sensitive information.
- Model Training: The Privacy Vault maintains its protection during model training, ensuring that sensitive data remains encrypted and inaccessible to unauthorized parties. This encryption safeguards sensitive data from potential breaches or unauthorized access during the training process.
- Model Deployment and Inference: When deployed for inference, LLMs interact with user-provided data. Skyflow's Privacy Vault extends its protection to this stage, ensuring that sensitive data is redacted or anonymized before being exposed to the LLM. This protection prevents the LLM from learning or disclosing sensitive information during inference.
Enhancing Data Privacy with Granular Controls and Compliance
Skyflow's Privacy Vault goes beyond basic data protection by providing organizations with granular controls over data access and usage. These controls enable organizations to define who can access sensitive data and for what purposes, ensuring that sensitive information is only used for authorized purposes.
Furthermore, the Privacy Vault facilitates compliance with various data privacy regulations, such as the General Data Protection Regulation (GDPR) and the California Consumer Privacy Act (CCPA). The vault's data residency capabilities ensure that sensitive data remains within the specified geographic regions, complying with data localization requirements.
Unlocking the Potential of LLMs with Confidence
Skyflow's Privacy Vault empowers organizations to harness the power of LLMs without compromising data privacy. By providing comprehensive data protection and compliance capabilities, the Privacy Vault enables organizations to build and deploy LLMs responsibly, fostering trust and transparency among stakeholders.
Key Benefits of Skyflow's Privacy Vault
- Protect sensitive data throughout the LLM lifecycle
- Implement granular access controls for sensitive data
- Comply with data privacy regulations, such as GDPR and CCPA
- Maintain data residency in specified geographic regions
- Build and deploy LLMs responsibly and ethically
Conclusion
As LLMs continue to revolutionize various industries, Skyflow's Privacy Vault plays a crucial role in ensuring that these powerful models are developed and deployed in a privacy-conscious manner. By safeguarding sensitive data and enabling compliance with data privacy regulations, the Privacy Vault empowers organizations to leverage the full potential of LLMs while upholding the highest standards of data protection.
",
+ "url": "https://codingcat.dev/podcast/3-24-skyflow-privacy-vault",
+ "title": "Skyflow's privacy vault for building LLMs",
+ "summary": "Skyflow's Privacy Vault empowers organizations to build and deploy LLMs responsibly, ensuring data privacy and compliance throughout the entire LLM lifecycle.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/13b3650dfe32395065b2e91425e0046e9f1dbb93-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2023-12-06T05:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "ZKdo3vLUqcb4sYreiCPDMg",
+ "content_html": "Angular v17 is here, and it's the purrfect update for all you cat-loving developers. This new version of the popular JavaScript framework is packed with features that will make your development purr like a kitten. With us today is Emma Twersky a Senior Developer Relations Engineer, to talk all about the new updates.
Deferrable Views
One of the biggest new features in Angular v17 is deferrable views. This means that you can now tell Angular to wait to render a view until it's actually needed. This can be a huge performance improvement, especially for views that are rarely seen.
Unknown block type \"image\", specify a component for it in the `components.types` option
For example, if you have a view that only shows up when the user clicks a button, you can defer it until the button is clicked. This will save Angular from having to render the view unnecessarily, which can improve the performance of your application.
Built-in Control Flow Loops
Another great new feature in Angular v17 is built-in control flow loops. This means that you can now use for and while loops in your Angular templates. This can make your templates more concise and easier to read.
For example, the following code snippet shows how to use a for loop to iterate over an array of cats:
Unknown block type \"code\", specify a component for it in the `components.types` option
This code will loop over the cats array and print out the name of each cat.
Up to 90% Faster Runtime
Angular v17 is also up to 90% faster than previous versions. This means that your applications will run faster than ever before.
The performance improvements in Angular v17 are due to a number of factors, including the new deferrable views feature and the built-in control flow loops.
Up to 87% Faster Builds
In addition to being faster at runtime, Angular v17 is also up to 87% faster to build. This means that you can get your applications up and running faster than ever before.
Unknown block type \"image\", specify a component for it in the `components.types` option
The build performance improvements in Angular v17 are due to a number of factors, including the new Ivy compiler and the improved TypeScript support.
New MDC-based Angular Material Components
Angular v17 also includes new MDC-based Angular Material components. These components are based on the Material Design Components (MDC) library, which is a collection of high-quality components that are designed to be beautiful, accessible, and performant.
The new MDC-based Angular Material components are a major improvement over the legacy components. They are more consistent with the Material Design specification, and they are also more performant. You can find the migration guide here.
Deprecated Legacy Components
The legacy Angular Material components have been deprecated in Angular v17. This means that they will be removed in a future version of Angular.
If you are still using the legacy Angular Material components, you should update your applications to use the new MDC-based components as soon as possible.
Conclusion
Angular v17 is a major release that brings a number of significant improvements to the framework. If you are a developer who uses Angular, you should definitely upgrade to v17 as soon as possible.
P.S. Don't forget to check out the new Angular website at angular.dev!
",
+ "url": "https://codingcat.dev/podcast/3-23-angular-17",
+ "title": "Angular.dev",
+ "summary": "Angular.dev is the future home for Angular developers.",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/54f923647199c5ed4e257a81ddbae1ace404e5ad-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2023-11-29T05:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ },
+ {
+ "id": "ZKdo3vLUqcb4sYreiCPDf4",
+ "content_html": "In today's rapidly evolving digital landscape, businesses are constantly seeking ways to enhance their online presence, improve application performance, and deliver seamless user experiences. Enter edge computing, a transformative technology that brings computational resources closer to the end-users, enabling faster data processing, reduced latency, and enhanced scalability. Fly.io emerges as a frontrunner in this domain, providing a powerful JavaScript platform that empowers developers to build, deploy, and manage edge applications with unparalleled ease and efficiency.
Interview with Dov from Fly.io
I was lucky enough to sit down and chat with Dov to learn more about his background and Fly.io.
"Fly.io has a lot of really powerful primitives if you want to build a platform." - Dov
Dov's Background
Dov actually started working with Fly.io through open source! He created a Terraform provider for their platform which brought him to their attention. Really cool example of open source kickstarting a career.
Some key things about Dov:
- Platform Engineer at Fly.io focused on infrastructure.
- Got into tech through freelance work across React, Node and some Go.
- Learned a lot through open source contributions.
- Currently getting a formal CS degree to complement his experience.
Why Fly.io for Frontend Devs?
When your app is running closer to users, it improves performance since requests don't need to travel as far. Let's think through an example...
Say you build an amazing new social media app called CodingCat Social. It starts to gain traction and users from all over the world begin signing up. If your back end was only running in a single region like US-East, any requests from Africa would need to make a long round trip before getting a response.
With Fly.io, you could deploy your containerized app to regions close to your users. Now when someone in Hong Kong goes to load their feed, it will pull from a local instance rather than one halfway across the world!
How it Works
Fly.io makes it easy to deploy docker containers globally. Under the hood they utilize:
- Firecracker microVMs
- A network overlay for communication
- Automated services like PostgreSQL databases in regions
- Easy scaling to add instances via CLI
There are also partnerships in the work to offer fully managed databases and other data services all running on Fly.io's infrastructure. Pretty cool!
CI/CD and Integrations
Since Fly.io uses the standard Docker tooling, it works nicely with CI/CD pipelines. Dov showed an example deploying a Remix app through GitHub Actions. It uses the Fly CLI and the syntax looks identical to local commands.
Here is what that workflow looks like:
- Make code changes and push to GitHub
- GitHub Actions runs tests and builds docker image
- Use Fly CLI to deploy freshly built container
- Image runs on Fly.io edge closest to you
They also have integrations for other tools like Terraform if you want to manage infrastructure as code. Overall, it seems pretty straightforward to get up and going!
Revolutionizing Content Delivery with Fly.io's Global Edge Network
At its core, Fly.io is a global edge network that transforms containers into micro-VMs, strategically distributed across 30+ regions on six continents. This extensive network enables developers to deploy their applications closer to their target audience, significantly reducing latency and ensuring lightning-fast content delivery. Whether you're serving static content, dynamic web applications, or APIs, Fly.io ensures that your users receive content with minimal delay, regardless of their geographical location.
Learn more about Fly.io's global edge network
Harnessing the Power of JavaScript for Edge Application Development
Fly.io differentiates itself from other edge computing platforms by embracing JavaScript as its primary language. This choice empowers developers to utilize their existing JavaScript skills and seamlessly transition to edge application development. With Fly.io, developers can write, test, and deploy their applications locally using familiar JavaScript frameworks and tools, eliminating the learning curve associated with new programming languages or complex infrastructure setups.
Discover Fly.io's JavaScript-centric approach
Simplified Deployment and Management for Effortless Edge Operations
Deploying and managing edge applications can often be a daunting task, requiring specialized expertise and intricate configurations. Fly.io simplifies this process by providing a user-friendly interface and intuitive command-line tools. Developers can effortlessly deploy their applications to any region in the world with just a few clicks or commands, ensuring a smooth and hassle-free deployment experience.
Learn how to deploy and manage edge applications with Fly.io
Unleashing the Scalability of Edge Applications
As user demand fluctuates, edge applications must adapt accordingly. Fly.io's auto-scaling capabilities dynamically adjust the number of micro-VMs allocated to each application based on real-time traffic patterns. This ensures that your applications can handle spikes in demand without performance degradation or downtime, providing a consistent and reliable user experience.
Explore Fly.io's auto-scaling capabilities
Ensuring Security at the Edge
In today's interconnected world, security remains paramount. Fly.io prioritizes security by employing a robust network architecture and implementing industry-standard security protocols. This safeguards your applications and data from unauthorized access, ensuring that your users' privacy and sensitive information are protected.
With Fly.io, you can rest assured that your cat website is safe and secure. Your cat photos and videos will be protected from prying eyes, and your users' personal information will be kept confidential.
Read more about Fly.io's security measures
Real-World Applications of Fly.io
Fly.io's versatility and ease of use have made it a popular choice for a wide range of applications. From high-traffic websites and e-commerce platforms to real-time gaming experiences and IoT applications, Fly.io empowers developers to build and deploy edge applications that deliver exceptional performance and scalability.
In addition to deploying cat websites, Fly.io can be used to build a variety of other applications, such as:
- Live streaming video chat platforms
- Real-time gaming applications
- IoT applications
- E-commerce platforms
- High-traffic websites
Explore some real-world use cases of Fly.io
Fly.io: A Catalyst for Innovation in the Edge Computing Era
As edge computing continues to revolutionize the digital landscape, Fly.io stands as a powerful platform that empowers developers to create innovative and high-performance edge applications. With its global edge network, JavaScript-centric approach, simplified deployment and management, auto-scaling capabilities, and robust security measures, Fly.io is poised to play a pivotal role in shaping the future of edge computing.
Wrap Up
That summarizes the key points from my chat with Dov. Fly.io is an interesting platform for running applications globally at the edge for better performance. If your users are distributed, it's worth checking out!
I want to thank Dov again for joining me. Make sure to Follow Fly.io on Twitter and keep tabs on their new offerings coming soon including GPU instances and managed data services.
Until next time, happy coding!
",
+ "url": "https://codingcat.dev/podcast/3-22-fly-io-with-dov-alperin",
+ "title": "Speedrun Your App Onto Fly.io",
+ "summary": "Fly is a platform for running full stack apps and databases close to your users",
+ "image": "https://cdn.sanity.io/images/hfh83o0w/production/8a10ce4a328dc24b0b795fb2238d9b81049635a3-1920x1080.png?rect=0,36,1920,1008&w=1200&h=630&fit=max&auto=format",
+ "date_modified": "2023-11-15T05:00:00.000Z",
+ "author": {
+ "name": "Alex Patterson",
+ "url": "https://codingcat.dev/author/alex-patterson"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/apps/site/baseline/podcasts_rss.xml b/apps/site/baseline/podcasts_rss.xml
new file mode 100644
index 000000000..e0e830ff5
--- /dev/null
+++ b/apps/site/baseline/podcasts_rss.xml
@@ -0,0 +1,1418 @@
+
+
+
+ CodingCat.dev Podcast
+ https://codingcat.dev/podcasts
+ The CodingCat.dev Podcast features conversations about web development, design, and technology with industry experts and community members.
+ en
+ Tue, 18 Aug 2026 13:50:22 GMT
+
+ All rights reserved 2026, CodingCat.dev
+ Alex Patterson
+
+ Alex Patterson
+ alex@codingcat.dev
+
+
+
+ false
+ episodic
+
+ https://codingcat.dev/icon.svg
+ CodingCat.dev Podcast
+ https://codingcat.dev/podcasts
+
+ -
+ GenAI for Engineers, What's Real, What's Not and What's Coming
+ https://codingcat.dev/podcast/genai-for-engineers-what-s-real-what-s-not-and-what-s-coming
+ 292601cc-5365-4644-9b34-82e67d033e4b
+ Wed, 19 Nov 2025 13:10:00 GMT
+
+ Alex Patterson
+
+ GenAI for Engineers, What's Real, What's Not and What's Coming
+ Alex Patterson
+
+ 5
+ 13
+ full
+ 00:45:09
+ false
+ <p>Curious about GenAI for engineers? Learn what's real, what's not, and what's next in AI. Watch for expert insights and share your thoughts below!<a href="https://codingcat.dev/podcast/genai-for-engineers-what-s-real-what-s-not-and-what-s-coming" target="_blank" rel="noopener noreferer">https://codingcat.dev/podcast/genai-for-engineers-what-s-real-what-s-not-and-what-s-coming</a>00:00 Introduction02:21 Startup Journey10:13 GenAI Evolution13:24 Engineering Excellence16:44 AI Job Impact25:11 New Product Launch29:08 Distributed Teams32:20 Future of GenAI43:27 Key Takeaways</p>
+
+
+
+ -
+ How OAuth, MCP, and the OpenAI Apps SDK, Power the Next Generation of Interactive AI Experiences (with Stytch & OpenAI)
+ https://codingcat.dev/podcast/how-oauth-mcp-and-the-openai-apps-sdk-power-the-next-generation-of-interactive-ai-experiences
+ ab22f27a-2d5d-4dfa-ad61-b5dd9ef85334
+ Tue, 04 Nov 2025 18:01:00 GMT
+
+ Alex Patterson
+ How OAuth, MCP, and the OpenAI Apps SDK, Power the Next Generation of Interactive AI Experiences (with Stytch & OpenAI)
+ Alex Patterson
+
+ 5
+ 12
+ full
+
+ -
+ Firebase SQL with Data Connect
+ https://codingcat.dev/podcast/firebase-sql-with-data-connect
+ 7fffc615-ed5a-491a-a5f3-e1dcf3338612
+ Tue, 28 Oct 2025 19:48:00 GMT
+
+ Alex Patterson
+
+ Firebase SQL with Data Connect
+ Alex Patterson
+
+ 5
+ 11
+ full
+ 00:48:35
+ false
+ <p>Discover Firebase Data Connect with Tyler Crowe! Dive into SQL, GraphQL, AI, and live demos to supercharge your app's data. Share your thoughts below!<a href="https://codingcat.dev/podcast/firebase-sql-with-data-connect" target="_blank" rel="noopener noreferer">https://codingcat.dev/podcast/firebase-sql-with-data-connect</a>00:00 Data Connect Overview04:29 AI Features & Vertex13:13 SQL Views & Raw SQL15:45 Downloading SDKs24:37 Full Text Search30:32 Vector Search Explained38:20 GraphQL Insights47:43 Firestore vs Data Connect48:32 Conclusion</p>
+
+
+
+ -
+ Identity in AI Agents
+ https://codingcat.dev/podcast/identity-access-management-for-agents-with-tobin-south
+ f78ab69a-69a7-4e88-9496-22d1217bbd72
+ Thu, 23 Oct 2025 23:42:00 GMT
+
+ Alex Patterson
+
+ Identity in AI Agents
+ Alex Patterson
+
+ 5
+ 10
+ full
+ 00:42:43
+ false
+ <p>Curious about AI identity and MCP servers? Join us as Tobin South breaks down securing agents, fine-grained access, and the future of AI identity. Share your thoughts below!<a href="https://codingcat.dev/podcast/identity-access-management-for-agents-with-tobin-south" target="_blank" rel="ugc noopener noreferrer">https://codingcat.dev/podcast/identity-access-management-for-agents-with-tobin-south</a>00:00 Meet Tobin South06:11 MCP Security Challenges13:28 Agent to Agent Future24:28 MCP Server Setup33:12 Real-World Orchestration40:53 MCP Protocol Updates42:15 Closing Thoughts</p>
+
+
+
+ -
+ Build a Custom No-Code App in 90 Minutes
+ https://codingcat.dev/podcast/build-a-custom-no-code-app-in-90-minutes
+ a3607438-2fa0-4660-be27-0b16bfb22b6c
+ Sun, 12 Oct 2025 15:27:00 GMT
+
+ Alex Patterson
+
+ Build a Custom No-Code App in 90 Minutes
+ Alex Patterson
+
+ 5
+ 8
+ full
+ 00:52:12
+ false
+ <p>Curious about no-code app building? Dive in as we explore Adalo vs Bubble, micro SaaS ideas, and real talk on launching products without code. Drop your thoughts below and share!<a href="https://codingcat.dev/podcast/build-a-custom-no-code-app-in-90-minutes" target="_blank" rel="noopener noreferer">https://codingcat.dev/podcast/build-a-custom-no-code-app-in-90-minutes</a>00:00 Getting Started01:18 Guest Story07:02 No-Code Tools10:44 Building in Adalo23:23 Database & Response29:24 Scaling & Auth36:14 Platform Comparison50:23 Wrap Up</p>
+
+
+
+ -
+ How to Automate Your Work and Free Your Time
+ https://codingcat.dev/podcast/how-to-automate-your-work-and-free-your-time
+ 28b2a0b5-07d1-4b23-a515-2cdc1c1e7731
+ Wed, 08 Oct 2025 18:00:00 GMT
+
+ Alex Patterson
+
+ How to Automate Your Work and Free Your Time
+ Alex Patterson
+
+ 5
+ 7
+ full
+ 00:55:00
+ false
+ <p>Discover how to automate your tasks and reclaim your time! Join Alex and Joe as they discuss podcasting, automation, and work-life balance strategies. https://codingcat.dev/podcast/how-to-automate-your-work-and-free-your-time</p>
+
+
+
+ -
+ Running the Cloud on Your Desktop with LocalStack
+ https://codingcat.dev/podcast/running-the-cloud-on-your-desktop-with-localstack
+ d2d147a2-5954-4ee4-9548-222667035923
+ Mon, 06 Oct 2025 13:06:00 GMT
+
+ Alex Patterson
+
+ Running the Cloud on Your Desktop with LocalStack
+ Alex Patterson
+
+ 5
+ 9
+ full
+ 00:51:07
+ false
+ <p>Ever wondered how to run AWS locally and save money? Dive into LocalStack with Bryan and Alex for tips, demos, and real dev insights. Comment your thoughts and share!<a href="https://codingcat.dev/podcast/running-the-cloud-on-your-desktop-with-localstack" target="_blank" rel="noopener noreferer">https://codingcat.dev/podcast/running-the-cloud-on-your-desktop-with-localstack</a>00:00 Getting Started06:36 LocalStack Overview12:18 Web App Demo20:17 S3 Bucket Setup28:16 Lambda Functions34:56 API Gateway Integration41:04 Dev Workflows46:20 Final Thoughts</p>
+
+
+
+ -
+ Secure AI Access Control Using: SpiceDB
+ https://codingcat.dev/podcast/secure-ai-access-control-using-spicedb
+ e4aefc68-fdf9-441a-962f-653d10a3f01a
+ Mon, 21 Jul 2025 17:17:00 GMT
+
+ Alex Patterson
+
+ Secure AI Access Control Using: SpiceDB
+ Alex Patterson
+
+ 5
+ 6
+ full
+ 00:45:58
+ false
+ <p>Dive into hands-on SpiceDB demos, real-world permission modeling, and AI-powered scenario testing!</p>
+
+
+
+ -
+ Stop Building Authz, Start Building Value: A Deep Dive into Authorization, REBAC, and SpiceDB
+ https://codingcat.dev/podcast/scaling-permissions-with-spicedb-rebac-explained
+ b9dfe3ba-bd69-4588-97fe-012b3a370f14
+ Mon, 21 Jul 2025 16:55:00 GMT
+
+ Alex Patterson
+
+ Stop Building Authz, Start Building Value: A Deep Dive into Authorization, REBAC, and SpiceDB
+ Alex Patterson
+
+ 5
+ 5
+ full
+ 00:42:48
+ false
+ <p>Dive into AuthZed, SpiceDB, and Permissions.</p>
+
+
+
+ -
+ Autoshow - Automatic Show Notes for Podcasts and Videos
+ https://codingcat.dev/podcast/automatic-show-notes-for-podcasts-and-videos
+ 4628e5fe-28d8-49c4-af37-15904dcf787a
+ Wed, 05 Mar 2025 21:26:00 GMT
+
+ Alex Patterson
+ Autoshow - Automatic Show Notes for Podcasts and Videos
+ Alex Patterson
+
+ 5
+ 4
+ full
+
+ -
+ AI for Peeps Scared of AI
+ https://codingcat.dev/podcast/ai-for-peeps-scared-of-ai
+ 8efab30e-08b7-408d-ac98-c51238069042
+ Wed, 26 Feb 2025 05:01:00 GMT
+
+ Alex Patterson
+
+ AI for Peeps Scared of AI
+ Alex Patterson
+
+ 5
+ 3
+ full
+ 00:38:49
+ No
+ <p>Explore Ray Camden's tech journey & dive into AI for beginners. Discover the latest in AI, from coding basics to powerful tools like Google's Gemini.<a href="https://codingcat.dev/podcast/ai-for-peeps-scared-of-ai" target="_blank" rel="noopener noreferer">https://codingcat.dev/podcast/ai-for-peeps-scared-of-ai</a></p><p><br></p><p><br></p>
+
+
+
+ -
+ AI-Powered Persuasion Engineering with Ben Bressington
+ https://codingcat.dev/podcast/ai-powered-persuasion-engineering-with-ben-bressington
+ 59b0c713-79dc-45cc-b7ef-95479932611b
+ Tue, 18 Feb 2025 19:58:00 GMT
+
+ Alex Patterson
+
+ AI-Powered Persuasion Engineering with Ben Bressington
+ Alex Patterson
+
+ 5
+ 2
+ full
+ 00:54:43
+ No
+ <p>Join us for a deep dive into indie hacking with Jonathan Wilke. Discover insights on building SaaS without funding. Share your thoughts in the comments!</p><p><a href="https://codingcat.dev/podcast/ai-powered-persuasion-engineering-with-ben-bressington" target="_blank" rel="ugc noopener noreferrer">https://codingcat.dev/podcast/ai-powered-persuasion-engineering-with-ben-bressington</a></p>
+
+
+
+ -
+ supastarter and Indie Hacking with Jonathan Wilke
+ https://codingcat.dev/podcast/supastarter-and-indie-hacking-with-jonathan-wilke
+ 668e776c-0102-416a-9f83-78c024dae636
+ Thu, 30 Jan 2025 02:01:00 GMT
+
+ Alex Patterson
+
+ supastarter and Indie Hacking with Jonathan Wilke
+ Alex Patterson
+
+ 5
+ 1
+ full
+ 00:52:16
+ No
+ <p>Join us for a deep dive into indie hacking with Jonathan Wilke. Discover insights on building SaaS without funding. Share your thoughts in the comments!
+
+<a href="https://codingcat.dev/podcast/supastarter-and-indie-hacking-with-jonathan-wilke" target="_blank" rel="ugc noopener noreferrer">https://codingcat.dev/podcast/supastarter-and-indie-hacking-with-jonathan-wilke</a>
+
+Sponsors:
+- Cloudinary <a href="https://cld.media/codingcatdev" target="_blank" rel="ugc noopener noreferrer">https://cld.media/codingcatdev</a>
+- Algolia <a href="https://www.algolia.com/?utm_source=codingcatdev" target="_blank" rel="ugc noopener noreferrer">https://www.algolia.com/?utm_source=codingcatdev</a>
+
+00:00 Introduction
+00:42 Indie Hacking Insights
+03:16 Superstarter Overview
+10:10 Ad Break
+11:35 Feature Highlights
+17:30 Better Auth Discussion
+22:32 Deployment Challenges
+39:20 Full Control Benefits
+50:14 Closing Thoughts</p>
+
+
+
+ -
+ Stop Killing your Largest Contentful Paint score with images!
+ https://codingcat.dev/podcast/stop-killing-your-largest-contentful-paint-score-with-images
+ 6c788031-cace-4c70-bcf5-66d0dcda085f
+ Fri, 24 Jan 2025 12:35:00 GMT
+
+ Alex Patterson
+
+ Stop Killing your Largest Contentful Paint score with images!
+ Alex Patterson
+
+ 4
+ 23
+ full
+ 00:57:38
+ No
+ <p>Stop Killing your Largest Contentful Paint score with images!</p>
+<p><br /></p>
+<p>Join us as we explore Cloudinary's new components for Next.js! Learn how to optimize images, integrate Cloudinary, and boost performance. Don't miss out!</p>
+<p><br /></p>
+<p><a href="https://codingcat.dev/podcast/stop-killing-your-largest-contentful-paint-score-with-images" target="_blank" rel="ugc noopener noreferrer">https://codingcat.dev/podcast/stop-killing-your-largest-contentful-paint-score-with-images</a></p>
+<p>Sponsors:- Algolia <a href="https://www.algolia.com/?utm_source=codingcatdev" target="_blank" rel="ugc noopener noreferrer">https://www.algolia.com/?utm_source=codingcatdev</a></p>
+<p><br /></p>
+<p>00:00 Introduction</p>
+<p>01:41 Cloudinary Overview</p>
+<p>05:08 Next.js Integration</p>
+<p>10:23 Image Transformations</p>
+<p>36:58 Performance Insights</p>
+<p>48:42 Additional Cloudinary Features</p>
+<p>54:47 Closing Thoughts</p>
+
+
+
+ -
+ How to add Sentry to Next.js the right way!
+ https://codingcat.dev/podcast/how-to-add-sentry-to-next-js-the-right-way
+ c091d38b-4812-4922-a6f9-2e8ea7aa1dd1
+ Wed, 16 Oct 2024 19:12:28 GMT
+
+ Alex Patterson
+
+ How to add Sentry to Next.js the right way!
+ Alex Patterson
+
+ 2
+ 6
+ full
+ 01:05:57
+ No
+ <p>Learn how to integrate Sentry with Next.js! Join us as Luca walks through essential steps, troubleshooting tips, and performance optimization. Share your thoughts below!
+
+<a href="https://codingcat.dev/podcast/how-to-add-sentry-to-next-js-the-right-way" target="_blank" rel="ugc noopener noreferrer">https://codingcat.dev/podcast/how-to-add-sentry-to-next-js-the-right-way</a>
+
+Sponsors:
+- Algolia <a href="https://www.algolia.com/?utm_source=codingcatdev" target="_blank" rel="ugc noopener noreferrer">https://www.algolia.com/?utm_source=codingcatdev</a>
+
+00:00 Introduction
+01:57 Installation Guide
+03:39 Configuration Details
+06:08 Performance Monitoring
+14:16 Troubleshooting Tips
+24:12 Advanced Features
+40:07 Deployment Insights
+55:02 Performance Optimization</p>
+
+---
+
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Unlocking your ADHD Superpowers
+ https://codingcat.dev/podcast/unlocking-your-adhd-superpowers
+ 94893625-c4bc-4adc-b59b-391fa13f2b5e
+ Thu, 10 Oct 2024 15:28:00 GMT
+
+ Alex Patterson
+
+ Unlocking your ADHD Superpowers
+ Alex Patterson
+
+ 4
+ 22
+ full
+ 01:13:33
+ No
+ <p>Discover the unique strengths of ADHD in coding! Join us as we explore superpowers, challenges, and tips for thriving with ADHD. Comment and share your experiences!
+
+https://codingcat.dev/podcast/unlocking-your-adhd-superpowersSponsors:- Cloudinary https://cld.media/codingcatdev- Algolia https://www.algolia.com/?utm_source=codingcatdev
+
+00:00 Introduction
+00:18 ADHD and Coding
+05:08 Understanding ADHD
+13:03 Diagnosing ADHD
+20:28 ADHD Hyperfocus
+29:07 Strategies for Success
+37:45 Mental Health Insights
+56:10 Different, Not Worse</p>
+
+---
+
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Building Successful Open Source Projects
+ https://codingcat.dev/podcast/building-successful-open-source-projects
+ c49ed931-7b57-4d27-a659-026d0c5c0e93
+ Wed, 25 Sep 2024 20:39:00 GMT
+
+ Alex Patterson
+
+ Building Successful Open Source Projects
+ Alex Patterson
+
+ 4
+ 21
+ full
+ 00:38:59
+ No
+ <p>I chatted with Brian (aka bdougie) about Hacktoberfest, building OpenSauced, and making successful open-source projects.</p>
+<p><br></p>
+<p><a href="https://codingcat.dev/podcast/building-successful-open-source-projects" target="_blank" rel="noopener noreferer">https://codingcat.dev/podcast/building-successful-open-source-projects</a></p>
+<p><br></p>
+<p>Sponsors:
+- Cloudinary <a href="https://cld.media/codingcatdev">https://cld.media/codingcatdev</a>
+- Algolia <a href="https://www.algolia.com/?utm_source=codingcatdev">https://www.algolia.com/?utm_source=codingcatdev</a></p>
+
+---
+
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Infrastructure Driven Development
+ https://codingcat.dev/podcast/infrastructure-driven-development
+ 09d69095-4287-4b69-9f02-3d2e873f358e
+ Fri, 20 Sep 2024 17:48:00 GMT
+
+ Alex Patterson
+
+ Infrastructure Driven Development
+ Alex Patterson
+
+ 4
+ 20
+ full
+ 00:46:15
+ No
+ <p>Join us as Sathiesh Veera shares his journey from engineering in India to a career in Java development, DevOps, and founding a tech startup. Don't miss out on his insights!</p>
+<p><br></p>
+<p><a href="https://codingcat.dev/podcast/infrastructure-driven-development" target="_blank" rel="noopener noreferer">https://codingcat.dev/podcast/infrastructure-driven-development</a></p>
+<p><br></p>
+<p>Sponsors:
+- Cloudinary <a href="https://cld.media/codingcatdev" target="_blank" rel="noopener noreferer">https://cld.media/codingcatdev</a>
+- Algolia <a href="https://www.algolia.com/?utm_source=codingcatdev" target="_blank" rel="noopener noreferer">https://www.algolia.com/?utm_source=codingcatdev</a></p>
+<p><br></p>
+
+---
+
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ WebDev Fundamentals | React, Angular, and Vue
+ https://codingcat.dev/podcast/webdev-fundamentals-or-react-angular-and-vue
+ 4edb915d-6b6c-4d9e-9692-0127da16ca1b
+ Wed, 24 Jul 2024 20:21:00 GMT
+
+ Alex Patterson
+
+ WebDev Fundamentals | React, Angular, and Vue
+ Alex Patterson
+
+ 4
+ 19
+ full
+ 00:52:29
+ No
+ <p>Join us as we dive into web development fundamentals with Corbin! Learn about his journey, from early education to teaching and contributing to TanStack. Perfect for both new and seasoned devs. Don't miss insights on frameworks, APIs, and more. Watch, comment, and share!
+
+https://codingcat.dev/podcast/webdev-fundamentals-or-react-angular-and-vue
+
+Sponsors:
+- Cloudinary https://cld.media/codingcatdev
+- Algolia https://www.algolia.com/?utm_source=codingcatdev
+
+00:00 Introduction
+00:18 Welcome Corbin
+00:57 WebDev Fundamentals
+01:28 College Years
+02:20 Overcoming Challenges
+03:50 Technical Projects
+05:11 Early Interests
+10:02 Working at Descript
+11:45 AI in WebDev
+13:00 Sponsor Break
+14:56 Framework Guide
+22:52 Learning Paths
+37:13 Rebuilding Projects
+48:02 Final Thoughts</p>
+
+---
+
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Turning Documentation into a Product: Best Practices for Success
+ https://codingcat.dev/podcast/turning-documentation-into-a-product-best-practices-for-success
+ 120b48ee-cd22-429e-b36f-d0a4704565e4
+ Wed, 17 Jul 2024 16:00:00 GMT
+
+ Alex Patterson
+
+ Turning Documentation into a Product: Best Practices for Success
+ Alex Patterson
+
+ 4
+ 18
+ full
+ 00:46:59
+ No
+ <p>Hey, everyone! Join us on CodingCat.dev as Vincent shares his journey from electrical engineering to tech writing. It's a fun chat with plenty of laughs and insights into the tech world. Don’t miss out and remember to share it with your friends!
+
+https://codingcat.dev/podcast/turning-documentation-into-a-product-best-practices-for-success
+
+Sponsors:
+- Cloudinary https://cld.media/codingcatdev
+- Algolia https://www.algolia.com/?utm_source=codingcatdev
+
+00:00 Welcome
+01:20 Correct Intro
+03:20 Starting Conversation
+06:50 Career Path
+10:01 Into Tech
+18:13 Docs as Product
+28:33 Continuous Improvement
+41:12 Future of Docs
+43:52 Conclusion</p>
+
+---
+
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ AI for System Design and Architecture Documentation
+ https://codingcat.dev/podcast/ai-for-system-design-and-architecture-documentation
+ 30be7590-3e6f-412f-bcd9-a5421ed57997
+ Mon, 08 Jul 2024 16:00:00 GMT
+
+ Alex Patterson
+
+ AI for System Design and Architecture Documentation
+ Alex Patterson
+
+ 4
+ 17
+ full
+ 00:39:51
+ No
+ <p>Learn how AI transforms system design and architecture with insights from Multiplayer's CTO. Explore new tools and collaborative features for developers.</p>
+<p><br></p>
+<p>https://codingcat.dev/podcast/ai-for-system-design-and-architecture-documentation</p>
+<p><br></p>
+<p>00:00 Introduction</p>
+<p>00:18 Meet Thomas Johnson</p>
+<p>00:54 What is Multiplayer?</p>
+<p>02:55 Early Tech Journey</p>
+<p>05:12 Career Transition</p>
+<p>07:04 VOIP Industry Insights</p>
+<p>08:56 System Design Importance</p>
+<p>12:47 Multiplayer Demo</p>
+<p>24:53 Collaborative Features</p>
+<p>26:42 AI in System Design</p>
+<p>29:36 Consulting Potential</p>
+<p>35:21 Perfect Picks</p>
+<p>39:30 Conclusion</p>
+
+---
+
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Google I/O Flutter Updates and Firebase
+ https://codingcat.dev/podcast/google-i-o-flutter-updates-and-firebase
+ 796783b3-91df-4657-9db0-316a77cf4cc1
+ Fri, 05 Jul 2024 16:00:00 GMT
+
+ Alex Patterson
+
+ Google I/O Flutter Updates and Firebase
+ Alex Patterson
+
+ 4
+ 16
+ full
+ 00:49:20
+ No
+ <p>Catch the latest updates on Flutter and Firebase from Google I/O 2023. Hear Roman Jacquez's insights and explore the exciting new features. Share your thoughts!https://codingcat.dev/podcast/google-i-o-flutter-updates-and-firebase
+
+00:00 Introduction
+00:18 Meet Roman
+00:34 Google I/O Highlights
+01:13 Flutter Moments
+01:35 Roman's Background
+02:25 Career Journey
+05:55 Philips Health System
+10:49 Fl |wb
+12:25 What's New in Flutter
+20:46 Firebase Data Connect
+27:59 Firebase Features
+36:17 Multimodal Integration
+41:32 Flame 2D Engine
+47:31 Conclusion</p>
+
+---
+
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Using Firebase with Communication APIs
+ https://codingcat.dev/podcast/using-firebase-with-communication-apis
+ ca69fea5-608a-4cee-9eba-8a2d915d7c84
+ Wed, 03 Jul 2024 16:00:00 GMT
+
+ Alex Patterson
+
+ Using Firebase with Communication APIs
+ Alex Patterson
+
+ 4
+ 15
+ full
+ 00:59:48
+ No
+ <p>Jump into "UsingFirebaseWithCommunicationsAPI.mp4" with Amanda and learn how to leverage Firebase and Vonage APIs for building secure, interactive web experiences. Comment and share!
+
+<a href="https://codingcat.dev/podcast/using-firebase-with-communication-apis" target="_blank" rel="noopener noreferer">https://codingcat.dev/podcast/using-firebase-with-communication-apis</a>
+
+00:00 Introduction
+00:20 Meet Amanda
+01:48 Amanda's Background
+04:10 Dual Language Education
+05:16 In-Person Meetups
+10:28 Developer Advocacy
+16:01 Firebase & Vonage Demo
+27:40 Setting up Firebase
+34:52 Deploying Functions
+46:39 Additional Resources
+58:57 Conclusion</p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ What's A Firebase Developer Advocate
+ https://codingcat.dev/podcast/what-is-a-firebase-developer-advocate
+ ee3176c9-76b8-44cf-9a87-0fcac0aa8e53
+ Mon, 03 Jun 2024 05:12:56 GMT
+
+ Alex Patterson
+
+ What's A Firebase Developer Advocate
+ Alex Patterson
+
+ 4
+ 14
+ full
+ 00:33:19
+ No
+ <p><a href="https://codingcat.dev/podcast/what-is-a-firebase-developer-advocate" target="_blank" rel="noopener noreferer">https://codingcat.dev/podcast/what-is-a-firebase-developer-advocate</a></p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Angular Cookbook
+ https://codingcat.dev/podcast/angular-cookbook
+ 1bb4346d-eba0-41af-aecf-644bc7bcdefe
+ Mon, 03 Jun 2024 04:42:37 GMT
+
+ Alex Patterson
+
+ Angular Cookbook
+ Alex Patterson
+
+ 4
+ 11
+ full
+ 00:45:58
+ No
+ <p>Discover advanced Angular tips and tricks with Mohammad from the Angular Cookbook 2nd Edition. Watch now and elevate your front-end development skills!</p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Video Tap: Automate Your Video Marketing with Chris Sev
+ https://codingcat.dev/podcast/video-tap-automate-your-video-marketing-with-chris-sev
+ 59b06426-2b94-4b31-ab7a-f18659c121e5
+ Mon, 03 Jun 2024 04:35:00 GMT
+
+ Alex Patterson
+
+ Video Tap: Automate Your Video Marketing with Chris Sev
+ Alex Patterson
+
+ 4
+ 12
+ full
+ 00:49:15
+ No
+ <p>Join us as Chris Sev shares his dev journey, insights on building Scotch IO, and his exciting new AI tool, Video Tap. </p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Firebase Security Rules: Effortless control over your app's data.
+ https://codingcat.dev/podcast/firebase-security-rules-effortless-control-over-your-app-s-data
+ b61bf55d-ab3d-4164-8a35-f5f7eb770536
+ Sat, 06 Apr 2024 04:00:00 GMT
+
+ Alex Patterson
+
+ Firebase Security Rules: Effortless control over your app's data.
+ Alex Patterson
+
+ 4
+ 13
+ full
+ 00:52:46
+ No
+ <p>Firebase Security Rules are a powerful feature that allows you to control access to your app's data in Firebase Realtime Database, Cloud Firestore, and Cloud Storage.</p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Flutter 2024
+ https://codingcat.dev/podcast/4-9-flutter-2024
+ ZKdo3vLUqcb4sYreiCPA9U
+ Wed, 06 Mar 2024 05:00:00 GMT
+
+ Alex Patterson
+
+ Flutter 2024
+ Alex Patterson
+
+ 4
+ 9
+ full
+ 00:49:34
+ No
+ <p>Flutter's position in the app development landscape remains strong in 2024. With its focus on quality, performance, new features, and a thriving community, Flutter empowers developers to build beautiful, cross-platform apps efficiently.
+
+https://codingcat.dev/podcast/4-9-flutter-2024
+
+Sponsors:
+Cloudinary - Build faster with AI powered image and video APIs.
+https://cld.media/codingcatdev
+
+Recorded: Feb 19, 2024</p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Not a Designer: Design bites for devs
+ https://codingcat.dev/podcast/not-a-designer-design-bites-for-devs
+ 8510028d-dec1-40c7-bdbe-bed50dbc1f3d
+ Thu, 29 Feb 2024 05:25:00 GMT
+
+ Alex Patterson
+
+ Not a Designer: Design bites for devs
+ Alex Patterson
+
+ 4
+ 10
+ full
+ 00:46:54
+ No
+ <p>In the tech world, developers often find themselves stepping into the realm of design, a space that can feel both unfamiliar and daunting. Recognizing this challenge, Saron Yitbarek has crafted "Not a Designer" Design Bites, a series specifically tailored for developers who find themselves in need of design knowledge.</p>
+<p><br></p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Design Systems: From Atomic Design to a Global Solution
+ https://codingcat.dev/podcast/4-8-design-systems-brad-frost
+ ZKdo3vLUqcb4sYreiCPARs
+ Wed, 28 Feb 2024 05:00:00 GMT
+
+ Alex Patterson, Brittney Postma
+
+ Design Systems: From Atomic Design to a Global Solution
+ Alex Patterson, Brittney Postma
+
+ 4
+ 8
+ full
+ 01:06:04
+ No
+ <p>Dive into the world of creating and maintaining successful design systems, unpack the layer cake approach, and explore the potential future of a global design system.
+
+https://codingcat.dev/podcast/4-8-design-systems-brad-frost
+
+Sponsors:
+Cloudinary - Build faster with AI powered image and video APIs.
+https://cld.media/codingcatdev
+
+Recorded: Jan 10, 2024</p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Secure Coding - Beyond the Surface with Snyk
+ https://codingcat.dev/podcast/secure-coding-beyond-the-surface-with-snyk
+ bc72d5c6-0286-4853-a62e-20662ba4ea51
+ Mon, 26 Feb 2024 17:59:00 GMT
+
+ Alex Patterson
+
+ Secure Coding - Beyond the Surface with Snyk
+ Alex Patterson
+
+ 2
+ 5
+ full
+ 01:24:23
+ No
+ <p><a href="https://codingcat.dev/podcast/secure-coding-beyond-the-surface-with-snyk" target="_blank" rel="noopener noreferer">https://codingcat.dev/podcast/secure-coding-beyond-the-surface-with-snyk</a></p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Refine the open-source Retool for Enterprise
+ https://codingcat.dev/podcast/cwcc-1_refine
+ ZKdo3vLUqcb4sYreiCP8ok
+ Wed, 21 Feb 2024 05:00:00 GMT
+
+ Alex Patterson
+
+ Refine the open-source Retool for Enterprise
+ Alex Patterson
+
+ 2
+ 4
+ full
+ 01:31:10
+ No
+ <p><a href="https://codingcat.dev/podcast/cwcc-1_refine" target="_blank" rel="noopener noreferer">https://codingcat.dev/podcast/cwcc-1_refine</a></p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Master Offline AWS Development: Harness the Power of LocalStack!
+ https://codingcat.dev/podcast/4-7-harness-the-power-of-localstack
+ ZKdo3vLUqcb4sYreiCPAb4
+ Tue, 20 Feb 2024 05:00:00 GMT
+
+ Alex Patterson
+
+ Master Offline AWS Development: Harness the Power of LocalStack!
+ Alex Patterson
+
+ 4
+ 7
+ full
+ 00:46:20
+ No
+ <p>Effortlessly Develop AWS Applications Offline with LocalStack: Your Ultimate Guide!</p>
+<p><br></p>
+<p><a href="https://codingcat.dev/podcast/4-7-harness-the-power-of-localstack" target="_blank" rel="noopener noreferer">https://codingcat.dev/podcast/4-7-harness-the-power-of-localstack</a></p>
+<p>Sponsors:
+Cloudinary - Build faster with AI powered image and video APIs.
+<a href="https://cld.media/codingcatdev" target="_blank" rel="noopener noreferer">https://cld.media/codingcatdev</a>
+
+Recorded: Friday Dec 29, 2023 ⋅ 2:00pm
+
+00:00 - Intro
+00:48 - How Waldemar came to LocalStack</p>
+<p>04:28 - Waldemar's academic and research background</p>
+<p>07:24 - Cloudinary Sponsorship</p>
+<p>08:00 - LocalStack Demo</p>
+<p>25:00 - How to move into staging and production</p>
+<p>27:15 - Cloud Pods</p>
+<p>32:45 - Fault Injection and Simulation</p>
+<p>36:01 - IAM Live Policy Generation</p>
+<p>38:56 - What's next for LocalStack?</p>
+<p>41:35 - How is LocalStack funded?</p>
+<p>42:35 - Purrfect Picks</p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Next.js Amplified: Full-Stack Web Apps on AWS Amplify Gen2
+ https://codingcat.dev/podcast/cwcc-1-2-aws-amplify
+ ZKdo3vLUqcb4sYreiCP9PW
+ Wed, 14 Feb 2024 05:00:00 GMT
+
+ Alex Patterson
+
+ Next.js Amplified: Full-Stack Web Apps on AWS Amplify Gen2
+ Alex Patterson
+
+ 2
+ 3
+ full
+ 01:27:26
+ No
+ <p><a href="https://codingcat.dev/podcast/cwcc-1-2-aws-amplify" target="_blank" rel="noopener noreferer">https://codingcat.dev/podcast/cwcc-1-2-aws-amplify</a></p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Supabase New Release
+ https://codingcat.dev/podcast/4-6-supabase-release-week-x-new-features
+ ZKdo3vLUqcb4sYreiCPAkG
+ Tue, 13 Feb 2024 05:00:00 GMT
+
+ Alex Patterson
+
+ Supabase New Release
+ Alex Patterson
+
+ 4
+ 6
+ full
+ 01:08:19
+ No
+ <p><a href="https://codingcat.dev/podcast/4-6-supabase-release-week-x-new-features" target="_blank" rel="noopener noreferer">https://codingcat.dev/podcast/4-6-supabase-release-week-x-new-features</a></p>
+<p><br></p>
+<p>Sponsors:
+Cloudinary - Build faster with AI powered image and video APIs.
+<a href="https://cld.media/codingcatdev">https://cld.media/codingcatdev</a></p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Building docs with Starlight and Astro
+ https://codingcat.dev/podcast/cwcc-1-building-docs-with-starlight-and-astro
+ ZKdo3vLUqcb4sYreiCP978
+ Wed, 07 Feb 2024 05:00:00 GMT
+
+ Alex Patterson
+
+ Building docs with Starlight and Astro
+ Alex Patterson
+
+ 2
+ 2
+ full
+ 01:28:43
+ No
+ <p><a href="https://codingcat.dev/podcast/cwcc-1-building-docs-with-starlight-and-astro" target="_blank" rel="noopener noreferer">https://codingcat.dev/podcast/cwcc-1-building-docs-with-starlight-and-astro</a></p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Nx - Project Crystal
+ https://codingcat.dev/podcast/4-8-nx-project-crystal
+ ZKdo3vLUqcb4sYreiCPAIg
+ Wed, 07 Feb 2024 05:00:00 GMT
+
+ Alex Patterson
+
+ Nx - Project Crystal
+ Alex Patterson
+
+ 4
+ 5
+ full
+ 00:41:57
+ No
+ <p>Katarina joins Alex to discuss Nx and all the of the changes it's gone through and to explain why you need Project Crystal.
+
+<a href="https://codingcat.dev/podcast/4-8-nx-project-crystal" target="_blank" rel="noopener noreferer">https://codingcat.dev/podcast/4-8-nx-project-crystal</a>
+
+Sponsors:
+Cloudinary - Build faster with AI powered image and video APIs.
+<a href="https://cld.media/codingcatdev" target="_blank" rel="noopener noreferer">https://cld.media/codingcatdev</a>
+
+Recorded: January 13 ,2023 2:00 PM
+
+00:00 Welcome Katerina
+10:16 Sponsor: Cloudinary
+11:14 Nx Iterations and Changes
+36: 18 Purfect Picks</p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Simplyfing SvelteKit View Transitions with Paolo Ricciuti
+ https://codingcat.dev/podcast/cwcc-1-3-svelte-view-transitions
+ ZKdo3vLUqcb4sYreiCP9GK
+ Mon, 05 Feb 2024 05:00:00 GMT
+
+ Alex Patterson
+
+ Simplyfing SvelteKit View Transitions with Paolo Ricciuti
+ Alex Patterson
+
+ 2
+ 1
+ full
+ 01:38:13
+ No
+ <p><a href="https://codingcat.dev/podcast/podcast-cwcc-1-3-svelte-view-transitions" target="_blank" rel="noopener noreferer">https://codingcat.dev/podcast/podcast-cwcc-1-3-svelte-view-transitions</a></p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Raycast a launcher faster than F1!
+ https://codingcat.dev/podcast/4-4-raycast
+ ZKdo3vLUqcb4sYreiCPAtS
+ Wed, 31 Jan 2024 05:00:00 GMT
+
+ Alex Patterson, Brittney Postma
+
+ Raycast a launcher faster than F1!
+ Alex Patterson, Brittney Postma
+
+ 4
+ 4
+ full
+ 00:57:31
+ No
+ <p>Pedro joins us to walk through his favorite features of Raycast and expose the secrets of what we didn’t even know Raycast could do!</p>
+<p><br></p>
+<p><a href="https://codingcat.dev/podcast/4-4-raycast" target="_blank" rel="noopener noreferer">https://codingcat.dev/podcast/4-4-raycast</a></p>
+<p><br></p>
+<p>Sponsors:Cloudinary - Build faster with AI powered image and video APIs.
+<a href="https://cld.media/codingcatdev" target="_blank" rel="noopener noreferer">https://cld.media/codingcatdev</a>
+
+Recorded: December 12, 2023 9:00 AM</p>
+<p><br></p>
+<p>00:00 Intro and Welcome Pedro</p>
+<p>03:45 Rainbow to Raycast </p>
+<p>05:37 Pedros Roll at Raycast</p>
+<p>07:57 Sponsor: Cloudinary </p>
+<p>08:42 What is Raycast?</p>
+<p>11:52 Tech Behind Raycast</p>
+<p>12:41 Favorite Features </p>
+<p>28:47 Raycast for Teams</p>
+<p>32:50 Walkthrough Getting Started</p>
+<p>36:40 Scheming </p>
+<p>41:17 Scope and Pricing </p>
+<p>45:22 Perfect Picks</p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Leveling Up Your Career: Unleashing Your Full Potential!
+ https://codingcat.dev/podcast/4-3-leveling-up-your-career-with-tdesseyn
+ ZKdo3vLUqcb4sYreiCPB2e
+ Wed, 24 Jan 2024 05:00:00 GMT
+
+ Alex Patterson
+
+ Leveling Up Your Career: Unleashing Your Full Potential!
+ Alex Patterson
+
+ 4
+ 3
+ full
+ 00:51:04
+ No
+ <p>Taylor joins the podcast to talk about how recruiters can work better for you and tell his journey to how he recruits differently.
+
+https://codingcat.dev/podcast/_leveling-up-your-career-with-tdesseyn</p>
+<p><br></p>
+<p>Sponsors:Cloudinary - Build faster with AI powered image and video APIs.
+https://cld.media/codingcatdev
+
+Recorded: December 6, 2023 3:00 PM
+
+00:00 Welcome Taylor and Backstory</p>
+<p>12:57 Sponsor: Cloudinary</p>
+<p>13:36 Starting as a Recruiter</p>
+<p>32:30 Finding a Job</p>
+<p>38:55 Practical Helping</p>
+<p>46:08 Purfect Picks</p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Compassionate Coding: Emotional Intelligence for Engineers
+ https://codingcat.dev/podcast/4-2-compassionate-coding
+ LzgcvqoNSwz5R8loykbNx1
+ Fri, 19 Jan 2024 05:00:00 GMT
+
+ Alex Patterson
+ Compassionate Coding: Emotional Intelligence for Engineers
+ Alex Patterson
+
+ 4
+ 2
+ full
+
+ -
+ TypeScript is freaking hard!
+ https://codingcat.dev/podcast/4-1-typescript-is-freaking-hard
+ ZKdo3vLUqcb4sYreiCPBBq
+ Mon, 01 Jan 2024 05:00:00 GMT
+
+ Alex Patterson
+
+ TypeScript is freaking hard!
+ Alex Patterson
+
+ 4
+ 1
+ full
+ 00:40:05
+ No
+ <p>April joins the podcast to tell us about her company and to explain what compassion looks like practically in the dev world. </p>
+<p><br></p>
+<p>https://codingcat.dev/podcast/_typescript-is-freaking-hard</p>
+<p><br></p>
+<p>Sponsors:</p>
+<p>Cloudinary - Build faster with AI powered image and video APIs. </p>
+<p>https://cld.media/codingcatdev</p>
+<p><br></p>
+<p>Recorded: December 5,2023 3:00 PM</p>
+<p><br></p>
+<p>00:00 Intro and Welcome April</p>
+<p>10:51 Sponsor: Cloudinary </p>
+<p>11:30 Compassionate Coding</p>
+<p>16:08 What is Compassion </p>
+<p>28:08 Compassionate Coding the Company </p>
+<p>35:22 Perfect Picks</p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Building your Own SaaS
+ https://codingcat.dev/podcast/cwcc-2023-12-20-building-your-own-sass
+ ZKdo3vLUqcb4sYreiCP8fY
+ Mon, 18 Dec 2023 05:00:00 GMT
+
+ Alex Patterson
+
+ Building your Own SaaS
+ Alex Patterson
+
+ 1
+ 4
+ full
+ 01:31:17
+ No
+ <p>https://codingcat.dev/podcast/cwcc-2023-12-20-building-your-own-sass</p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Expo from a Web Developers View
+ https://codingcat.dev/podcast/cwcc-1-expo-react-native
+ ZKdo3vLUqcb4sYreiCP8xw
+ Mon, 18 Dec 2023 05:00:00 GMT
+
+ Alex Patterson
+
+ Expo from a Web Developers View
+ Alex Patterson
+
+ 1
+ 3
+ full
+ 01:31:20
+ No
+ <p>https://codingcat.dev/podcast/podcast-cwcc-1-expo-react-native</p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ CodingCat.dev Podcast - Holiday Special Live December 20 9a ET
+ https://codingcat.dev/podcast/3-holiday-special
+ ZKdo3vLUqcb4sYreiCPBL2
+ Sun, 17 Dec 2023 05:00:00 GMT
+
+ Alex Patterson, Brittney Postma
+
+ CodingCat.dev Podcast - Holiday Special Live December 20 9a ET
+ Alex Patterson, Brittney Postma
+
+ 3
+ 52
+ full
+ 00:12:57
+ No
+ <p>This is the Origin story for purrfect.dev and its creators.</p>
+<p>We are still figuring out what we want purrfect.dev to become, but having fun learning about everyone in the web development community.</p>
+<p>You can also find the video version at <a href="https://link.ajonp.com/youtube">https://link.ajonp.com/youtube</a></p>
+<p><br></p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ OpenSauced: Your fast track to open source
+ https://codingcat.dev/podcast/3-25-your-fast-track-to-open-source
+ ZKdo3vLUqcb4sYreiCPD4I
+ Wed, 13 Dec 2023 05:00:00 GMT
+
+ Alex Patterson, Brittney Postma
+
+ OpenSauced: Your fast track to open source
+ Alex Patterson, Brittney Postma
+
+ 3
+ 25
+ full
+ 01:05:45
+ No
+ <p>Bekah joins Brittney and Alex to tell her journey to tech and all about her roll at OpenSauced as well as to do a walkthrough of how to get the most out of OpenSauced. </p>
+<p><br></p>
+<p><a href="https://codingcat.dev/podcast/3-22-fly-io-with-dov-alperin">https://codingcat.dev/podcast/_opensauced</a></p>
+<p><br></p>
+<p>Sponsors: Storyblok Have you already discovered Storyblok? They have an official Svelte SDK! 74,000 + developers & marketers use it to deliver powerful content experiences on any frontend: Websites, eCommerce, mobile apps, AR/VR, or voice content!
+Recorded: November 17, 2023 9:00 PM</p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Open Source Svelte Magic: Melting Minds through Melt UI!
+ https://codingcat.dev/podcast/cwcc-1-2-meltui
+ LzgcvqoNSwz5R8loykbN6l
+ Mon, 11 Dec 2023 05:00:00 GMT
+
+ Alex Patterson
+
+ Open Source Svelte Magic: Melting Minds through Melt UI!
+ Alex Patterson
+
+ 1
+ 2
+ full
+ 01:29:35
+ No
+ <p>https://codingcat.dev/podcast/cwcc-1-2-meltui</p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Skyflow's privacy vault for building LLMs
+ https://codingcat.dev/podcast/3-24-skyflow-privacy-vault
+ ZKdo3vLUqcb4sYreiCPDDU
+ Wed, 06 Dec 2023 05:00:00 GMT
+
+ Alex Patterson
+
+ Skyflow's privacy vault for building LLMs
+ Alex Patterson
+
+ 3
+ 24
+ full
+ 00:54:32
+ No
+ <p>Sean shows us Skyflow as we discuss consumer privacy and how to stay on to of data management and safety. </p>
+<p>https://codingcat.dev/podcast/_skyflow</p>
+<p><br></p>
+<p>Sponsors: Storyblok Have you already discovered Storyblok? They have an official Svelte SDK! 74,000 + developers & marketers use it to deliver powerful content experiences on any frontend: Websites, eCommerce, mobile apps, AR/VR, or voice content!
+Recorded: September 14, 2023 1:15 PM</p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Angular.dev
+ https://codingcat.dev/podcast/3-23-angular-17
+ ZKdo3vLUqcb4sYreiCPDMg
+ Wed, 29 Nov 2023 05:00:00 GMT
+
+ Alex Patterson
+
+ Angular.dev
+ Alex Patterson
+
+ 3
+ 23
+ full
+ 00:57:19
+ No
+ <p>Alex joins Emma to pick her brain on the new Angular rebrand and to get an explanation on all of the new updates coming soon!</p>
+<p><br></p>
+<p><a href="https://codingcat.dev/podcast/3-22-fly-io-with-dov-alperin">https://codingcat.dev/podcast/3-23-angular-17</a></p>
+<p><br></p>
+<p>Sponsors: Storyblok Have you already discovered Storyblok? They have an official Svelte SDK! 74,000 + developers & marketers use it to deliver powerful content experiences on any frontend: Websites, eCommerce, mobile apps, AR/VR, or voice content!
+Recorded: November 13, 2023 4:00 PM</p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+ -
+ Speedrun Your App Onto Fly.io
+ https://codingcat.dev/podcast/3-22-fly-io-with-dov-alperin
+ ZKdo3vLUqcb4sYreiCPDf4
+ Wed, 15 Nov 2023 05:00:00 GMT
+
+ Alex Patterson
+
+ Speedrun Your App Onto Fly.io
+ Alex Patterson
+
+ 3
+ 22
+ full
+ 00:51:10
+ No
+ <p>Dov joins Alex to discuss his learning experiences and his path to <a href="http://Fly.io">Fly.io</a> and to talk about all the features it offers. </p>
+<p><br></p>
+<p><a href="https://codingcat.dev/podcast/3-22-fly-io-with-dov-alperin" target="_blank" rel="noreferrer noopener">https://codingcat.dev/podcast/3-22-fly-io-with-dov-alperin</a></p>
+<p><br></p>
+<p>Sponsors: Storyblok Have you already discovered Storyblok? They have an official Svelte SDK! 74,000 + developers & marketers use it to deliver powerful content experiences on any frontend: Websites, eCommerce, mobile apps, AR/VR, or voice content!
+Recorded: November 13, 2023 2:00 PM</p>
+
+---
+
+Send in a voice message: https://podcasters.spotify.com/pod/show/codingcatdev/message
+Support this podcast: <a href="https://podcasters.spotify.com/pod/show/codingcatdev/support" rel="payment">https://podcasters.spotify.com/pod/show/codingcatdev/support</a>
+
+
+
+
\ No newline at end of file
diff --git a/apps/site/baseline/robots.txt b/apps/site/baseline/robots.txt
new file mode 100644
index 000000000..814f14a08
--- /dev/null
+++ b/apps/site/baseline/robots.txt
@@ -0,0 +1,7 @@
+User-Agent: *
+Allow: /
+Disallow: /api/
+Disallow: /dashboard/
+
+Host: https://codingcat.dev
+Sitemap: https://codingcat.dev/sitemap.xml
diff --git a/apps/site/baseline/sitemap.xml b/apps/site/baseline/sitemap.xml
new file mode 100644
index 000000000..979ee6662
--- /dev/null
+++ b/apps/site/baseline/sitemap.xml
@@ -0,0 +1,2835 @@
+
+
+
+https://codingcat.dev
+2026-08-18T14:07:47.164Z
+monthly
+1
+
+
+https://codingcat.dev/search
+2026-08-18T14:07:47.164Z
+daily
+0.1
+
+
+https://codingcat.dev/post/why-we-are-absolutely-obsessed-with-sticker-mule-s-holographic-stickers
+2026-06-23T02:29:09.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/video-tap-automate-your-video-marketing-with-chris-sev
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/cwcc-2023-12-20-building-your-own-sass
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/cwcc-1-expo-react-native
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/cwcc-1-building-docs-with-starlight-and-astro
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/4-8-nx-project-crystal
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-6-Effective-Testing-using-Cypress.io
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-5-Breaking-down-Qwik-with-Shai-Reznik
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-16-MDSvex-with-pngwn
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-5-scaling-transactional-data-globally-with-fauna
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-43-Growing-as-a-DXE-and-Building-Dev-communities
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-11-build-faster-websites-with-astro
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-10-creating-a-startup
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/0-7-scully-with-aaron-frost
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/aws-amplify-video
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/updating-ajonps-brand
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/the-basics-of-big-data-for-web-developers
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/scully-io-angular-static-site-generator
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/sanity-io-to-builder-io-data-transfer
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/rxfire-in-svelte-3-using-firebase-firestore-and-authentication
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/rxfire-in-reactjs-using-firebase-firestore-and-authentication
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/reviewing-2021-and-predicting-2022
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/pulumi-automation-api
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/notion-to-hashnode-and-devto-using-pubsub-cloud-functions
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/nextjs-11-features
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/make-pathing-easier-with-aliases-in-sveltekit
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/learning-builder-io-from-a-developers-perspective
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/increasing-productivity-with-notion
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/integrating-storybook-with-sveltekit
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/hugo-ionic-template
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/google-cloud-reference
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/hacktober-2022-contest
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/how-to-achieve-16-9-resolution-while-using-divvy-on-a-mac
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/git-your-build-system-right
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/gatsby-cloud-with-sanity-cms
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/front-end-web-development-podcasts
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/flutter-flame-engine-flappy-bird
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/firebase-multisite-hosting
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/firebase-hosting-version-settings
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/finding-a-company-with-great-values
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/design-systems-with-web-components
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/debug-angular-9-in-chrome-console
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/codingcatdev-is-joining-the-learn-build-teach-community
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/cfp-2024
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/building-a-web-application-with-next-js-and-firebase-firebase-summit-2021
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/changing-the-channel-for-youtube-engagement
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/angular-17-cypress-testing
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/anchor-links-from-sanity-in-gatsby
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/ajonp-hugo-ionic-template
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/adding-web-component-for-ionic-modals
+2026-03-25T14:56:14.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/terms-of-use
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/code-with-codingcatdev
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/blog-sponsorship
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/sponsorships
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/privacy-policy
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/ftc-disclosure
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/pro
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/infrastructure-driven-development
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/turning-documentation-into-a-product-best-practices-for-success
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/angular-cookbook
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/github-cms
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/vdo-ninja-woes-and-overlay-setup
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/how-to-automate-your-work-and-free-your-time
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/genai-for-engineers-what-s-real-what-s-not-and-what-s-coming
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/ai-for-system-design-and-architecture-documentation
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/automatic-show-notes-for-podcasts-and-videos
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/webdev-fundamentals-or-react-angular-and-vue
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/sveltekit-with-daisyui
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/ai-powered-persuasion-engineering-with-ben-bressington
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/supastarter-and-indie-hacking-with-jonathan-wilke
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/stop-killing-your-largest-contentful-paint-score-with-images
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/google-i-o-flutter-updates-and-firebase
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/firebase-sql-with-data-connect
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/not-a-designer-design-bites-for-devs
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/twitter-profile-banner-part-1
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/ai-for-peeps-scared-of-ai
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/unlocking-your-adhd-superpowers
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/cwcc-1-2-meltui
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-9-Earning-Digital-Badges-with-Holopin
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/4-2-compassionate-coding
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-19-angular-16-hydration-signals-vite
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-10-WebContainers-with-Eric-Simons
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-9-getting-graphiql-with-wordpress
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-1-using-no-code-with-builder-io-part-1
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-10-creative-coding-to-boost-your-skills
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-33-bringing-the-jamstack-together-with-tinacms
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/0-6-nextjs-with-guillermo-rauch
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/cwcc-1_refine
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/cwcc-1-3-svelte-view-transitions
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/cwcc-1-2-aws-amplify
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/cwcc-1-1-angular-17
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/cwcc-0-9-clerk-with-appwrite
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/cwcc-0-6-remix-with-kent-c-dodds
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/cwcc-0-8-amplify-ui
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/4-9-flutter-2024
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/4-8-design-systems-brad-frost
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/4-7-harness-the-power-of-localstack
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/4-6-supabase-release-week-x-new-features
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/4-4-raycast
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/4-3-leveling-up-your-career-with-tdesseyn
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/4-1-typescript-is-freaking-hard
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-holiday-special
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-8-Growing-your-Brand
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-7-Astro-2.0-with-Matthew-Phillips
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-4-RavenDB-with-Oren-Eini
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-3-Tea-CLI
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-25-your-fast-track-to-open-source
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-24-skyflow-privacy-vault
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-23-angular-17
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-21-slicing-up-sveltekit-with-prismic-slice-machine
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-22-fly-io-with-dov-alperin
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-20-flightcontrol-is-aws-but-awesome-with-brandon-bayer
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-2-How-to-create-a-Conference-with-Clark-Sell
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-18-prompt-engineering-with-chatgpt
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-17-sveltelab-a-supercharged-repl-for-svelte
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-15-prisma-with-alex-ruheni
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-14-adam-argyle-presents-css-features-for-2023-and-beyond
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-13-radix-svelte
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-11-build-a-frontend-web-framework-from-scratch-book
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-12-how-to-become-a-full-time-content-creator
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/3-1-Content-Creation-with-Richard-Bray
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-8-powering-up-with-gatsby-4-and-gatsbyconf
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-7-building-design-systems-with-backlight-dev
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-6-open-props-an-open-source-css-props-library
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-48-Fully-Passwordless-Authentication-with-Passage
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-47-Build-Healthy-OSS-Communities-with-OpenSauced
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-44-Storyblok-and-achieving-better-Accessibility-from-Developer-Experience
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-46-Breaking-Down-ViteConf
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-45-Breaking-into-Tech-through-Streaming
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-40-RedwoodJS
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-41-Nx-the-Smart-Fast-and-Extensible-Build-System
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-42-Pinia-Vue-New-State-Management
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-4-the-other-side-of-tech-a-documentarian-perspective
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-39-Plasmic-with-Ekene-Eze-Kenny
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-36-Building-in-CodeSandbox-with-Adewale-Ace-Abati
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-38-a11y-with-ben-myers
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-37-Developer-Events-and-Community-on-CFE-dev
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-35-podcasters-unite
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-34-WPEngine-and-FaustJS
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-33-Managing-DataOps-with-Meltano
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-32-ionic-6
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-31-creating-a-standardized-design-system-for-the-web
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-30-quicknode-builds-infrastructure-for-the-future
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-3-supabase-an-open-source-alternative
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-29-women-of-jamstack
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-28-nextjs-script-with-partytown-web-workers
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-27-automating-infrastructure-with-digger-dev
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-26-open-sourcing-your-database-with-appwrite
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-25-building-maintaining-design-systems
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-23-routes-for-svelte-with-routify
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-24-making-progress-with-kendo-ui
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-22-building-connections-for-the-underrepresented
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-20-regaining-java-script-performance-in-your-website
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-21-linking-your-microservices-with-workflows
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-2-using-no-code-with-builder-io-part-2
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-19-enterprise-databases-that-scale
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-18-making-the-web-easier-to-build-with-una-kavets
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-16-optimize-your-media-experience
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-17-building-the-future-of-svelte-at-vercel
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-15-stacks-on-stacks-on-stacks-with-code-stac-kr
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-13-managing-multiple-data-sources-in-graphql
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-14-building-up-better-communities
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/2-12-empower-your-content-with-prismic
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-9-safe-is-scrum-on-steriods
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-8-design-for-developers
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-7-socialbee-with-ovi-negrean-and-vlad-hosu
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-6-jamstack-handbook-with-colby-fayock
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-52-Holiday Funisode - Ask us Anything
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-50-making-state-management-domain-agnostic-with-derxjs
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-5-mdx-with-chris-biscardi
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-51-creating-opportunities-with-girl-develop-it
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-49-qwik-the-html-first-framework
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-47-unlocking-dxp-with-uniform
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-48-learn-to-build-reliable-and-efficient-software-in-rust
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-46-revealing-all-of-next-js-12-s-features
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-45-crossover-for-compressed-fm-and-purrfect-dev
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-44-composing-modern-applications-with-bit
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-42-using-content-as-data-with-sanity-io
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-43-scaling-your-javascript-career-with-ui-dev
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-41-making-tasty-courses-for-web-developers
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-4-next-js-10-with-guillermo-rauch
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-40-creating-code-in-your-browser-with-stackblitz
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-39-organizing-your-content-for-the-world-with-storyblok
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-38-creating-fake-data-with-tonic
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-36-reviewing-nextjs-features-with-tim-neutkens
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-35-the-coding-interview-and-transitioning-in-tech
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-34-making-flutter-games-with-flame-engine
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-37-building-better-authentication-with-clerk
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-32-side-project-hustle
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-31-delivering-digital-content-with-graphcms
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-30-journey-of-a-freelance-designer-developer
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-3-aws-amplify-admin-ui-with-rene-brandel
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-29-using-css-in-js-with-stitches
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-28-unlocking-the-jamstack-with-auth0
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-26-creating-great-ecommerce-with-jamstack
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-24-diving-deeper-into-nuxt-js
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-25-micro-frontends
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-23-creating-a-better-vue-with-nuxt
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-27-producing-newsletters-with-buttondown
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-22-hosting-nextjs-on-netlify
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-21-becoming-a-developer-advocate
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-20-100daysofcloud
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-19-teaching-others-to-code-lessons-learned
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-17-payload-cms
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-18-fostering-developer-education
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-16-delivering-a-content-mesh
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-15-whats-up-with-svelte
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-2-kinsta-brian-li
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-14-firestore-for-unity-c-plus-plus
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-13-creating-a-learning-platform
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-12-agility-cms
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-11-moving-from-wordpress-to-jamstack
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/1-1-infrastructure-as-code-with-pulumi
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/0-9-jeff-delaney-of-fireship-io
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/0-8-cloudinary-with-tessa-mero
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/0-5-agile-methodology
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/0-4-sanity-io
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/0-3-cloudinary-community
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/0-2-purrfect-program-management
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/0-10-amplify-with-nader-dabit
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/0-0-the-purrfect-beginning
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/0-1-purrfect-origin-story
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/build-a-custom-no-code-app-in-90-minutes
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/supabase-next-js-and-builder-io
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/sveltekit-with-notion-cms
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/how-oauth-mcp-and-the-openai-apps-sdk-power-the-next-generation-of-interactive-ai-experiences
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/firebase-security-rules-effortless-control-over-your-app-s-data
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/scaling-permissions-with-spicedb-rebac-explained
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/secure-coding-beyond-the-surface-with-snyk
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/how-to-add-sentry-to-next-js-the-right-way
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/building-successful-open-source-projects
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/using-firebase-with-communication-apis
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/running-the-cloud-on-your-desktop-with-localstack
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/secure-ai-access-control-using-spicedb
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/what-is-a-firebase-developer-advocate
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/sveltekit-with-notion-cms-and-cloudinary
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/podcast/identity-access-management-for-agents-with-tobin-south
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/how-to-secure-vercel-cron-job-routes-in-next-js-14-app-router
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/firebase-firestore-vs-data-connect-unlocking-your-data-s-full-potential
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/vscode-extension-peacock
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/use-firestore-to-build-hugo-content
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/slack-workspace-theme-customization
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/npm-tips-and-tricks
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/jamstackgr-2-git-your-build-system-right-vs-deploying-fast
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/building-a-fun-app-with-ionic-studio-and-cloudinary
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/backup-sanity-with-cloud-firestore
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/better-performance-through-analysis
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/aws-amplify-vs-aws-cdk-2020
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/angular-cli-deploying
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/accessibility-best-practices-for-html-and-css
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/2023-databases-for-serverless
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/the-myth-of-just-press-record-in-podcasting
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/the-7-strategies-i-used-to-defeat-imposter-syndrome
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/supabase-nextjs-and-builderio
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/slap-someone-with-slack-commands
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/send-gcp-build-status-to-discord
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/reflections-from-2021-and-goals-for-2022
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/lets-learn-svelte
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/jamstackgr-1-sanity-io-as-a-backend
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/ionic-icons
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/install-tailwindcss-in-svelte-with-1-command
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/google-cloud-repositories-ci-cd
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/firebase-app-check-web
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/content-modeling-in-a-headless-cms-vs-a-traditional-webcms
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/cloudinary-in-jamstacks-using-webp
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/cloud-function-github-update
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/angular-pwa-to-google-play-store-using-trusted-web-activity
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/aj-is-loving-sticker-mule
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/post/adding-angular-components-to-your-static-site
+2026-03-25T14:56:13.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/author/brittney-postma
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/author/alex-patterson
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/author/nick-rouech
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/author/lukas-spydon
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/author/anthony-campolo
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/sathiesh-veera
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/scott-rees
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/vincent-wen-yu-ge
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/muhammad-ahsan-ayaz
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/balki-kodarapu
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/tobin-south
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/andrea-wu
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/chris-sev
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/raymond-camden
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/tyler-crowe
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/chris-ferdinandi
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/sam-kim
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/luca-forstner
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/corbin-crutchley
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/max-gerber
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/benjamin-bressington
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/joe-casabona
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/brittney-postma-guest
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/roman-jaquez
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/zoe-wilson
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/zack-derose
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/willow
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/wes-bos
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/waldemar-hummer
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/vlad-hosu
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/vagish-vela
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/una-kravets
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/tyler-mcginnis
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/tomek-sulkowski
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/tom-preston-werner
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/tim-neutkens
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/tim-benniks
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/thomas-g-lopes
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/tessa-mero
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/taylor-desseyn
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/swyx
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/steve-sewell
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/stefan-kingham
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/simon-grimm
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/shivay-lamba
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/shai-reznik
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/sean-falconer
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/scott-spence
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/sarah-kapehe
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/sam-littlefair
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/sam-bhagwat
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/salma-alam-naylor
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/rob-sutter
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/ryan-carniato
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/richard-bray
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/rene-brandel
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/rich_harris
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/renan
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/randal-schwartz
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/scott-tolinski
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/prince-wilson
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/pngwn
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/patricio-vargas
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/patrick-martin
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/pedro-duarte
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/paolo-ricciuti
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/ovi-negrean
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/pariss-athena
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/nick-hodges
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/nick-rouech
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/omed-habib
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/oren-eini
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/nick-dejesus
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/misko-hevery
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/nader-dabit
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/mike-hartington
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/michael-dowden
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/michael-chan
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/maya-shavin
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/max-howell
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/matthew-phillips
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/m4dz
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/matias-capeletto
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/lukas-spydon
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/lee-briggs
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/lee-robinson
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/laurie-barth
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/lauren-lee
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/knut-melvaer
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/kelly-vaughn
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/kent-c-dodds
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/lane-wagner
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/kellen-mace
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/kathryn-grayson
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/katie-franco
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/katerina-skroumpelou
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/justin-duke
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/justin-schwartzenberger
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/juan-andres
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/juri-strumpflohner
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/josefine-schaefer
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/jordan-powell
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/jon-meyers
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/jesse-hall
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/jochum-wolfen
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/joel-varty
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/jenn-junod
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/jhey-tompkins
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/jeff-delaney
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/jason-lengstorf
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/jason-bahl
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/james-vidler
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/james-r-perkins
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/james-q-quick
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/igor-zalutski
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/jake-rosenberg
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/houssein-djirdeh
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/heather-buchel
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/greg-whitworth
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/grayson-hicks
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/guillermo-rauch
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/fred-k-schott
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/flor-antara
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/erik-hanchett
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/facundo-giuliani
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/eric-simons
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/elizabeth-wilson
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/elizabeth-bush
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/emma-twersky
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/elena-lape
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/dov-alperin
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/ekene-eze
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/eduardo-san-martin-morote
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/douwe-maan
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/domitrius-clark
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/debbie-o-brien
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/daniel-roe
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/daniel-ribbens
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/clark-sell
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/colby-fayock
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/christy-ennis-kloote
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/christopher-burns
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/chris-swithinbank
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/cassidoo
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/chris-biscardi
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/bryan-robinson
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/brian-li
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/brian-rinaldi
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/brittany-walker
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/brian-douglas
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/brandon-roberts
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/brandon-bayer
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/brett-fitzgerald
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/braden-sidoti
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/brad-frost
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/bekah-hawrot-weigel
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/ben-myers
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/ben-hong
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/april-wensel
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/batuhan-ozdemir
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/brad-garropy
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/antonio-sarcevic
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/anthony-campolo
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/anuradha-kumari
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/antonio-lo-fiego
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/ankita-kulkarni
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/angel-sola-orbaiceta
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/amanda-giannelli
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/alyssa-nicoll
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/amy-dutton
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/alexander-gallego
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/ali-spittel
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/alexander-lichter
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/alex-trost
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/alex-jover-morales
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/alex-gogan
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/alex-ruheni
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/alex-feldman
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/adam-argyle
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/ace-kyd
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/adam-bradley
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/aaron-frost
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/saron-yitbarek
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/amanda-cavallaro
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/thomas-johnson
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/brian-clark
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/jonathan-wilke
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/tamas-piros
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/mark-huggins
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/nick-taylor
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/guest/dan-hafner
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/sponsor/multiplayer
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/sponsor/authzed
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/sponsor/storyblok
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/sponsor/stickermule
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/sponsor/refine
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/sponsor/jamstackconf
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/sponsor/cloudinary
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/sponsor/builder-io
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
+https://codingcat.dev/sponsor/algolia
+2026-03-25T14:56:10.000Z
+monthly
+0.5
+
+
diff --git a/apps/site/biome.json b/apps/site/biome.json
new file mode 100644
index 000000000..bbe0489c9
--- /dev/null
+++ b/apps/site/biome.json
@@ -0,0 +1,67 @@
+{
+ "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
+ "vcs": {
+ "enabled": true,
+ "clientKind": "git",
+ "defaultBranch": "main",
+ "useIgnoreFile": true
+ },
+ "files": {
+ "includes": [
+ "**/*.ts",
+ "**/*.tsx",
+ "**/*.js",
+ "**/*.mjs",
+ "!**/node_modules",
+ "!**/dist",
+ "!**/.astro",
+ "!**/.wrangler",
+ "!src/lib/sanity/types.gen.ts",
+ "!worker-configuration.d.ts",
+ "!baseline"
+ ]
+ },
+ "formatter": {
+ "enabled": true,
+ "useEditorconfig": true
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "recommended": true,
+ "suspicious": {
+ "noExplicitAny": "off",
+ "noArrayIndexKey": "off"
+ },
+ "complexity": {
+ "noForEach": "off"
+ },
+ "correctness": {
+ "useExhaustiveDependencies": "off",
+ "noUnusedFunctionParameters": "warn"
+ },
+ "style": {
+ "noUnusedTemplateLiteral": { "level": "error", "fix": "safe" },
+ "noNonNullAssertion": "warn",
+ "useBlockStatements": "error"
+ },
+ "a11y": {
+ "noSvgWithoutTitle": "off"
+ }
+ }
+ },
+ "assist": {
+ "actions": {
+ "source": {
+ "organizeImports": "on"
+ }
+ }
+ },
+ "javascript": {
+ "jsxRuntime": "transparent",
+ "formatter": {
+ "trailingCommas": "all",
+ "semicolons": "always"
+ }
+ }
+}
diff --git a/apps/site/package.json b/apps/site/package.json
new file mode 100644
index 000000000..74608ed87
--- /dev/null
+++ b/apps/site/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "@codingcatdev/site",
+ "type": "module",
+ "private": true,
+ "engines": {
+ "node": ">=22.12"
+ },
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "build:production": "CLOUDFLARE_ENV=production astro build",
+ "preview": "astro build && wrangler dev",
+ "check": "astro check",
+ "typecheck": "astro check",
+ "lint": "biome check .",
+ "format": "biome format . --write",
+ "cf-typegen": "CI=true wrangler types",
+ "test:e2e": "node tests/run-e2e.mjs"
+ },
+ "dependencies": {
+ "@astrojs/cloudflare": "^14.2.1",
+ "@astrojs/react": "^6.0.2",
+ "@portabletext/to-html": "^6.0.0",
+ "@portabletext/types": "^4.0.2",
+ "@sanity/client": "^7.26.2",
+ "@sanity/image-url": "^2.1.1",
+ "@sanity/visual-editing-standalone": "^2.0.1",
+ "@tailwindcss/vite": "^4.2.1",
+ "astro": "^7.2.2",
+ "astro-portabletext": "^0.13.0",
+ "groq": "^6.9.2",
+ "react": "^19.2.6",
+ "react-dom": "^19.2.6",
+ "tailwindcss": "^4.2.1",
+ "workers-og": "^0.0.27"
+ },
+ "devDependencies": {
+ "@astrojs/check": "^0.9.10",
+ "@biomejs/biome": "2.2.4",
+ "@tailwindcss/typography": "^0.5.19",
+ "@types/node": "^24.12.4",
+ "typescript": "^5.9",
+ "wrangler": "^4.123.0"
+ }
+}
diff --git a/apps/web/public/.assetsignore b/apps/site/public/.assetsignore
similarity index 100%
rename from apps/web/public/.assetsignore
rename to apps/site/public/.assetsignore
diff --git a/apps/site/public/ads.txt b/apps/site/public/ads.txt
new file mode 100644
index 000000000..67a1db01e
--- /dev/null
+++ b/apps/site/public/ads.txt
@@ -0,0 +1 @@
+google.com, pub-8603586563858288, DIRECT, f08c47fec0942fa0
\ No newline at end of file
diff --git a/apps/site/public/apple-touch-icon.png b/apps/site/public/apple-touch-icon.png
new file mode 100644
index 000000000..7c160366a
Binary files /dev/null and b/apps/site/public/apple-touch-icon.png differ
diff --git a/apps/site/public/favicon-16x16.png b/apps/site/public/favicon-16x16.png
new file mode 100644
index 000000000..c7e13aeae
Binary files /dev/null and b/apps/site/public/favicon-16x16.png differ
diff --git a/apps/site/public/favicon-32x32.png b/apps/site/public/favicon-32x32.png
new file mode 100644
index 000000000..33956da3c
Binary files /dev/null and b/apps/site/public/favicon-32x32.png differ
diff --git a/apps/site/public/favicon.ico b/apps/site/public/favicon.ico
new file mode 100644
index 000000000..1bbde665e
Binary files /dev/null and b/apps/site/public/favicon.ico differ
diff --git a/apps/site/public/icon.png b/apps/site/public/icon.png
new file mode 100644
index 000000000..6e05a1ec0
Binary files /dev/null and b/apps/site/public/icon.png differ
diff --git a/apps/site/public/icon.svg b/apps/site/public/icon.svg
new file mode 100644
index 000000000..6826590ef
--- /dev/null
+++ b/apps/site/public/icon.svg
@@ -0,0 +1,35 @@
+
diff --git a/apps/web/src/assets/fonts/Inter-Bold.ttf b/apps/site/src/assets/fonts/Inter-Bold.ttf
similarity index 100%
rename from apps/web/src/assets/fonts/Inter-Bold.ttf
rename to apps/site/src/assets/fonts/Inter-Bold.ttf
diff --git a/apps/web/src/assets/fonts/Inter-Regular.ttf b/apps/site/src/assets/fonts/Inter-Regular.ttf
similarity index 100%
rename from apps/web/src/assets/fonts/Inter-Regular.ttf
rename to apps/site/src/assets/fonts/Inter-Regular.ttf
diff --git a/apps/site/src/components/AnimatedHero.astro b/apps/site/src/components/AnimatedHero.astro
new file mode 100644
index 000000000..1e2d79fd3
--- /dev/null
+++ b/apps/site/src/components/AnimatedHero.astro
@@ -0,0 +1,48 @@
+---
+import AjHeadphones from "@/components/icons/AjHeadphones.astro";
+import AjPrimaryAlt from "@/components/icons/AjPrimaryAlt.astro";
+import { HERO_ICONS } from "@/lib/hero-icons";
+
+/**
+ * Ported from the Next `animated-hero.tsx`. Its only state was `isHovered`,
+ * driving a transform on the two paw marks — expressed here with
+ * `group-hover:`, so the hero ships no JavaScript at all.
+ */
+---
+
+
+
+ {
+ HERO_ICONS.map((icon, index) => (
+
+
+
+ ))
+ }
+
+
+
+
+ CodingCat.dev Podcast
+
+
+ Purrfect Podcast for Web Developers
+
+
+
+
+
diff --git a/apps/site/src/components/Avatar.astro b/apps/site/src/components/Avatar.astro
new file mode 100644
index 000000000..7a1df277c
--- /dev/null
+++ b/apps/site/src/components/Avatar.astro
@@ -0,0 +1,65 @@
+---
+import { stegaClean } from "@sanity/client/stega";
+import type { SanityImageSource } from "@sanity/image-url";
+
+interface Props {
+ name?: string | null;
+ href?: string | null;
+ coverImage?: SanityImageSource | null;
+ imgSize?: string;
+ width?: number;
+ height?: number;
+}
+
+const {
+ name,
+ href,
+ coverImage,
+ imgSize = "w-12 h-12 mr-4",
+ width = 48,
+ height = 48,
+} = Astro.props;
+
+const src = Astro.locals.sanity
+ .urlForImage(coverImage ?? undefined)
+ ?.width(width)
+ .height(height)
+ .url();
+
+// stegaClean: href is an attribute; zero-width characters break the URL.
+const cleanHref = stegaClean(href);
+---
+
+{
+ src &&
+ (cleanHref ? (
+
+
+

+
+ {name && (
+ {name}
+ )}
+
+ ) : (
+
+

+
+ ))
+}
diff --git a/apps/site/src/components/Breadcrumbs.astro b/apps/site/src/components/Breadcrumbs.astro
new file mode 100644
index 000000000..d222800c4
--- /dev/null
+++ b/apps/site/src/components/Breadcrumbs.astro
@@ -0,0 +1,33 @@
+---
+interface Props {
+ links: Array<{ title: string; href?: string }>;
+}
+
+const { links } = Astro.props;
+---
+
+
diff --git a/apps/site/src/components/Container.astro b/apps/site/src/components/Container.astro
new file mode 100644
index 000000000..9381a9809
--- /dev/null
+++ b/apps/site/src/components/Container.astro
@@ -0,0 +1,11 @@
+---
+interface Props {
+ class?: string;
+}
+
+const { class: className = "" } = Astro.props;
+---
+
+
+
+
diff --git a/apps/site/src/components/ContentCard.astro b/apps/site/src/components/ContentCard.astro
new file mode 100644
index 000000000..788838df5
--- /dev/null
+++ b/apps/site/src/components/ContentCard.astro
@@ -0,0 +1,73 @@
+---
+import Avatar from "@/components/Avatar.astro";
+import CoverImage from "@/components/CoverImage.astro";
+import FormattedDate from "@/components/FormattedDate.astro";
+import { type ContentListItem, isPerson } from "@/lib/content";
+import { resolveHref } from "@/lib/sanity/resolve-href";
+
+interface Props {
+ item: ContentListItem;
+ priority?: boolean;
+}
+
+const { item, priority = false } = Astro.props;
+
+const href = resolveHref(item._type, item.slug ?? undefined);
+const person = isPerson(item._type);
+const people = [
+ ...(item.author ?? []).map((p) => ({ ...p, type: "author" })),
+ ...(item.guest ?? []).map((p) => ({ ...p, type: "guest" })),
+];
+---
+
+
+ {
+ href && (
+
+ {person ? (
+
+ ) : (
+
+ )}
+
+ )
+ }
+
+ {
+ href ? (
+
+ {item.title}
+
+ ) : (
+ item.title
+ )
+ }
+
+ {
+ !person && item.date && (
+
+
+
+ )
+ }
+ {item.excerpt && {item.excerpt}
}
+ {
+ people.length > 0 && (
+
+ {people.map((p) => (
+
+ ))}
+
+ )
+ }
+
diff --git a/apps/site/src/components/ContentGrid.astro b/apps/site/src/components/ContentGrid.astro
new file mode 100644
index 000000000..2b3d2fff9
--- /dev/null
+++ b/apps/site/src/components/ContentGrid.astro
@@ -0,0 +1,22 @@
+---
+import ContentCard from "@/components/ContentCard.astro";
+import type { ContentListItem } from "@/lib/content";
+
+interface Props {
+ items: ContentListItem[];
+ /** First N cards load eagerly; they are typically above the fold. */
+ priorityCount?: number;
+ class?: string;
+}
+
+const { items, priorityCount = 2, class: className } = Astro.props;
+---
+
+
+ {items.map((item, index) => )}
+
diff --git a/apps/site/src/components/CoverImage.astro b/apps/site/src/components/CoverImage.astro
new file mode 100644
index 000000000..3a27de766
--- /dev/null
+++ b/apps/site/src/components/CoverImage.astro
@@ -0,0 +1,54 @@
+---
+import { stegaClean } from "@sanity/client/stega";
+import type { SanityImageSource } from "@sanity/image-url";
+
+interface Props {
+ image?: (SanityImageSource & { alt?: string }) | null;
+ width?: number;
+ height?: number;
+ quality?: number;
+ /** Above-the-fold images opt out of lazy loading. */
+ priority?: boolean;
+ class?: string;
+}
+
+const {
+ image,
+ width = 1920,
+ height = 1080,
+ quality = 80,
+ priority = false,
+ class: className = "w-full h-auto aspect-video rounded-md",
+} = Astro.props;
+
+const src = Astro.locals.sanity
+ .urlForImage(image ?? undefined)
+ ?.width(width)
+ .height(height)
+ .quality(quality)
+ .url();
+
+// stegaClean: alt lands in an attribute, where stega's zero-width characters
+// would be read out verbatim by screen readers.
+const alt = stegaClean((image as { alt?: string } | null)?.alt) ?? "";
+---
+
+
+ {
+ src ? (
+

+ ) : (
+
+ )
+ }
+
diff --git a/apps/site/src/components/CoverMedia.astro b/apps/site/src/components/CoverMedia.astro
new file mode 100644
index 000000000..f31bc3bd2
--- /dev/null
+++ b/apps/site/src/components/CoverMedia.astro
@@ -0,0 +1,61 @@
+---
+import { stegaClean } from "@sanity/client/stega";
+import type { SanityImageSource } from "@sanity/image-url";
+import CoverImage from "@/components/CoverImage.astro";
+import PtYouTube from "@/components/portable-text/PtYouTube.astro";
+
+/**
+ * Picks the hero medium for a content page, in the Next app's precedence:
+ * uploaded video, then YouTube, then the cover image.
+ */
+interface Props {
+ image?: (SanityImageSource & { alt?: string }) | null;
+ video?: { asset?: { _ref?: string } } | null;
+ youtube?: string | null;
+ class?: string;
+}
+
+const { image, video, youtube, class: className } = Astro.props;
+
+/**
+ * Sanity file refs are `file-{hash}-{ext}`. The Next version read the project
+ * id and dataset from `process.env.NEXT_PUBLIC_*` at module scope, which does
+ * not exist on Workers — resolved from the per-request config instead.
+ */
+function fileUrl(ref: string | undefined) {
+ if (!ref?.startsWith("file-")) {
+ return undefined;
+ }
+ const parts = ref.split("-");
+ const ext = parts.pop();
+ const hash = parts.slice(1).join("-");
+ if (!ext || !hash) {
+ return undefined;
+ }
+ const { projectId, dataset } = Astro.locals.sanity.config;
+ return `https://cdn.sanity.io/files/${projectId}/${dataset}/${hash}.${ext}`;
+}
+
+const videoUrl = fileUrl(stegaClean(video?.asset?._ref));
+const youtubeUrl = stegaClean(youtube);
+---
+
+{
+ videoUrl ? (
+
+
+
+ ) : youtubeUrl ? (
+
+ ) : (
+
+ )
+}
diff --git a/apps/site/src/components/DraftModeToggle.astro b/apps/site/src/components/DraftModeToggle.astro
new file mode 100644
index 000000000..999e8eae5
--- /dev/null
+++ b/apps/site/src/components/DraftModeToggle.astro
@@ -0,0 +1,22 @@
+---
+/**
+ * Only rendered while the preview cookie is set. Posts to the disable
+ * endpoint rather than linking, so the redirect back to the current page
+ * cannot be triggered by a crawler following a GET.
+ */
+const returnTo = Astro.url.pathname + Astro.url.search;
+---
+
+
diff --git a/apps/site/src/components/Footer.astro b/apps/site/src/components/Footer.astro
new file mode 100644
index 000000000..3f430e7fa
--- /dev/null
+++ b/apps/site/src/components/Footer.astro
@@ -0,0 +1,106 @@
+---
+import NavLink from "./NavLink.astro";
+
+const year = new Date().getUTCFullYear();
+
+const legalLinks = [
+ { href: "/ftc-disclosure", label: "FTC Disclosure" },
+ { href: "/terms-of-use", label: "Terms of Use" },
+ { href: "/privacy-policy", label: "Privacy Policy" },
+ { href: "/sponsors", label: "Sponsors" },
+ { href: "/sponsorships", label: "Sponsorships" },
+];
+
+// Inlined rather than pulled from react-icons — four glyphs do not justify
+// shipping an icon library (and its React runtime) to every page.
+const socialLinks = [
+ {
+ href: "https://twitter.com/codingcatdev",
+ label: "X",
+ path: "M18.9 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z",
+ },
+ {
+ href: "https://github.com/codingcatdev",
+ label: "GitHub",
+ path: "M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12",
+ },
+ {
+ href: "https://linkedin.com/company/codingcatdev",
+ label: "LinkedIn",
+ path: "M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286ZM5.337 7.433a2.062 2.062 0 1 1 0-4.124 2.062 2.062 0 0 1 0 4.124ZM7.119 20.452H3.555V9h3.564v11.452ZM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003Z",
+ },
+ {
+ href: "https://youtube.com/codingcatdev",
+ label: "YouTube",
+ path: "M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814ZM9.545 15.568V8.432L15.818 12l-6.273 3.568Z",
+ },
+];
+---
+
+
diff --git a/apps/site/src/components/FormattedDate.astro b/apps/site/src/components/FormattedDate.astro
new file mode 100644
index 000000000..d940a45f1
--- /dev/null
+++ b/apps/site/src/components/FormattedDate.astro
@@ -0,0 +1,38 @@
+---
+/**
+ * Replaces the Next app's date-fns `format(..., "LLLL d, yyyy")` with
+ * Intl.DateTimeFormat — same output, no dependency, and no locale data
+ * shipped into the Worker bundle.
+ *
+ * The timeZone is pinned to UTC so a Worker in any colo renders the same
+ * date as the `datetime` attribute. Without it, a post dated
+ * 2026-08-18T02:00Z renders as "August 17" from a US-hosted isolate.
+ */
+import { stegaClean } from "@sanity/client/stega";
+
+interface Props {
+ dateString?: string | null;
+ class?: string;
+}
+
+const { dateString, class: className } = Astro.props;
+
+const clean = stegaClean(dateString);
+const parsed = clean ? new Date(clean) : null;
+const valid = parsed && !Number.isNaN(parsed.getTime()) ? parsed : null;
+
+const formatter = new Intl.DateTimeFormat("en-US", {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ timeZone: "UTC",
+});
+---
+
+{
+ valid && (
+
+ )
+}
diff --git a/apps/site/src/components/Header.astro b/apps/site/src/components/Header.astro
new file mode 100644
index 000000000..008031a3c
--- /dev/null
+++ b/apps/site/src/components/Header.astro
@@ -0,0 +1,139 @@
+---
+import { settingsQuery } from "@/lib/sanity/queries";
+import NavLink from "./NavLink.astro";
+import ThemeToggle from "./ThemeToggle.astro";
+import SearchModal from "./SearchModal.astro";
+
+const settings = await Astro.locals.sanity.loadQuery(settingsQuery);
+const navLinks = settings?.navLinks ?? [];
+
+// The Next header split these across a desktop