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 @@ -50,3 +50,6 @@ jobs:
HASHNODE_ACCESS_TOKEN: ${{ secrets.PRIVATE_HASHNODE }}
AWS_BUILDER_SESSION_TOKEN: ${{ secrets.AWS_BUILDER_SESSION_TOKEN }}
DAILY_DEV_TOKEN: ${{ secrets.DAILY_DEV_TOKEN }}
GOOGLE_WORKSPACE_REFRESH_TOKEN: ${{ secrets.GOOGLE_WORKSPACE_REFRESH_TOKEN }}
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
68 changes: 68 additions & 0 deletions apps/sanity/functions/syndicate-content/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,74 @@ export const handler = documentEventHandler<ContentDocument>(
publishErr instanceof Error ? publishErr.message : String(publishErr);
console.error(`[Syndicate Content] Error publishing to ${platform}:`, errMsg);

if (platform === "hashnode") {
const refreshToken = process.env.GOOGLE_WORKSPACE_REFRESH_TOKEN;
const clientId = process.env.GOOGLE_CLIENT_ID;
const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
if (refreshToken && clientId && clientSecret) {
try {
const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: clientId,
client_secret: clientSecret,
refresh_token: refreshToken,
grant_type: "refresh_token",
}),
});
const tokenData = (await tokenRes.json()) as { access_token?: string };
if (tokenData.access_token) {
const taskTitle = `Cross-post to Hashnode & Medium: ${data.title || "Untitled"}`;
const taskNotes = [
`Canonical URL: ${canonicalUrl}`,
``,
`1. Hashnode (Free UI Editor): https://hashnode.com/draft`,
`2. Medium (1-Click Import): https://medium.com/p/import`,
`3. daily.dev Squad: https://daily.dev/squads/codingcatdev`,
].join("\n");
const tasksRes = await fetch(
"https://tasks.googleapis.com/tasks/v1/lists/@default/tasks",
{
method: "POST",
headers: {
Authorization: `Bearer ${tokenData.access_token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: taskTitle,
notes: taskNotes,
due: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
}),
},
);
if (!tasksRes.ok) {
const startDate = new Date(Date.now() + 60 * 60 * 1000);
const endDate = new Date(startDate.getTime() + 30 * 60 * 1000);
await fetch(
"https://www.googleapis.com/calendar/v3/calendars/alex.patterson%40gmail.com/events",
{
method: "POST",
headers: {
Authorization: `Bearer ${tokenData.access_token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
summary: `📋 [Task] ${taskTitle}`,
description: taskNotes,
start: { dateTime: startDate.toISOString() },
end: { dateTime: endDate.toISOString() },
}),
},
);
}
}
} catch (taskErr) {
console.warn("[Syndicate Content] Failed creating Google Task:", taskErr);
}
}
}

await client.createOrReplace({
_id: syndicationDocId,
_type: "syndication",
Expand Down
101 changes: 101 additions & 0 deletions scripts/run-syndicate-post.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,102 @@ const client = createClient({
useCdn: false,
});

async function createGoogleSyndicationTask({ title, canonicalUrl, tags = [] }) {
const refreshToken = process.env.GOOGLE_WORKSPACE_REFRESH_TOKEN;
const clientId = process.env.GOOGLE_CLIENT_ID;
const clientSecret = process.env.GOOGLE_CLIENT_SECRET;

if (!refreshToken || !clientId || !clientSecret) {
console.log("Google Workspace credentials not set; skipping Google Task creation.");
return;
}

try {
const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: clientId,
client_secret: clientSecret,
refresh_token: refreshToken,
grant_type: "refresh_token",
}),
});
const tokenData = await tokenRes.json();
const accessToken = tokenData.access_token;
if (!accessToken) {
console.warn("Could not obtain Google access_token for Task creation.");
return;
}

const taskTitle = `Cross-post to Hashnode & Medium: ${title}`;
const taskNotes = [
`Canonical URL: ${canonicalUrl}`,
`Tags: ${tags.join(", ")}`,
``,
`1. Hashnode (Free UI Editor):`,
` - Open: https://hashnode.com/draft`,
` - Set Original Article URL (Canonical): ${canonicalUrl}`,
``,
`2. Medium (1-Click Import):`,
` - Open: https://medium.com/p/import`,
` - Paste: ${canonicalUrl} (automatically sets canonical tag)`,
``,
`3. daily.dev Squad:`,
` - Open: https://daily.dev/squads/codingcatdev -> Click "Share a link" -> Paste ${canonicalUrl}`,
].join("\n");

// 1. Try Google Tasks API first
const tasksRes = await fetch("https://tasks.googleapis.com/tasks/v1/lists/@default/tasks", {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: taskTitle,
notes: taskNotes,
due: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
}),
});

if (tasksRes.ok) {
const createdTask = await tasksRes.json();
console.log(`✓ Created Google Task: "${taskTitle}" (ID: ${createdTask.id})`);
return;
}

// 2. Fallback to Google Calendar To-Do Event on alex.patterson@gmail.com if Tasks scope is not yet authorized
const startDate = new Date(Date.now() + 60 * 60 * 1000);
const endDate = new Date(startDate.getTime() + 30 * 60 * 1000);
const calRes = await fetch(
"https://www.googleapis.com/calendar/v3/calendars/alex.patterson%40gmail.com/events",
{
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
summary: `📋 [Task] ${taskTitle}`,
description: taskNotes,
start: { dateTime: startDate.toISOString() },
end: { dateTime: endDate.toISOString() },
}),
}
);

if (calRes.ok) {
const createdEvent = await calRes.json();
console.log(`✓ Created Google Calendar Task Reminder: "${taskTitle}" (${createdEvent.htmlLink})`);
} else {
console.warn("Could not create Google Calendar fallback reminder:", await calRes.text());
}
} catch (err) {
console.warn("Error creating Google syndication task:", err);
}
}

async function run() {
const doc = await client.getDocument(docId);
if (!doc) {
Expand Down Expand Up @@ -303,6 +399,11 @@ async function run() {
console.log(`✓ Updated Sanity doc ${doc._id} hashnode slug`);
} else {
console.error("✗ Hashnode publication failed:", publishData?.errors);
await createGoogleSyndicationTask({
title: doc.title || "Untitled",
canonicalUrl,
tags: hashnodeTags.map((t) => t.slug),
});
}
} catch (err) {
console.error("✗ Hashnode request error:", err);
Expand Down
Loading