diff --git a/.github/workflows/syndicate-post.yml b/.github/workflows/syndicate-post.yml index aa83c1e5b..6ce76e300 100644 --- a/.github/workflows/syndicate-post.yml +++ b/.github/workflows/syndicate-post.yml @@ -51,6 +51,9 @@ jobs: elif [ "$TARGET_DOCUMENT_ID" = "firebase-archive-2017-2024" ]; then FIREBASE_ARCHIVE_YEARS=2017,2018,2019,2020,2021,2022,2023,2024 SANITY_STUDIO_DATASET=production node scripts/generate-firebase-archive-posts.mjs FIREBASE_ARCHIVE_YEARS=2017,2018,2019,2020,2021,2022,2023,2024 SANITY_STUDIO_DATASET=dev node scripts/generate-firebase-archive-posts.mjs + elif [ "$TARGET_DOCUMENT_ID" = "publish-circular-ai-s1" ]; then + SANITY_STUDIO_DATASET=production node scripts/publish-circular-ai-s1-post.mjs + SANITY_STUDIO_DATASET=dev node scripts/publish-circular-ai-s1-post.mjs else node scripts/run-syndicate-post.mjs fi diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 000000000..fbb8815cd --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,10 @@ +# CodingCat.dev Monorepo Always-On Skills & Editorial Rules + +Every interactive session, subagent, and script in this repository (`codingcat.dev`) MUST follow these live CodingCat.dev skills: + +- @[alex-voice-and-copy](/home/alex/.gemini/config/skills/alex-voice-and-copy/SKILL.md) +- @[alex-writing-style](/home/alex/.gemini/config/skills/alex-voice-and-copy/references/writing-style.md) +- @[alex-wtf-is-series](/home/alex/.gemini/config/skills/alex-voice-and-copy/references/wtf-is-series.md) +- @[codingcat-design-system](/home/alex/.gemini/config/skills/codingcat-design-system/SKILL.md) +- @[codingcat-media-studio](/home/alex/.gemini/config/skills/codingcat-media-studio/SKILL.md) +- @[codingcat-diagrams](/home/alex/.gemini/config/skills/codingcat-media-studio/references/excalidraw-diagrams.md) diff --git a/apps/sanity/schemas/custom/social-embed.ts b/apps/sanity/schemas/custom/social-embed.ts new file mode 100644 index 000000000..e5e3736df --- /dev/null +++ b/apps/sanity/schemas/custom/social-embed.ts @@ -0,0 +1,151 @@ +import { defineType } from "sanity"; +import type { IconType } from "react-icons"; +import { + FaBluesky, + FaGithub, + FaInstagram, + FaLinkedin, + FaMastodon, + FaReddit, + FaShareNodes, + FaThreads, + FaTiktok, +} from "react-icons/fa6"; + +const socialFields = [ + { + name: "platform", + type: "string", + title: "Platform", + description: "Optional: auto-detected from URL when left blank.", + options: { + list: [ + { title: "Auto-detect from URL", value: "auto" }, + { title: "X (Twitter)", value: "x" }, + { title: "Bluesky", value: "bluesky" }, + { title: "LinkedIn", value: "linkedin" }, + { title: "Reddit", value: "reddit" }, + { title: "TikTok", value: "tiktok" }, + { title: "Instagram", value: "instagram" }, + { title: "Threads", value: "threads" }, + { title: "Mastodon", value: "mastodon" }, + { title: "GitHub Gist", value: "gist" }, + { title: "YouTube", value: "youtube" }, + ], + }, + }, + { + name: "url", + type: "url", + title: "Post URL", + validation: (rule: { required: () => unknown }) => rule.required(), + }, + { + name: "author", + type: "string", + title: "Author Name (Optional)", + }, + { + name: "handle", + type: "string", + title: "Author Handle (Optional)", + }, + { + name: "fallbackText", + type: "text", + title: "Post Text / Fallback Excerpt (Optional)", + rows: 3, + }, + { + name: "caption", + type: "string", + title: "Caption (Optional)", + }, +]; + +function createPlatformEmbedSchema( + name: string, + title: string, + defaultPlatform: string, + icon: IconType, +) { + return defineType({ + name, + type: "object", + title, + icon, + fields: socialFields, + initialValue: { + platform: defaultPlatform, + }, + preview: { + select: { + title: "author", + subtitle: "url", + }, + prepare({ title: authorName, subtitle }) { + return { + title: authorName ? `${title}: ${authorName}` : title, + subtitle: subtitle || "", + }; + }, + }, + }); +} + +export const socialEmbed = createPlatformEmbedSchema( + "socialEmbed", + "Social Media Embed", + "auto", + FaShareNodes, +); +export const blueskyEmbed = createPlatformEmbedSchema( + "bluesky", + "Bluesky Embed", + "bluesky", + FaBluesky, +); +export const linkedinEmbed = createPlatformEmbedSchema( + "linkedin", + "LinkedIn Embed", + "linkedin", + FaLinkedin, +); +export const redditEmbed = createPlatformEmbedSchema( + "reddit", + "Reddit Embed", + "reddit", + FaReddit, +); +export const tiktokEmbed = createPlatformEmbedSchema( + "tiktok", + "TikTok Embed", + "tiktok", + FaTiktok, +); +export const instagramEmbed = createPlatformEmbedSchema( + "instagram", + "Instagram Embed", + "instagram", + FaInstagram, +); +export const threadsEmbed = createPlatformEmbedSchema( + "threads", + "Threads Embed", + "threads", + FaThreads, +); +export const mastodonEmbed = createPlatformEmbedSchema( + "mastodon", + "Mastodon Embed", + "mastodon", + FaMastodon, +); +export const githubGistEmbed = createPlatformEmbedSchema( + "githubGist", + "GitHub Gist Embed", + "gist", + FaGithub, +); + +export default socialEmbed; diff --git a/apps/sanity/schemas/custom/twitter-embed.ts b/apps/sanity/schemas/custom/twitter-embed.ts index 43bb927ef..a15320323 100644 --- a/apps/sanity/schemas/custom/twitter-embed.ts +++ b/apps/sanity/schemas/custom/twitter-embed.ts @@ -1,28 +1,57 @@ import { defineType } from "sanity"; -import { FaTwitter } from "react-icons/fa6"; +import { FaXTwitter } from "react-icons/fa6"; import preview from "../../components/TwitterPreview"; export default defineType({ name: "twitter", type: "object", - title: "Twitter Embed", - icon: FaTwitter, + title: "X (Twitter) Post Embed", + icon: FaXTwitter, components: { preview, }, preview: { select: { id: "id", + url: "url", + handle: "handle", }, }, fields: [ { name: "id", type: "string", - title: "Twitter(X) tweet id", + title: "X (Twitter) Post ID or URL", + description: "Paste either the numeric status ID or the full x.com / twitter.com status URL.", + }, + { + name: "url", + type: "url", + title: "Full Post URL (Optional)", + }, + { + name: "author", + type: "string", + title: "Author Display Name (Optional)", + }, + { + name: "handle", + type: "string", + title: "Author Handle (Optional, without @)", + }, + { + name: "text", + type: "text", + title: "Post Text Fallback (Optional)", + rows: 3, + }, + { + name: "date", + type: "string", + title: "Display Date (Optional)", }, ], initialValue: { - id: "1083592734038929408", + id: "2114705042817110293", }, }); diff --git a/apps/sanity/schemas/partials/base.ts b/apps/sanity/schemas/partials/base.ts index 8074d4b45..63558794c 100644 --- a/apps/sanity/schemas/partials/base.ts +++ b/apps/sanity/schemas/partials/base.ts @@ -17,6 +17,16 @@ import twitter from "../custom/twitter-embed"; import quote from "../custom/quote"; import youtube from "../custom/youtube"; import youtubeShorts from "../custom/youtube-shorts"; +import socialEmbed, { + blueskyEmbed, + githubGistEmbed, + instagramEmbed, + linkedinEmbed, + mastodonEmbed, + redditEmbed, + threadsEmbed, + tiktokEmbed, +} from "../custom/social-embed"; const baseType = defineType({ name: "base", @@ -101,6 +111,15 @@ const baseType = defineType({ defineArrayMember(codepen), defineArrayMember(codesandbox), defineArrayMember(twitter), + defineArrayMember(socialEmbed), + defineArrayMember(blueskyEmbed), + defineArrayMember(linkedinEmbed), + defineArrayMember(redditEmbed), + defineArrayMember(tiktokEmbed), + defineArrayMember(instagramEmbed), + defineArrayMember(threadsEmbed), + defineArrayMember(mastodonEmbed), + defineArrayMember(githubGistEmbed), defineArrayMember(html), defineArrayMember(quote), defineArrayMember({ type: "table" }), diff --git a/apps/site/public/images/posts/anthropic-s1-circular-ai/ai-titans-circular-handshake-hero.jpg b/apps/site/public/images/posts/anthropic-s1-circular-ai/ai-titans-circular-handshake-hero.jpg new file mode 100644 index 000000000..935801189 Binary files /dev/null and b/apps/site/public/images/posts/anthropic-s1-circular-ai/ai-titans-circular-handshake-hero.jpg differ diff --git a/apps/site/public/images/posts/anthropic-s1-circular-ai/diagram-1-ai-circular-investment-loop.jpg b/apps/site/public/images/posts/anthropic-s1-circular-ai/diagram-1-ai-circular-investment-loop.jpg new file mode 100644 index 000000000..d7e9e0143 Binary files /dev/null and b/apps/site/public/images/posts/anthropic-s1-circular-ai/diagram-1-ai-circular-investment-loop.jpg differ diff --git a/apps/site/public/images/posts/anthropic-s1-circular-ai/diagram-2-cross-stack-tech-dependencies.jpg b/apps/site/public/images/posts/anthropic-s1-circular-ai/diagram-2-cross-stack-tech-dependencies.jpg new file mode 100644 index 000000000..8122d34dc Binary files /dev/null and b/apps/site/public/images/posts/anthropic-s1-circular-ai/diagram-2-cross-stack-tech-dependencies.jpg differ diff --git a/apps/site/public/images/posts/anthropic-s1-circular-ai/diagram-3-dario-amodei-vision-vs-s1-reality.jpg b/apps/site/public/images/posts/anthropic-s1-circular-ai/diagram-3-dario-amodei-vision-vs-s1-reality.jpg new file mode 100644 index 000000000..baca15234 Binary files /dev/null and b/apps/site/public/images/posts/anthropic-s1-circular-ai/diagram-3-dario-amodei-vision-vs-s1-reality.jpg differ diff --git a/apps/site/public/images/posts/anthropic-s1-circular-ai/diagram-4-etf-401k-contagion-dominoes.jpg b/apps/site/public/images/posts/anthropic-s1-circular-ai/diagram-4-etf-401k-contagion-dominoes.jpg new file mode 100644 index 000000000..1925cff25 Binary files /dev/null and b/apps/site/public/images/posts/anthropic-s1-circular-ai/diagram-4-etf-401k-contagion-dominoes.jpg differ diff --git a/apps/site/public/images/posts/anthropic-s1-circular-ai/diagram-5-meta-gemini-anthropic-google-cloud.jpg b/apps/site/public/images/posts/anthropic-s1-circular-ai/diagram-5-meta-gemini-anthropic-google-cloud.jpg new file mode 100644 index 000000000..a667abcb7 Binary files /dev/null and b/apps/site/public/images/posts/anthropic-s1-circular-ai/diagram-5-meta-gemini-anthropic-google-cloud.jpg differ diff --git a/apps/site/src/components/portable-text/PortableTextContent.astro b/apps/site/src/components/portable-text/PortableTextContent.astro index 4622f183c..771fb14db 100644 --- a/apps/site/src/components/portable-text/PortableTextContent.astro +++ b/apps/site/src/components/portable-text/PortableTextContent.astro @@ -11,6 +11,7 @@ import PtImage from "./PtImage.astro"; import PtInternalLink from "./PtInternalLink.astro"; import PtLink from "./PtLink.astro"; import PtQuote from "./PtQuote.astro"; +import PtSocialEmbed from "./PtSocialEmbed.astro"; import PtTable from "./PtTable.astro"; import PtTweet from "./PtTweet.astro"; import PtYouTube from "./PtYouTube.astro"; @@ -19,7 +20,7 @@ import PtYouTubeShorts from "./PtYouTubeShorts.astro"; interface Props { /** * Sanity TypeGen emits `children?:` as optional on blocks, while - * `PortableTextBlock` requires it — so the generated content types are not + * `PortableTextBlock` requires it, so the generated content types are not * assignable to it. `TypedObject` is the library's own minimum contract * (`{_type: string}`), which the generated types do satisfy, and it is what * PortableText actually needs to dispatch on. @@ -31,11 +32,9 @@ interface Props { const { value, class: className } = Astro.props; /** - * astro-portabletext uses SINGULAR keys — `type`, `block`, `mark` — where - * @portabletext/react uses `types` / `marks`. Getting this wrong fails - * silently: the custom components are simply never called and every custom - * block renders as nothing, which is exactly what the pre-Next Astro app did. - */ + * astro-portabletext uses SINGULAR keys (`type`, `block`, `mark`) where + * @portabletext/react uses `types` / `marks`. + */ const components = { type: { image: PtImage, @@ -45,6 +44,15 @@ const components = { youtube: PtYouTube, youtubeShorts: PtYouTubeShorts, twitter: PtTweet, + socialEmbed: PtSocialEmbed, + bluesky: PtSocialEmbed, + linkedin: PtSocialEmbed, + reddit: PtSocialEmbed, + tiktok: PtSocialEmbed, + instagram: PtSocialEmbed, + threads: PtSocialEmbed, + mastodon: PtSocialEmbed, + githubGist: PtSocialEmbed, htmlBlock: PtHtml, quote: PtQuote, table: PtTable, @@ -70,14 +78,22 @@ const components = { {/* One delegated listener per page rather than one island per block. */}