Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v1 of Bluesky embed support #304

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
50 changes: 50 additions & 0 deletions packages/bluesky/.eleventy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
const merge = require("deepmerge");
const spotPattern = require("./lib/spotPattern.js");
const extractMatch = require("./lib/extractMatch.js");
const buildEmbed = require("./lib/buildEmbed.js");
const pluginDefaults = require("./lib/pluginDefaults.js");

module.exports = function (eleventyConfig, options) {
const pluginConfig = options ? merge(pluginDefaults, options) : pluginDefaults;

eleventyConfig.addTransform("blueskyEmbed", async function (content, outputPath) {
if (!outputPath?.endsWith(".html")) return content;

try {
// First split by code blocks
const parts = content.split(/```[^`]*```/);

// Process each non-code-block part
for (let i = 0; i < parts.length; i += 2) {
// Find all matches in this part
const matches = spotPattern(parts[i]);
if (!matches) continue;

// Track number of embeds if maxEmbeds is set
let embedCount = 0;

// Process each match
for (const match of matches) {
// Skip if we've hit the max embeds limit
if (pluginConfig.maxEmbeds > 0 && embedCount >= pluginConfig.maxEmbeds) break;

// Extract post details
const post = extractMatch(match);
if (!post) continue;

// Build embed HTML
const embedHtml = await buildEmbed(post, pluginConfig);
if (embedHtml) {
parts[i] = parts[i].replace(match, embedHtml);
embedCount++;
}
}
}

return parts.join("");
} catch (error) {
console.warn("Error processing Bluesky embeds:", error);
return content;
}
});
};
66 changes: 66 additions & 0 deletions packages/bluesky/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# eleventy-plugin-embed-bluesky

Embed [Bluesky](https://bsky.app) posts in your Eleventy site.

Part of [eleventy-plugin-embed-everything](https://github.com/gfscott/eleventy-plugin-embed-everything), a suite of plugins for rich media embeds in Eleventy.

## Install

```sh
npm install @11ty/eleventy @gfscott/eleventy-plugin-embed-bluesky
```

## Usage

In your Eleventy config file:

```js
const embedBluesky = require("@gfscott/eleventy-plugin-embed-bluesky");

module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(embedBluesky);
};
```

Then use it in your templates:

```html
This is my first Bluesky post!

https://bsky.app/profile/shellen.com/post/3ldmp5qd6es2p

More content...
```

The plugin will automatically transform any valid Bluesky post URL into an embedded post.

## Supported URL formats

The plugin supports various Bluesky URL formats:

- Standard URLs: `https://bsky.app/profile/bsky.app/post/3lgu4lg6j2k2v`
- Custom domain handles: `https://bsky.app/profile/shellen.com/post/3ldmp5qd6es2p`
- DID handles: `https://bsky.app/profile/did:plc:yc6gmb3bo56qotdsywnsxrxp/post/3lgvpv7k5sc26`
- bsky.social handles: `https://bsky.app/profile/user.bsky.social/post/3lgzl25gr2c2k`

URLs can be with or without protocol and with or without trailing slashes.

## Options

You can pass an options object to customize the plugin's behavior:

```js
eleventyConfig.addPlugin(embedBluesky, {
// Set a custom embed height (default: 300)
height: 500,
});
```

## Credits

- Created by [@shellen](https://github.com/shellen)
- Part of [eleventy-plugin-embed-everything](https://github.com/gfscott/eleventy-plugin-embed-everything) by [@gfscott](https://github.com/gfscott)

## License

MIT
19 changes: 19 additions & 0 deletions packages/bluesky/lib/buildEmbed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const EleventyFetch = require("@11ty/eleventy-fetch");
const deepmerge = require("deepmerge");
const buildOptions = require("./buildOptions.js");

module.exports = async function(post, options) {
const opts = buildOptions(options);

try {
// Build embed HTML
let embedHtml = `<div class="${opts.embedClass}">`;
embedHtml += `<iframe src="https://${opts.embedDomain}/profile/${post.handle}/post/${post.postId}/embed" style="width: 100%; height: ${opts.height || 300}px;" frameborder="0"></iframe>`;
embedHtml += "</div>";

return embedHtml;
} catch (error) {
console.warn("Error building Bluesky embed:", error);
return "";
}
};
28 changes: 28 additions & 0 deletions packages/bluesky/lib/buildOptions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const merge = require("deepmerge");
const defaults = require("./pluginDefaults.js");

/**
* Build options object by merging defaults with user options
* @param {Object} options - User-provided options
* @returns {Object} - Complete options object
*/
module.exports = function(options = {}) {
// Deep merge user options with defaults
const opts = merge(defaults, options);

// Validate and normalize options
opts.width = parseInt(opts.width, 10) || defaults.width;
opts.maxEmbeds = parseInt(opts.maxEmbeds, 10) || defaults.maxEmbeds;

// Ensure cache duration is valid
if (typeof opts.cacheDuration !== 'string' || !opts.cacheDuration.match(/^\d+[hdwmy]$/)) {
opts.cacheDuration = defaults.cacheDuration;
}

// Ensure oEmbed endpoint is valid
if (typeof opts.oEmbedEndpoint !== 'string' || !opts.oEmbedEndpoint.startsWith('https://')) {
opts.oEmbedEndpoint = defaults.oEmbedEndpoint;
}

return opts;
};
37 changes: 37 additions & 0 deletions packages/bluesky/lib/extractMatch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Extract handle and post ID from a matched Bluesky URL.
* @param {string} url - The matched URL
* @returns {Object|null} Object containing handle and postId, or null if invalid
*/
module.exports = function (url) {
try {
// Skip if inside code tag
if (url.includes("<code>")) {
return null;
}

// Clean URL and extract parts
const cleanUrl = url.replace(/<[^>]+>/g, "").trim();

// Extract URL from paragraph tags if present
const urlMatch = cleanUrl.match(
/(?:https?:)?(?:\/\/)??(?:bsky\.app|staging\.bsky\.app)\/profile\/([^/\s]+)\/post\/([a-zA-Z0-9]+)/,
);

if (!urlMatch) {
return null;
}

// Validate post ID length (should be at least 10 characters)
if (urlMatch[2].length < 10) {
return null;
}

return {
handle: urlMatch[1],
postId: urlMatch[2],
};
} catch (e) {
return null;
}
};
25 changes: 25 additions & 0 deletions packages/bluesky/lib/pluginDefaults.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module.exports = {
// CSS class for the embed wrapper
embedClass: "eleventy-plugin-embed-bluesky",

// Default width for embeds (matches Bluesky's default)
width: 550,

// Cache duration for oEmbed responses
cacheDuration: "1d",

// oEmbed endpoint
oEmbedEndpoint: "https://bsky.app/oembed",

// Whether to cache oEmbed responses
cacheResponses: true,

// Maximum embeds per page (0 for unlimited)
maxEmbeds: 0,

// Domain to use for embeds
embedDomain: "bsky.app",

// Whether to add noscript fallback
addNoscript: true,
};
34 changes: 34 additions & 0 deletions packages/bluesky/lib/spotPattern.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Check if a string contains a Bluesky URL.
* @param {string} str - The string to check
* @returns {boolean} True if the string contains a Bluesky URL
*/
module.exports = function (str) {
try {
// Skip if inside code tag
if (str.includes("<code>")) {
return false;
}

// Clean URL and extract parts
const cleanUrl = str.replace(/<[^>]+>/g, "").trim();

// Extract URL from paragraph tags if present
const urlMatch = cleanUrl.match(
/(?:https?:)?(?:\/\/)??(?:bsky\.app|staging\.bsky\.app)\/profile\/([^/\s]+)\/post\/([a-zA-Z0-9]+)/,
);

if (!urlMatch) {
return false;
}

// Validate post ID length (should be at least 10 characters)
if (urlMatch[2].length < 10) {
return false;
}

return true;
} catch (e) {
return false;
}
};
Loading