> Canonical URL: https://codingcat.dev/post/notion-to-hashnode-and-devto-using-pubsub-cloud-functions

# Notion to Hashnode and Dev.to using Pub/Sub Cloud Functions

✏️ Blog PostSeptember 1, 2022[![](/logos/firebase/firebase-symbol.png)![](/logos/firebase/firebase-symbol.png)Firebase](/topic/firebase)[![](/logos/tags/github-symbol-light.svg)![](/logos/tags/github-symbol-dark.svg)GitHub](/topic/github)[![](/logos/tags/notion-symbol-light.svg)![](/logos/tags/notion-symbol-dark.svg)Notion](/topic/notion)[![](/logos/tags/google-cloud-symbol-light.svg)![](/logos/tags/google-cloud-symbol-dark.svg)Google Cloud](/topic/google-cloud)[![](/logos/tags/devops-symbol-light.svg)![](/logos/tags/devops-symbol-dark.svg)DevOps](/topic/devops)[![](/logos/tags/javascript-symbol-light.svg)![](/logos/tags/javascript-symbol-dark.svg)JavaScript](/topic/javascript)[![](/logos/tags/security-symbol-light.svg)![](/logos/tags/security-symbol-dark.svg)Security](/topic/security)[![](/logos/tags/oss-symbol-light.svg)![](/logos/tags/oss-symbol-dark.svg)Open Source](/topic/oss)

# Notion to Hashnode and Dev.to using Pub/Sub Cloud Functions

![CodingCat.dev tutorial cover image](https://cdn.sanity.io/media-libraries/mlttp0BZbCpV/images/bbf6e5212bcec055b854494b3fdac46312e35be8-1920x1080.jpg?w=1920&q=82&fit=max&auto=format)[![Alex Patterson](https://cdn.sanity.io/media-libraries/mlttp0BZbCpV/images/a606dfaa381aab08ddbe16a33fe1d3e1fb37c927-400x400.jpg?w=96&h=96&q=82&fit=max&auto=format)Alex Patterson](/author/alex-patterson)

A goal at [CodingCat.dev](http://CodingCat.dev) is to reach the most students possible. In order to do that we use [Dev.to](http://Dev.to) and Hashnode to cross post our articles and leverage canonical urls to point those articles back the the original. For us it allows more eyeballs to get to the content and videos.

## Searching for the Notion Data

In order to keep find what data that needs processing in notion the simplest way is to keep a record of the url that the new blog will be held in. For example when finding records that we will need to add to [Dev.to](http://Dev.to) we will add a column called `devto`. We can then search the database for any records fitting a criteria with that field being empty using Notion’s API and filtered fields.

In the below example, the database from `database_id` is searched for CodingCat.dev’s database. We also add the start_cursor because we want these going out in order we pass 1 to this field so that we can guarantee that our posts go out in order and don’t process out of sequence. We also look for podcasts that have a `slug` , are `Released` , have an `Episode` assigned, include a `start` date on or after today’s date.

Copied! 🐾

```
export const queryPurrfectStreamDevTo = async (
  page_size?: number,
  start_cursor?: string | null
) => {
  const raw = await notionClient.databases.query({
    database_id: notionConfig.purrfectStreamsDb,
    start_cursor: start_cursor ? start_cursor : undefined,
    page_size,
    filter: {
      and: [
        {
          property: 'slug',
          url: {
            is_not_empty: true,
          },
        },
        {
          property: 'Status',
          select: {
            equals: 'Released',
          },
        },
        {
          property: 'Episode',
          number: {
            is_not_empty: true,
          },
        },
        {
          property: 'start',
          date: {
            on_or_before: new Date().toISOString(),
          },
        },
        {
          property: 'devto',
          url: {
            is_empty: true,
          },
        },
        {
          property: 'youtube',
          url: {
            is_not_empty: true,
          },
        },
        {
          property: 'spotify',
          url: {
            is_not_empty: true,
          },
        },
      ],
    },
    sorts: [
      {
        property: 'Season',
        direction: 'ascending',
      },
      {
        property: 'Episode',
        direction: 'ascending',
      },
    ],
  });
  return await formatPosts(raw, 'podcast');
};
```

## Scheduled Cloud Functions

We can now create a Firebase function that creates a Google Cloud - Cloud Scheduler Job that can run at a given interval. This is a [convenience method](https://firebase.google.com/docs/functions/schedule-functions) that uses pubsub functions. Notice below we will have this function run `every 5 minutes` and check to see if there are any new articles that match our criteria. We could probably back this off to every 1 hour, but it is nice to see this trigger relatively quickly if you have the correct criteria.

Copied! 🐾

```
const topicId = 'devtoCreateFromNotion';

export const scheduledNotionToDevto = functions.pubsub
  .schedule('every 5 minutes')
  .onRun(async () => {
    // Check to see if ther are scheduled pods
    console.log('Checking for scheduled pods');
    const scheduledRes = await queryPurrfectStreamDevTo(1);
    console.log('Scheduled Result:', JSON.stringify(scheduledRes));

    if (scheduledRes?.results) {
      const needCloudinaryPods = scheduledRes?.results;
      console.log('Pods to add to pub/sub', JSON.stringify(needCloudinaryPods));

      for (const pod of needCloudinaryPods) {
        await sendTopic(topicId, pod);
      }
    }

    console.log('Checking for devto missing');
    const posts = await queryByDevto('post', 1);
    console.log('Posts:', JSON.stringify(posts));

    if (posts?.results) {
      const needposts = posts?.results;
      console.log('Posts to add to pub/sub', JSON.stringify(needposts));

      for (const p of needposts) {
        await sendTopic(topicId, p);
      }
    }

    return true;
  });
```

Now if there is something actually found in `queryPurrfectStreamDevTo` function we can then take the data from that entry and pass this on to another pub/sub function. For [CodingCat.dev](http://CodingCat.dev) we just look for 1 entry so that we can publish things in the order that they are created, as we have found with some of these API’s like [dev.to](http://dev.to) there is not an easy way to set original publish date on the actual platform.

## Pub/Sub Function to publish post

This pub/sub function is looking for anything that sends a topic of `devtoCreateFromNotion` to it and then looks up the appropriate post information. This is often the easiest way for massive scaling to happen if you have several items you are trying to publish to the platform.

For our example we change the `body_markdown` depending on our post type. An important note here is that we are sending `canonical_url` so that search engine bots don’t think this is original content and know where to add 301 if necessary.

Copied! 🐾

```
export const devtoToNotionPubSub = functions.pubsub
  .topic(topicId)
  .onPublish(async (message, context) => {
    console.log('The function was triggered at ', context.timestamp);
    console.log('The unique ID for the event is', context.eventId);
    const page = JSON.parse(JSON.stringify(message.json));
    console.log('page', page);

    let data;
    if (page._type === 'podcast') {
      data = {
        article: {
          title: page.title,
          published: true,
          tags: ['podcast', 'webdev', 'javascript', 'beginners'],
          series: `codingcatdev_podcast_${page.properties.Season.number}`,
          main_image: `https://media.codingcat.dev/image/upload/b_rgb:5e1186,c_pad,w_1000,h_420/${page?.coverPhoto?.public_id}`,
          canonical_url: `https://codingcat.dev/${page._type}/${page.slug}`,
          description: page.excerpt,
          organization_id: '1009',
          body_markdown: `Original: https://codingcat.dev/${page._type}/${
            page.slug
          }
          {% youtube ${page.properties.youtube.url} %}
          {% spotify spotify:episode:${page.properties.spotify.url
            .split('/')
            .at(-1)
            .split('?')
            .at(0)} %}

          `,
        },
      };
    } else {
      console.log(
        `Getting ${page._type}: ${page.id} markdown, with slug ${page?.properties?.slug?.url}`
      );
      const post = await getNotionPageMarkdown({
        _type: page._type,
        slug: page?.properties?.slug?.url,
        preview: false,
      });

      console.log('Block Result', post);

      if (post && post?.content) {
        data = {
          article: {
            title: page.title,
            published: true,
            tags: ['podcast', 'webdev', 'javascript', 'beginners'],
            main_image: `https://media.codingcat.dev/image/upload/b_rgb:5e1186,c_pad,w_1000,h_420/${page?.coverPhoto?.public_id}`,
            canonical_url: `https://codingcat.dev/${page._type}/${page.slug}`,
            description: page.excerpt,
            organization_id: '1009',
            body_markdown: post.content,
          },
        };
      }
    }

    if (data) {
      try {
        console.log('addArticle to devto');
        const response = await addArticle(data);
        console.log('addArticle result:', response);

        const devto = response?.data?.url;

        if (!devto) {
          console.log('devto url missing');
          return;
        }

        const update = {
          page_id: page.id,
          properties: {
            devto: {
              id: 'remote',
              type: 'url',
              url: devto,
            },
          },
        };
        console.log('Updating page with: ', JSON.stringify(update));
        const purrfectPagePatchRes = await patchPurrfectPage(update);
        console.log(
          'Page update result:',
          JSON.stringify(purrfectPagePatchRes)
        );

        return purrfectPagePatchRes;
      } catch (error) {
        console.error(error);
      }
    } else {
      console.error('No Data matched for article');
    }
    return;
  });
```

## Bonus: To test locally

I would recommend testing these functions locally before having them run on their own. You can use the firebase emulation suite for this. Then update your code for both the schedule and http version to call the same function like below `scheduleCheck()`.

Copied! 🐾

```
const scheduleCheck = async () => {
  // Check to see if ther are scheduled pods
  console.log('Checking for scheduled pods');
  const scheduledRes = await queryPurrfectStreamDevTo(1);
  console.log('Scheduled Result:', JSON.stringify(scheduledRes));

  if (scheduledRes?.results) {
    const needCloudinaryPods = scheduledRes?.results;
    console.log('Pods to add to pub/sub', JSON.stringify(needCloudinaryPods));

    for (const pod of needCloudinaryPods) {
      await sendTopic(topicId, pod);
    }
  }

  for (const _type of ['post', 'tutorial']) {
    console.log('Checking for devto missing');
    const posts = await queryByDevto(_type, 1);
    console.log('Posts:', JSON.stringify(posts));

    if (posts?.results) {
      const needposts = posts?.results;
      console.log('Posts to add to pub/sub', JSON.stringify(needposts));

      for (const p of needposts) {
        await sendTopic(topicId, p);
      }
    }
  }
};

export const httpNotionToDevto = functions.https.onRequest(async (req, res) => {
  await scheduleCheck();

  res.send({ msg: 'started' });
});

export const scheduledNotionToDevto = functions.pubsub
  .schedule('every 5 minutes')
  .onRun(async () => {
    await scheduleCheck();
    return true;
  });
```

## Full Source

Full [Dev.to](http://Dev.to) example can be found at

[https://github.com/CodingCatDev/codingcat.dev/blob/dev/backend/firebase/functions/src/devto/scheduledNotionToDevto.ts](https://github.com/CodingCatDev/codingcat.dev/blob/dev/backend/firebase/functions/src/devto/scheduledNotionToDevto.ts)

Full Hashnode example can be found at

[https://github.com/CodingCatDev/codingcat.dev/blob/dev/backend/firebase/functions/src/hashnode/scheduledNotionToHashNode.ts](https://github.com/CodingCatDev/codingcat.dev/blob/dev/backend/firebase/functions/src/hashnode/scheduledNotionToHashNode.ts)

Note that this is TypeScript and all functions need to be exposed in the `index.ts` file to be deployed.

[https://github.com/CodingCatDev/codingcat.dev/blob/dev/backend/firebase/functions/src/index.ts](https://github.com/CodingCatDev/codingcat.dev/blob/dev/backend/firebase/functions/src/index.ts)

## Resources, Links & Official Documentation

- [**Firebase Documentation:**](https://firebase.google.com/docs) Firestore, Data Connect, Authentication, App Check, and Cloud Functions
- [**GitHub Documentation:**](https://docs.github.com/) Pull requests, GitHub Actions CI/CD, and repository security

## Related CodingCat.dev Tutorials & Podcast Episodes

- [**Google Cloud Repositories CI/CD**](/post/google-cloud-repositories-ci-cd): Google Cloud Continuous Integration and Delivery (CI/CD)
- [**Build Responsive, AI-powered Apps with Cloud Functions for Firebase: Web Architecture & Implementation Guide**](/post/firebase-2025-03-streaming-cloud-functions-genkit): Hands-on web engineering breakdown of "Build Responsive, AI-powered Apps with Cloud Functions for Firebase" (2025-03-06). Learn how to integrate Firebase, Googl...
- [**Send GCP Build Status to Discord**](/post/send-gcp-build-status-to-discord): If you use Google Cloud Build and find yourself in Discord all day, it might be handy to receive a messge about your builds. Or just any webhook message in Disc...

## Frequently Asked Questions (FAQ)

### What are the main takeaways from Notion to Hashnode and Dev.to using Pub/Sub Cloud Functions?

Depending on your goals cross posting with a canonical url can increase your user engagements 10x. I will show you an easy way to take your Notion articles out to Hashnode and Dev.to. Notion to Hashnode and Dev.to using Pub/Sub Cloud Functions explores practical patterns for Firebase, GitHub in modern software engineering. Throughout this post, Alex Patterson breaks down real-world implementation details for Firebase, GitHub so developers can apply these patterns directly in production.

### How do you implement Firebase best practices in production?

When adopting Firebase and GitHub in a production stack, start by validating your configuration against the [**Firebase Documentation**](https://firebase.google.com/docs) (Firestore, Data Connect, Authentication, App Check, and Cloud Functions). Focus on incremental adoption, automated testing, and observability so your team can measure performance and developer velocity improvements at each stage.

### Where can I watch the full Notion to Hashnode and Dev.to using Pub/Sub Cloud Functions walkthrough and find related code resources?

You can explore the complete write-up and code examples right here on CodingCat.dev, and subscribe to the [**CodingCat.dev YouTube channel**](https://www.youtube.com/@CodingCatDev?sub_confirmation=1) for weekly video tutorials. For a deeper dive into Firebase, check out our related guide: [**Google Cloud Repositories CI/CD**](/post/google-cloud-repositories-ci-cd).

✏️ Visual Architecture Diagram

## how this post connects together.

Follow AJ from first principles to production-ready architecture.

Let’s build it!✏️ Visual Diagram: Notion to Hashnode and Dev.to using Pub/Sub Clou…✓ Good PatternVISUAL DIAGRAM[Diagram Walkthrough]Step-by-step diagram breakdown for Notion to Hashnode and Dev.to using Pub/Sub Cloud Functions: start with Notion to Hashnode and, implement Hands-On Implementation, and ship to production.AAA Contrast Verified▶ Watch on @CodingCatDev YouTubeRelated Course & Video

## Watch the Related Video Deep Dive: The Odin Project 2: Git & GitHub Engineering Workflows

Understand how Git actually stores snapshots, master branching and interactive rebase, write atomic commits, and automate PR checks with GitHub Actions.

[View Full Page →](/course/odin-git-and-github-workflows)![](https://cdn.sanity.io/images/hfh83o0w/production/665a6b1f24fc33a6ec350bd555607c1017e79848-1376x768.png?rect=6,0,1365,768&w=1920&h=1080&q=82&fit=max&auto=format)[@CodingCatDev62.5K / 100K Subscribers Goal](https://www.youtube.com/@codingcatdev?sub_confirmation=1)[Subscribe on YouTube→](https://www.youtube.com/@codingcatdev?sub_confirmation=1)[Watch on YouTube App ↗](https://www.youtube.com/watch?v=IIUMYo1GWx0&list=PLCOTCGP0a3VY)

## related deep dives.

[All Blog Posts&rarr;](/blog)[![The Firebase plugin is now available in Codex: Web Architecture & Implementation Guide](https://cdn.sanity.io/media-libraries/mlttp0BZbCpV/images/4e2c8e99f437cb576972d406ec32394a82a19480-1200x630.svg?w=1200&q=82&fit=max&auto=format)](/post/firebase-2026-09-firebase-plugin-for-codex)September 29, 2026[![](/logos/firebase/firebase-symbol.png)![](/logos/firebase/firebase-symbol.png)Firebase](/topic/firebase)[![](/logos/tags/google-cloud-symbol-light.svg)![](/logos/tags/google-cloud-symbol-dark.svg)Google Cloud](/topic/google-cloud)

### [The Firebase plugin is now available in Codex: Web Architecture & Implementation Guide](/post/firebase-2026-09-firebase-plugin-for-codex)

Alex Patterson's hands-on web engineering breakdown of "The Firebase plugin is now available in Codex" (2026-09-28). Learn how to wire up Firebase, Google Cloud, Web Dev, AI in production web apps.

[![Alex Patterson](https://cdn.sanity.io/media-libraries/mlttp0BZbCpV/images/a606dfaa381aab08ddbe16a33fe1d3e1fb37c927-400x400.jpg?w=96&h=96&q=82&fit=max&auto=format)Alex Patterson](/author/alex-patterson)[![PHP2Shell (CVE-2026-109618): How AI Found a Decade-Old Bug in 70% of the Web](https://cdn.sanity.io/media-libraries/mlttp0BZbCpV/images/e83065d0a4e2f841ce64d3f4aea8ddbbf07e5cb9-1376x768.jpg?w=1376&q=82&fit=max&auto=format)](/post/php2shell-cve-2026-109618-ai-php-security-scramble)October 22, 2026[![](/logos/tags/aws-symbol-light.svg)![](/logos/tags/aws-symbol-dark.svg)AWS](/topic/aws)[![](/logos/tags/php-symbol-light.svg)![](/logos/tags/php-symbol-dark.svg)PHP](/topic/php)

### [PHP2Shell (CVE-2026-109618): How AI Found a Decade-Old Bug in 70% of the Web](/post/php2shell-cve-2026-109618-ai-php-security-scramble)

Two critical pre-authentication RCE flaws in PHP (CVE-2026-109618 and CVE-2026-109619), uncovered by AI models from OpenAI and Google Gemini, sparked a global patching scramble across 70% of the web. Here is how PHP2Shell works and how to patch your servers.

[![Alex Patterson](https://cdn.sanity.io/media-libraries/mlttp0BZbCpV/images/a606dfaa381aab08ddbe16a33fe1d3e1fb37c927-400x400.jpg?w=96&h=96&q=82&fit=max&auto=format)Alex Patterson](/author/alex-patterson)