Skip to content

danielraffel/raindrop-to-ghost-sync

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

14 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

raindrop-to-ghost-sync

raindrop-to-ghost-sync is a serverless Google Cloud Function that syncs your Raindrop.io bookmarks to your Ghost blog. It lets you use a custom tag to automatically publish selected bookmarksβ€”with notes and highlightsβ€”straight to your blog, making it perfect for public linkrolls or curated reading lists.

✨ For a high-level overview and design rationale, check out this post.


πŸ“š Table of Contents


⁉️ Why Use It?

You can publish a link to your blog the moment you find something worth sharing using the Raindrop.io browser extension. It’s a fast, natural way to save and post what you’re reading.

This also lets you post links to a dedicated section of your blogβ€”without cluttering your main posts. With Ghost.org adding ActivityPub support, you’ll soon be able to automatically syndicate these posts to the fediverse and other social platformsβ€”while keeping full ownership of your content and avoiding the need to repost manually.

You can use this feature to:

  • Share quotes with commentary
  • Build a public reading list
  • Leave breadcrumbs from your research
  • Start lightweight posts that are part blog, part bookmark
  • Seamlessly distribute link posts to the fediverse

Example Workflow

  1. Save a page using the Raindrop extension in your browser (desktop or mobile).

  2. Highlight a passage and add a note using the Raindrop extension.

  3. Automatically publish to your Ghost blog.


✨ Features

  • Automatic Publishing: Syncs the most recent Raindrop bookmark with a custom tag of your choice to your Ghost blog.
  • Update Detection: If the bookmark was already synced, the corresponding Ghost post will be updated (not duplicated).
  • Clean Formatting: Notes and highlights are wrapped in semantic HTML and stored in a Ghost HTML card block.
    • Paragraphs, line breaks, and bullet lists (- or *) are preserved.
    • Inline code (`like this`) and fenced code blocks (```lang) are rendered using proper HTML code tags.
    • Safe HTML tags like <b>, <i>, and <a href="..."> are allowed and sanitized.
  • Media Embeds (YouTube & Spotify): Bookmarks linking to YouTube or Spotify automatically include an embedded player. If you've added a note to the Raindrop bookmark, your note will appear first, followed by the media player. These media links are processed even if they don't have other notes or highlights.
  • Metadata Embedded: Posts include embedded metadata (like Raindrop ID and tags) for filtering or custom display logic.
  • RSS Feed Friendly: Works well with Ghost’s RSS system to support custom feeds using the links tag.

🧾 Content Formatting

When you add notes or highlights to a Raindrop bookmark, this function converts them into readable, structured HTML for Ghost.

Supported features:

  • Paragraphs: Line breaks are preserved between paragraphs.
  • Bullet Lists: Lines starting with - or * are turned into <ul><li> HTML lists.
  • Inline Code: Wrap text in backticks like `code` to render it as <code>code</code>.
  • Code Blocks: Fenced code blocks using triple backticks (```) are supported and optionally language-tagged:

Entering:

```js
console.log("Hello");
```

Becomes:

console.log("Hello");
  • Safe HTML: Simple tags like <b>, <strong>, <i>, <em>, and <a href="..."> are preserved and sanitized for safe rendering in Ghost.

  • Highlight + Note Pairing: Highlights from Raindrop are rendered inside <blockquote> elements. Notes attached to highlights are displayed beneath them with full formatting.

  • Media Embeds:

    • YouTube: Links like https://www.youtube.com/watch?v=VIDEO_ID or https://youtu.be/VIDEO_ID will generate an iframe embed:
      <iframe width="560" height="315" src="https://www.youtube.com/embed/VIDEO_ID" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
    • Spotify: Links like https://open.spotify.com/album/ALBUM_ID or https://open.spotify.com/track/TRACK_ID will generate an iframe embed:
      <!-- For albums/playlists/shows -->
      <iframe style="border-radius:12px" src="https://open.spotify.com/embed/album/ALBUM_ID?utm_source=generator" width="100%" height="352" frameBorder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"></iframe>
      
      <!-- For tracks/episodes -->
      <iframe style="border-radius:12px" src="https://open.spotify.com/embed/track/TRACK_ID?utm_source=generator" width="100%" height="152" frameBorder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"></iframe>

    If a note exists on the Raindrop bookmark, it will be displayed above the embedded media player.


βš™οΈ Technical Stack

  • Google Cloud Functions (Gen 2, Node.js 20)
  • Google Cloud Scheduler (optional): Automates sync on a recurring schedule
  • Raindrop REST API: Fetches bookmarks
  • Ghost Admin API: Publishes or updates blog posts
  • Node.js Libraries: axios, @tryghost/admin-api, @google-cloud/functions-framework

πŸš€ Setup Instructions

1. Prerequisites

Before you begin, make sure you have:

πŸ› οΈ Google Cloud Account

πŸ”– A Raindrop Account with Developer Integration Configured

  • A Raindrop.io developer integration
  • A test token (used as the RAINDROP_API_KEY environment variable)
  • A Raindrop Premium account if you want notes on highlights to appear in Ghost

πŸ‘» Ghost Blog Setup

  • A Ghost blog with Admin API access
  • A Ghost Admin API Key and blog URL
    (Found under Ghost Admin β†’ Settings β†’ Integrations β†’ Add Custom Integration)

2. Clone the Repository

git clone https://github.com/danielraffel/raindrop-to-ghost-sync.git
cd raindrop-to-ghost-sync

3. Install Dependencies

npm install

4. Set Environment Variables

You’ll need to define the following variables when deploying:

Variable Description
RAINDROP_API_KEY Your Raindrop test token
GHOST_API_URL Your Ghost blog URL (e.g. https://yourdomain.com)
GHOST_ADMIN_API_KEY Your Ghost Admin API key (<id>:<secret>)
SYNC_SECRET A token you’ll use in the Authorization header to trigger syncs securely

5. Deploy to Google Cloud Functions

gcloud functions deploy raindropToGhostSync \
  --gen2 \
  --runtime nodejs20 \
  --trigger-http \
  --region YOUR_REGION \
  --entry-point raindropToGhostSync \
  --set-env-vars RAINDROP_API_KEY=YOUR_RAINDROP_KEY,GHOST_API_URL=https://yourdomain.com,GHOST_ADMIN_API_KEY=YOUR_ADMIN_KEY,SYNC_SECRET=YOUR_SECRET

πŸ“Œ Replace:

  • YOUR_REGION with a region like us-central1
  • RAINDROP_API_KEY with your Raindrop test token
  • GHOST_API_URL with your Ghost Admin API key
  • GHOST_ADMIN_API_KEY with your YOUR_ADMIN_KEY
  • SYNC_SECRET with your token

When prompted:

Allow unauthenticated invocations of new function [raindropToGhostSync]? (y/N)? y

πŸ§ͺ Testing the Function

Trigger the sync manually with curl using your SYNC_SECRET:

curl -X POST https://REGION-PROJECT.cloudfunctions.net/raindropToGhostSync \
  -H "Authorization: Bearer YOUR_SECRET"

To verify your Raindrop bookmarks are being tagged using YOUR_RAINDROP_API_KEY:

curl -H "Authorization: Bearer YOUR_RAINDROP_API_KEY" \
  "https://api.raindrop.io/rest/v1/raindrops/0?tag=1"

πŸ” Updating the Function

If you make changes to index.js, re-deploy using YOUR_REGION:

gcloud functions deploy raindropToGhostSync \
  --gen2 \
  --runtime nodejs20 \
  --trigger-http \
  --region YOUR_REGION \
  --entry-point raindropToGhostSync

βœ… You do not need to re-set environment variables unless they change.


⏰ Automate with Google Cloud Scheduler

To run your Raindrop β†’ Ghost sync automatically every minute, use Google Cloud Scheduler to call your function on a recurring schedule.


βœ… Step 1: Grant Invoke Permissions

Cloud Scheduler needs permission to call your Cloud Function. Run:

gcloud functions add-iam-policy-binding raindropToGhostSync \
  --region=us-central1 \
  --member="serviceAccount:SERVICE_ACCOUNT_EMAIL" \
  --role="roles/cloudfunctions.invoker"

Replace SERVICE_ACCOUNT_EMAIL with the service account Scheduler will use.

To find that email:

In most cases, the default is:

PROJECT_ID@appspot.gserviceaccount.com

You can confirm this in the IAM section of the Cloud Console or by running:

gcloud iam service-accounts list

βœ… Step 2: Create the Scheduler Job

Once the correct service account has access, create the job (update this with your region, your cloud function URI, and your SYNC_SECRET :

gcloud scheduler jobs create http raindrop-ghost-sync \
  --location=us-central1 \
  --schedule="* * * * *" \
  --uri=https://us-central1-YOUR_PROJECT.cloudfunctions.net/raindropToGhostSync \
  --http-method=POST \
  --headers="Authorization=Bearer ${SYNC_SECRET}" \
  --attempt-deadline=540s

πŸ” This sends your pre-defined SYNC_SECRET as a bearer token in the Authorization header. Your Cloud Function should reject requests that don’t include this.


πŸ§ͺ Manual Test (Optional)

You can test the sync manually by running:

gcloud scheduler jobs run raindrop-ghost-sync --location=us-central1

Check your function logs to confirm it was triggered successfully:

gcloud functions logs read raindropToGhostSync --region=us-central1 --limit=10

Changed something in your Raindrop bookmark and don’t want to wait for an automated update? Trigger a sync manually with curl using your SYNC_SECRET:

curl -X POST https://REGION-PROJECT.cloudfunctions.net/raindropToGhostSync \
  -H "Authorization: Bearer YOUR_SECRET"

Or make it even easierβ€”add a shortcut to your terminal by including this in your ~/.zshrc:

alias pushlink='curl -X POST https://REGION-PROJECT.cloudfunctions.net/raindropToGhostSync -H "Authorization: Bearer YOUR_SECRET"'

Then just type pushlink in your terminal whenever you want to instantly sync a new link.


πŸ”§ How to Create a Custom Tag in index.js

By default, the function looks for the most recent bookmark tagged with 1. You can change this by editing the tag filter in getLatestRaindropBookmark():

params: {
  tag: '1', // ← change this to your preferred tag (e.g. 'publish', 'linkroll')
  sort: '-created',
  perpage: 10
}

You can also adjust:

  • How the post content is formatted (inside formatGhostContent)
  • The logic for filtering out empty bookmarks (via shouldProcessBookmark)
  • Any HTML structure or metadata formatting as needed

πŸ“Œ To Do

  • Add deletion support: Remove Ghost posts if the corresponding Raindrop no longer has the custom tag
    • Strategy: Retrieve all links-tagged Ghost posts β†’ extract raindrop-id from each β†’ query Raindrop API β†’ if tag is missing, delete the post
    • Consider adding a simple database or caching layer to avoid redundant API calls
  • Add support for a .env file

πŸ“„ License

MIT License.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published