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
6 changes: 5 additions & 1 deletion .github/workflows/syndicate-post.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ jobs:

- name: Syndicate Post
run: |
node scripts/run-syndicate-post.mjs
if [ "$TARGET_DOCUMENT_ID" = "backfill-dailydev" ]; then
node scripts/backfill-dailydev-and-links.mjs
else
node scripts/run-syndicate-post.mjs
fi
env:
TARGET_DOCUMENT_ID: ${{ inputs.document_id }}
SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
Expand Down
173 changes: 171 additions & 2 deletions apps/ops/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useDocuments } from "@sanity/sdk-react";
import { useState } from "react";
import { useEffect, useState } from "react";

type WorkspaceTab = "crm" | "bookings" | "billing";
type WorkspaceTab = "crm" | "bookings" | "billing" | "syndication";

interface SeededGmailDraft {
companyName: string;
Expand Down Expand Up @@ -111,6 +111,33 @@ export function App() {
const sponsorLeads = useDocuments({ documentType: "sponsorLead" });
const sponsorPool = useDocuments({ documentType: "sponsorPool" });
const bookings = useDocuments({ documentType: "booking" });
const syndications = useDocuments({ documentType: "syndication" });
const [syndicatedContent, setSyndicatedContent] = useState<Array<any>>([]);

useEffect(() => {
const query = encodeURIComponent(
`*[_type in ["post", "podcast"] && !(_id in path("drafts.**"))] | order(coalesce(date, _createdAt) desc)[0...40] {
_id,
_type,
title,
"slug": slug.current,
"date": coalesce(date, _createdAt),
youtube,
dailydev,
devto,
hashnode,
awsbuilder,
medium,
reddit
}`,
);
fetch(`https://${projectId}.apicdn.sanity.io/v2025-09-30/data/query/${dataset}?query=${query}`)
.then((r) => r.json())
.then((d) => {
if (Array.isArray(d?.result)) setSyndicatedContent(d.result);
})
.catch(() => {});
}, [projectId, dataset]);

const handleDiscoverOutbound = async () => {
setDiscovering(true);
Expand Down Expand Up @@ -185,6 +212,21 @@ export function App() {
>
3. Stripe 50/50 Billing &amp; Deal Rooms
</button>
<button
type="button"
onClick={() => setActiveTab("syndication")}
style={{
padding: "8px 16px",
borderRadius: 8,
border: "1px solid #ccc",
background: activeTab === "syndication" ? "#111" : "#fff",
color: activeTab === "syndication" ? "#fff" : "#111",
fontWeight: 600,
cursor: "pointer",
}}
>
4. Syndication &amp; Platform Links ({syndications.data?.length || syndicatedContent.length})
</button>
</div>
</header>

Expand Down Expand Up @@ -318,6 +360,133 @@ export function App() {
</ul>
</section>
)}

{activeTab === "syndication" && (
<section style={{ padding: 20, border: "1px solid #ddd", borderRadius: 12 }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
<div>
<h2 style={{ margin: 0, fontSize: 18 }}>Cross-Platform Syndication &amp; YouTube Funnel Tracker</h2>
<p style={{ margin: "4px 0 0", color: "#666", fontSize: 13 }}>
Tracks canonical <code>codingcat.dev</code> URLs and outbound links across YouTube, daily.dev Squad, Dev.to, Hashnode, AWS Builder Center, Medium, and Reddit.
</p>
</div>
<div style={{ display: "flex", gap: 8 }}>
<a
href="https://daily.dev/squads/codingcatdev"
target="_blank"
rel="noreferrer"
style={{ padding: "8px 12px", border: "1px solid #ccc", borderRadius: 6, textDecoration: "none", color: "#111", fontSize: 12, fontWeight: 600 }}
>
daily.dev Squad &nearr;
</a>
<a
href="https://dev.to/codingcatdev"
target="_blank"
rel="noreferrer"
style={{ padding: "8px 12px", border: "1px solid #ccc", borderRadius: 6, textDecoration: "none", color: "#111", fontSize: 12, fontWeight: 600 }}
>
Dev.to Org &nearr;
</a>
</div>
</div>
<div style={{ overflowX: "auto" }}>
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
<thead>
<tr style={{ textAlign: "left", borderBottom: "2px solid #eee" }}>
<th style={{ padding: "8px 6px" }}>Date</th>
<th style={{ padding: "8px 6px" }}>Title (Canonical)</th>
<th style={{ padding: "8px 6px" }}>YouTube</th>
<th style={{ padding: "8px 6px" }}>daily.dev Squad</th>
<th style={{ padding: "8px 6px" }}>Dev.to</th>
<th style={{ padding: "8px 6px" }}>Hashnode</th>
<th style={{ padding: "8px 6px" }}>AWS Builder</th>
<th style={{ padding: "8px 6px" }}>Medium / Reddit</th>
</tr>
</thead>
<tbody>
{syndicatedContent.map((item) => {
const canonical = `${siteOrigin}/${item._type}/${item.slug}`;
const hnUrl = item.hashnode
? item.hashnode.startsWith("http")
? item.hashnode
: `https://hashnode.codingcat.dev/${item.hashnode}`
: null;
return (
<tr key={item._id} style={{ borderBottom: "1px solid #f0f0f0" }}>
<td style={{ padding: "8px 6px", whiteSpace: "nowrap", color: "#666" }}>
{(item.date || "").slice(0, 10)}
</td>
<td style={{ padding: "8px 6px", fontWeight: 600 }}>
<a href={canonical} target="_blank" rel="noreferrer" style={{ color: "#111" }}>
{item.title}
</a>
</td>
<td style={{ padding: "8px 6px" }}>
{item.youtube ? (
<a href={item.youtube} target="_blank" rel="noreferrer" style={{ color: "#c00", fontWeight: 600 }}>
Watch &nearr;
</a>
) : (
<span style={{ color: "#aaa" }}>-</span>
)}
</td>
<td style={{ padding: "8px 6px" }}>
{item.dailydev ? (
<a href={item.dailydev} target="_blank" rel="noreferrer" style={{ color: "#7c3aed", fontWeight: 600 }}>
Squad &nearr;
</a>
) : (
<span style={{ color: "#aaa" }}>-</span>
)}
</td>
<td style={{ padding: "8px 6px" }}>
{item.devto ? (
<a href={item.devto} target="_blank" rel="noreferrer" style={{ color: "#2563eb", fontWeight: 600 }}>
Dev.to &nearr;
</a>
) : (
<span style={{ color: "#aaa" }}>-</span>
)}
</td>
<td style={{ padding: "8px 6px" }}>
{hnUrl ? (
<a href={hnUrl} target="_blank" rel="noreferrer" style={{ color: "#2563eb", fontWeight: 600 }}>
Hashnode &nearr;
</a>
) : (
<span style={{ color: "#aaa" }}>-</span>
)}
</td>
<td style={{ padding: "8px 6px" }}>
{item.awsbuilder ? (
<a href={item.awsbuilder} target="_blank" rel="noreferrer" style={{ color: "#d97706", fontWeight: 600 }}>
AWS &nearr;
</a>
) : (
<span style={{ color: "#aaa" }}>-</span>
)}
</td>
<td style={{ padding: "8px 6px" }}>
{item.medium && (
<a href={item.medium} target="_blank" rel="noreferrer" style={{ marginRight: 8 }}>
Medium &nearr;
</a>
)}
{item.reddit && (
<a href={item.reddit} target="_blank" rel="noreferrer" style={{ color: "#ea580c" }}>
Reddit &nearr;
</a>
)}
{!item.medium && !item.reddit && <span style={{ color: "#aaa" }}>-</span>}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</section>
)}
</div>
);
}
22 changes: 22 additions & 0 deletions apps/reddit-devvit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# CodingCat.dev Reddit Devvit Auto-Poster (`r/CodingCatDev`)

This Reddit Devvit app runs inside Reddit's own infrastructure (`@devvit/public-api`) so **it does not require external Reddit Data API approval**.

## What It Does
1. **Polls Sanity (`production`) every 30 minutes** via `Devvit.addSchedulerJob` (or on-demand via the subreddit moderator menu item **"Sync Latest from CodingCat.dev Now"**).
2. **YouTube-First Native Player**:
- When a post or podcast has a `youtube` URL in Sanity, it submits the `https://www.youtube.com/watch?v=<id>` URL as the Reddit Link Post so Reddit renders the **native inline YouTube player** right in the `r/CodingCatDev` feed.
- Automatically adds a pinned moderator comment containing the excerpt, the canonical `https://codingcat.dev/post/<slug>` guide + transcript link, and a direct YouTube subscribe link (`?sub_confirmation=1`).
3. **Automatic Sanity Link Tracking**:
- When `sanityWriteToken` is configured in the app's installation settings, it automatically creates `syndication.<docId>.reddit` and populates the `reddit` URL field on the Sanity document.

## 2-Command Setup & Deployment

```bash
cd apps/reddit-devvit
npx devvit login
npx devvit upload
npx devvit install r/CodingCatDev
```

*(Optional)* After installing on `r/CodingCatDev`, open `https://developers.reddit.com/r/CodingCatDev/apps/codingcat-syndicator` and paste your `SANITY_API_WRITE_TOKEN` into the **Sanity API Write Token** field so every Reddit post URL is automatically saved back to Sanity.
2 changes: 2 additions & 0 deletions apps/reddit-devvit/devvit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
name: codingcat-syndicator
version: 0.1.0
19 changes: 19 additions & 0 deletions apps/reddit-devvit/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@codingcatdev/reddit-devvit",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"login": "devvit login",
"upload": "devvit upload",
"playtest": "devvit playtest r/CodingCatDev",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@devvit/public-api": "^0.11.0"
},
"devDependencies": {
"devvit": "^0.11.0",
"typescript": "^5.7.0"
}
}
Loading
Loading