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 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
5 changes: 5 additions & 0 deletions demo/src/bluesky.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: Bluesky
---

https://bsky.app/profile/bsky.app/post/3lgu4lg6j2k2v
3 changes: 2 additions & 1 deletion demo/src/index.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
This is an example site to demonstrate and test the functionality of [Embed Everything](https://gfscott.com/embed-everything).

- [Bluesky example](/bluesky/)
- [Instagram example](/instagram/)
- [OpenStreetMap example](/osm/)
- [Soundcloud example](/soundcloud/)
Expand All @@ -9,4 +10,4 @@ This is an example site to demonstrate and test the functionality of [Embed Ever
- [Twitch example](/twitch/)
- [Twitter example](/twitter/)
- [Vimeo example](/vimeo/)
- [YouTube example](/youtube/)
- [YouTube example](/youtube/)
5 changes: 5 additions & 0 deletions packages/bluesky/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## 1.0.0

### Major Changes

- Adds support for embedding Bluesky posts using just their URLs.
107 changes: 107 additions & 0 deletions packages/bluesky/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# eleventy-plugin-embed-bluesky

This [Eleventy](https://www.11ty.dev/) plugin automatically embeds Bluesky posts from URLs in markdown files. It's part of the [`eleventy-plugin-embed-everything`](https://gfscott.com/embed-everything/) project.

## Install in Eleventy

In your Eleventy project, [install the plugin](https://www.11ty.dev/docs/plugins/#adding-a-plugin) through npm:

```sh
$ npm i eleventy-plugin-embed-bluesky
```

Then add it to your [Eleventy config](https://www.11ty.dev/docs/config/) file (usually `.eleventy.js`):

```javascript
const embedBluesky = require("eleventy-plugin-embed-bluesky");

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

## Usage

To embed a Bluesky post into any markdown page, paste its URL into a new line. The URL should be the only thing on that line.

### Markdown file example:

```markdown
...

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam vehicula, elit vel condimentum porta, purus.

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

Maecenas non velit nibh. Aenean eu justo et odio commodo ornare. In scelerisque sapien at.

...
```

## Settings

You can configure the plugin to change its behavior by passing an options object to the `addPlugin` function:

```javascript
eleventyConfig.addPlugin(embedBluesky, {
// just an example, see default values below:
embedClass: "custom-classname",
embedDomain: "staging.bsky.app",
});
```

### Plugin default options

The plugin's default settings reside in [lib/defaults.js](lib/defaults.js). All of these values can be customized with an options object passed to the plugin.

| Option | Type | Default | Notes |
| ------------------- | ------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `allowFullscreen` | Boolean | `true` | Set to false to prevent the embedded post from being viewed in fullscreen mode. |
| `containerCss` | String | `"position: relative; width: 100%; padding-bottom: 0;"` | CSS applied to the container `<div>` that wraps the embedded post. |
| `embedClass` | String | `"eleventy-plugin-embed-bluesky"` | CSS class applied to the container `<div>` that wraps the embedded post. |
| `iframeCss` | String | `"border: 0; position: relative; width: 100%;"` | CSS applied to the `<iframe>` that contains the embedded post. |
| `iframeFrameborder` | String | `"0"` | Width of the `iframe`'s border in pixels. |
| `iframeHeight` | String | `"300"` | Height of the `iframe`. |
| `iframeScrolling` | String | `"no"` | Whether the `iframe` should have scrollbars. |
| `iframeWidth` | String | `"550"` | Width of the `iframe`. |
| `embedDomain` | String | `"bsky.app"` | Domain to use for embeds. Can be set to `staging.bsky.app` for staging or a custom domain for self-hosted instances. |

### Supported URL patterns

The plugin supports common URL variants as well. These will all work:

```markdown
<!-- No protocol: -->

bsky.app/profile/bsky.app/post/3lgu4lg6j2k2v

<!-- With custom domains: -->

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

<!-- With DID: -->

https://bsky.app/profile/did:plc:yc6gmb3bo56qotdsywnsxrxp/post/3lgvpv7k5sc26

<!-- With query parameters: -->

https://bsky.app/profile/bsky.app/post/3lgu4lg6j2k2v?foo=bar
```

## Notes and caveats

- This plugin is deliberately designed _only_ to embed when the URL is on its own line, and not inline with other text.
- To do this, it uses a regular expression to recognize Bluesky URLs, wrapped in an HTML `<p>` tag. If your Markdown parser produces any other output, it won't be recognized.
- The plugin supports both standard handles (e.g., `bsky.app`) and DID-based handles (e.g., `did:plc:yc6gmb3bo56qotdsywnsxrxp`).
- Post IDs must be at least 10 characters long and can contain letters and numbers.
- You can use the `embedDomain` option to embed posts from a staging environment or a self-hosted Bluesky instance. The custom domain must implement the Bluesky embed endpoint at `/profile/:handle/post/:id/embed`.
- This plugin uses [transforms](https://www.11ty.dev/docs/config/#transforms), so it alters Eleventy's HTML output as it's generated. It doesn't alter the source markdown.

## Credits

- Bluesky package 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
24 changes: 24 additions & 0 deletions packages/bluesky/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const pattern = require("./lib/pattern.js");
const replace = require("./lib/replace.js");
const defaults = require("./lib/defaults.js");

module.exports = function (eleventyConfig, options = {}) {
const config = Object.assign({}, defaults, options);

eleventyConfig.addTransform(
"embedBluesky",
async function (content, outputPath) {
// Return content untouched if there's no output path or it's not HTML
if (!outputPath || !outputPath.endsWith(".html")) {
return content;
}

try {
return content.replace(pattern, (...match) => replace(match, config));
} catch (error) {
console.warn("Error processing Bluesky embeds:", error);
return content;
}
},
);
};
24 changes: 24 additions & 0 deletions packages/bluesky/lib/defaults.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Default configuration for Bluesky embeds
*
* embedDomain: The domain to use for embeds.
* - Production: bsky.app
* - Staging: staging.bsky.app
* - Custom: Set to your own Bluesky-compatible instance
*
* Note: Custom domains must implement the Bluesky embed endpoint at /profile/:handle/post/:id/embed
*/
module.exports = {
// Embed appearance
embedClass: "eleventy-plugin-embed-bluesky",
containerCss: "position: relative; width: 100%; padding-bottom: 0;",
iframeCss: "border: 0; position: relative; width: 100%;",
iframeWidth: "550",
iframeHeight: "300",
iframeFrameborder: "0",
iframeScrolling: "no",
allowFullscreen: true,

// Bluesky configuration
embedDomain: "bsky.app", // Default to production, can be overridden for staging or custom domains
};
17 changes: 17 additions & 0 deletions packages/bluesky/lib/pattern.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Regex for parsing Bluesky URLs
*
* Notable points:
* - Matches both bsky.app and staging.bsky.app domains
* - Post IDs are alphanumeric and at least 10 characters long
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would there be a logical maximum character count for the post ID? Just wondering about RegEx efficiency 🤔

* - Profile IDs follow the handle format (including DID and custom domains)
* - Supports URLs with and without protocol
* - Handles query parameters
* - Handles trailing slashes
*
* The numbered capture groups are:
* 1. The handle
* 2. The post ID
*/

module.exports = /<p>\s*(?:<a [^>]*?>)?\s*(?:https?:)?(?:\/\/)?(?:w{3}\.)?(?:staging\.)?bsky\.app\/profile\/([^/\s]+)\/post\/([a-zA-Z0-9]{10,})(?:\/|\?[^<>\s]*)?\s*(?:<\/a>)?\s*<\/p>/;
31 changes: 31 additions & 0 deletions packages/bluesky/lib/replace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module.exports = function(match, config) {
try {
if (!match || !match[0]) {
return "";
}

// Extract handle and post ID from the match array
// match[1] is the handle
// match[2] is the post ID
const handle = match[1];
const postId = match[2];

if (!handle || !postId) {
return match[0];
}

// Create the embed URL using Bluesky's embed endpoint
const embedDomain = match[0].includes("staging.bsky.app") ? "staging.bsky.app" : config.embedDomain;
const embedUrl = `https://${embedDomain}/profile/${handle}/post/${postId}/embed`;

// Build the embed HTML
let embed = `<div class="${config.embedClass}" style="${config.containerCss}">`;
embed += `<iframe src="${embedUrl}" style="${config.iframeCss}" width="${config.iframeWidth}" height="${config.iframeHeight}" frameborder="${config.iframeFrameborder}" scrolling="${config.iframeScrolling}" ${config.allowFullscreen ? "allowfullscreen" : ""}></iframe>`;
embed += "</div>";

return embed;
} catch (error) {
console.warn("Error creating Bluesky embed:", error);
return match[0] || "";
}
};
31 changes: 31 additions & 0 deletions packages/bluesky/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "eleventy-plugin-embed-bluesky",
"version": "1.0.0",
"description": "An Eleventy plugin to automatically embed Bluesky posts, using just their URLs.",
"keywords": [
"11ty",
"eleventy",
"eleventy-plugin",
"bluesky"
],
"main": "index.js",
"scripts": {
"test": "npx ava",
"coverage": "nyc --reporter=lcov ava"
},
"files": [
"index.js",
"lib/**"
],
"author": {
"name": "Jason Shellen",
"url": "https://shellen.com"
},
"license": "MIT",
"homepage": "https://gfscott.com/embed-everything/",
"repository": {
"type": "git",
"url": "https://github.com/gfscott/eleventy-plugin-embed-everything.git"
},
"bugs": "https://github.com/gfscott/eleventy-plugin-embed-everything/issues"
}
25 changes: 25 additions & 0 deletions packages/bluesky/test/_validUrls.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Test URLs for Bluesky embeds
*/

export const validUrls = [
"https://bsky.app/profile/bsky.app/post/3lgu4lg6j2k2v",
"https://bsky.app/profile/stereogum.bsky.social/post/3lh27cswiwc27/",
"http://bsky.app/profile/merlevans.bsky.social/post/3lgzl25gr2c2k",
"//bsky.app/profile/bsky.app/post/3le6bze3nus2c",
"bsky.app/profile/bsky.app/post/3lgu4lg6j2k2v",
"https://bsky.app/profile/did:plc:yc6gmb3bo56qotdsywnsxrxp/post/3lgvpv7k5sc26",
"https://bsky.app/profile/merlevans.bsky.social/post/3lgzl25gr2c2k",
"https://bsky.app/profile/shellen.com/post/3ldmp5qd6es2p",
"https://bsky.app/profile/bsky.app/post/3lgu4lg6j2k2v?foo",
"https://bsky.app/profile/bsky.app/post/3lgu4lg6j2k2v?foo=bar",
"https://bsky.app/profile/bsky.app/post/3lgu4lg6j2k2v?foo&bar",
"https://bsky.app/profile/bsky.app/post/3lgu4lg6j2k2v?foo=bar&baz=qux"
];

export const invalidUrls = [
"https://bsky.app/profile/bsky.app/post/abc",
"https://bsky.app/user/bsky.app/post/3lgu4lg6j2k2v",
"https://bsky.app/profile/bsky.app/status/3lgu4lg6j2k2v",
"https://example.com/profile/bsky.app/post/3lgu4lg6j2k2v"
];
37 changes: 37 additions & 0 deletions packages/bluesky/test/pattern.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import test from "ava";
import pattern from "../lib/pattern.js";
import { validUrls, invalidUrls } from "./_validUrls.mjs";

/**
* Test pattern matching with valid strings
*/
validUrls.forEach((str) => {
test(`Pattern matches valid string: ${str}`, (t) => {
// Test basic URL
const basicMatch = pattern.exec(`<p>${str}</p>`);
t.truthy(basicMatch, "Basic URL should match");

// Test with link tags
const linkedMatch = pattern.exec(`<p><a href="${str}">${str}</a></p>`);
t.truthy(linkedMatch, "URL with link tags should match");

// Test with whitespace
const spacedMatch = pattern.exec(`<p> ${str} </p>`);
t.truthy(spacedMatch, "URL with whitespace should match");
});
});

/**
* Test pattern rejection of invalid strings
*/
invalidUrls.forEach((str) => {
test(`Pattern rejects invalid string: ${str}`, (t) => {
// Test basic invalid URL
const basicMatch = pattern.exec(`<p>${str}</p>`);
t.falsy(basicMatch, "Invalid URL should not match");

// Test with whitespace
const spacedMatch = pattern.exec(`<p> ${str} </p>`);
t.falsy(spacedMatch, "Invalid URL with whitespace should not match");
});
});
Loading