Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/syndicate-post.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions GEMINI.md
Original file line number Diff line number Diff line change
@@ -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)
151 changes: 151 additions & 0 deletions apps/sanity/schemas/custom/social-embed.ts
Original file line number Diff line number Diff line change
@@ -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;
39 changes: 34 additions & 5 deletions apps/sanity/schemas/custom/twitter-embed.ts
Original file line number Diff line number Diff line change
@@ -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",
},
});
19 changes: 19 additions & 0 deletions apps/sanity/schemas/partials/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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" }),
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 30 additions & 14 deletions apps/site/src/components/portable-text/PortableTextContent.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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.
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -70,14 +78,22 @@ const components = {

{/* One delegated listener per page rather than one island per block. */}
<script>
// X's widgets.js upgrades any `.twitter-tweet` blockquote in place. Loaded
// only when the page actually contains one, so pages without an embedded
// post make no third-party request at all.
if (document.querySelector(".twitter-tweet")) {
const script = document.createElement("script");
script.src = "https://platform.twitter.com/widgets.js";
script.async = true;
document.head.appendChild(script);
const embedWidgets: Array<{ selector: string; src: string }> = [
{ selector: ".twitter-tweet", src: "https://platform.twitter.com/widgets.js" },
{ selector: ".bluesky-embed", src: "https://embed.bsky.app/static/embed.js" },
{ selector: ".reddit-embed-bq", src: "https://embed.reddit.com/widgets.js" },
{ selector: ".tiktok-embed", src: "https://www.tiktok.com/embed.js" },
{ selector: ".instagram-media", src: "https://www.instagram.com/embed.js" },
{ selector: ".text-post-media", src: "https://www.threads.net/embed.js" },
];

for (const { selector, src } of embedWidgets) {
if (document.querySelector(selector)) {
const script = document.createElement("script");
script.src = src;
script.async = true;
document.head.appendChild(script);
}
}

document.addEventListener("click", (event) => {
Expand Down
Loading
Loading