diff --git a/demo/src/bluesky.md b/demo/src/bluesky.md new file mode 100644 index 00000000..2a931e50 --- /dev/null +++ b/demo/src/bluesky.md @@ -0,0 +1,5 @@ +--- +title: Bluesky +--- + +https://bsky.app/profile/bsky.app/post/3lgu4lg6j2k2v diff --git a/demo/src/index.md b/demo/src/index.md index d39d9af1..77269885 100644 --- a/demo/src/index.md +++ b/demo/src/index.md @@ -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/) @@ -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/) \ No newline at end of file +- [YouTube example](/youtube/) diff --git a/packages/bluesky/CHANGELOG.md b/packages/bluesky/CHANGELOG.md new file mode 100644 index 00000000..fd3ba3e0 --- /dev/null +++ b/packages/bluesky/CHANGELOG.md @@ -0,0 +1,5 @@ +## 1.0.0 + +### Major Changes + +- Adds support for embedding Bluesky posts using just their URLs. diff --git a/packages/bluesky/README.md b/packages/bluesky/README.md new file mode 100644 index 00000000..208d236f --- /dev/null +++ b/packages/bluesky/README.md @@ -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 `
` that wraps the embedded post. | +| `embedClass` | String | `"eleventy-plugin-embed-bluesky"` | CSS class applied to the container `
` that wraps the embedded post. | +| `iframeCss` | String | `"border: 0; position: relative; width: 100%;"` | CSS applied to the ``; + embed += "
"; + + return embed; + } catch (error) { + console.warn("Error creating Bluesky embed:", error); + return match[0] || ""; + } +}; diff --git a/packages/bluesky/package.json b/packages/bluesky/package.json new file mode 100644 index 00000000..fd6b9185 --- /dev/null +++ b/packages/bluesky/package.json @@ -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" +} diff --git a/packages/bluesky/test/_validUrls.mjs b/packages/bluesky/test/_validUrls.mjs new file mode 100644 index 00000000..e9908e10 --- /dev/null +++ b/packages/bluesky/test/_validUrls.mjs @@ -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" +]; diff --git a/packages/bluesky/test/pattern.test.mjs b/packages/bluesky/test/pattern.test.mjs new file mode 100644 index 00000000..bb8de416 --- /dev/null +++ b/packages/bluesky/test/pattern.test.mjs @@ -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(`

${str}

`); + t.truthy(basicMatch, "Basic URL should match"); + + // Test with link tags + const linkedMatch = pattern.exec(`

${str}

`); + t.truthy(linkedMatch, "URL with link tags should match"); + + // Test with whitespace + const spacedMatch = pattern.exec(`

${str}

`); + 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(`

${str}

`); + t.falsy(basicMatch, "Invalid URL should not match"); + + // Test with whitespace + const spacedMatch = pattern.exec(`

${str}

`); + t.falsy(spacedMatch, "Invalid URL with whitespace should not match"); + }); +}); diff --git a/packages/bluesky/test/replace.test.mjs b/packages/bluesky/test/replace.test.mjs new file mode 100644 index 00000000..68e2fc78 --- /dev/null +++ b/packages/bluesky/test/replace.test.mjs @@ -0,0 +1,128 @@ +import test from "ava"; +import pattern from "../lib/pattern.js"; +import replace from "../lib/replace.js"; +import { validUrls } from "./_validUrls.mjs"; + +test("Creates valid embed HTML with default domain", (t) => { + const url = "https://bsky.app/profile/bsky.app/post/3lgu4lg6j2k2v"; + const matches = pattern.exec(`

${url}

`); + const result = replace(matches, { + embedDomain: "bsky.app", + embedClass: "eleventy-plugin-embed-bluesky", + containerCss: "width: 100%;", + iframeCss: "width: 100%; border: 0;", + iframeWidth: "550", + iframeHeight: "300", + iframeFrameborder: "0", + iframeScrolling: "no", + allowFullscreen: true, + }); + + t.true( + result.includes('class="eleventy-plugin-embed-bluesky"'), + "Should include the default class", + ); + t.true( + result.includes( + 'src="https://bsky.app/profile/bsky.app/post/3lgu4lg6j2k2v/embed"', + ), + "Should use correct embed URL", + ); + t.true(result.includes('width="550"'), "Should include width"); + t.true(result.includes('height="300"'), "Should include height"); + t.true(result.includes("allowfullscreen"), "Should include allowfullscreen"); +}); + +test("Supports staging domain", (t) => { + const url = "https://staging.bsky.app/profile/bsky.app/post/3lgu4lg6j2k2v"; + const matches = pattern.exec(`

${url}

`); + const result = replace(matches, { + embedDomain: "staging.bsky.app", + embedClass: "eleventy-plugin-embed-bluesky", + containerCss: "width: 100%;", + iframeCss: "width: 100%; border: 0;", + iframeWidth: "550", + iframeHeight: "300", + iframeFrameborder: "0", + iframeScrolling: "no", + allowFullscreen: true, + }); + + t.true( + result.includes( + 'src="https://staging.bsky.app/profile/bsky.app/post/3lgu4lg6j2k2v/embed"', + ), + "Should use staging domain for embed", + ); +}); + +test("Supports custom domain", (t) => { + const url = "https://bsky.app/profile/bsky.app/post/3lgu4lg6j2k2v"; + const matches = pattern.exec(`

${url}

`); + const result = replace(matches, { + embedDomain: "custom.example.com", + embedClass: "eleventy-plugin-embed-bluesky", + containerCss: "width: 100%;", + iframeCss: "width: 100%; border: 0;", + iframeWidth: "550", + iframeHeight: "300", + iframeFrameborder: "0", + iframeScrolling: "no", + allowFullscreen: true, + }); + + t.true( + result.includes( + 'src="https://custom.example.com/profile/bsky.app/post/3lgu4lg6j2k2v/embed"', + ), + "Should use custom domain for embed", + ); +}); + +test("Handles invalid match gracefully", (t) => { + const result = replace(null, { + embedDomain: "bsky.app", + embedClass: "eleventy-plugin-embed-bluesky", + }); + + t.is(result, "", "Should return empty string for invalid match"); +}); + +/** + * Test embed generation with valid strings + */ +validUrls.forEach((str) => { + test(`Generates valid embed: ${str}`, (t) => { + const matches = pattern.exec(`

${str}

`); + t.truthy(matches, "Should find pattern matches"); + + const result = replace(matches, { + embedDomain: "bsky.app", + embedClass: "eleventy-plugin-embed-bluesky", + containerCss: "width: 100%;", + iframeCss: "width: 100%; border: 0;", + iframeWidth: "550", + iframeHeight: "300", + iframeFrameborder: "0", + iframeScrolling: "no", + allowFullscreen: true, + }); + + t.true( + result.includes('class="eleventy-plugin-embed-bluesky"'), + "Should include default class", + ); + t.true(result.includes("iframe"), "Should create iframe element"); + t.true(result.includes('/embed"'), "Should use embed endpoint"); + + // Extract handle and post ID from the URL for verification + const urlParts = str.match(/\/profile\/([^/]+)\/post\/([^/?]+)/); + t.truthy(urlParts, "Should be able to parse URL parts"); + + const [_, handle, postId] = urlParts; + t.true( + result.includes(`/profile/${handle}/post/${postId}/embed`), + "Should use correct handle and post ID", + ); + }); +}); diff --git a/packages/everything/.eleventy.js b/packages/everything/.eleventy.js index 99bc9e4a..36941c5d 100644 --- a/packages/everything/.eleventy.js +++ b/packages/everything/.eleventy.js @@ -1,4 +1,5 @@ // Needs to be in global context to be accessed by module +global.bluesky = require("eleventy-plugin-embed-bluesky"); global.instagram = require("eleventy-plugin-embed-instagram"); global.openstreetmap = require("eleventy-plugin-embed-openstreetmap"); global.soundcloud = require("eleventy-plugin-embed-soundcloud"); diff --git a/packages/everything/lib/pluginDefaults.js b/packages/everything/lib/pluginDefaults.js index abab733a..00eb071d 100644 --- a/packages/everything/lib/pluginDefaults.js +++ b/packages/everything/lib/pluginDefaults.js @@ -4,6 +4,7 @@ * The definitive list of all plugins that _can_ be aggregated by this plugin. */ exports.validPlugins = [ + "bluesky", "instagram", "openstreetmap", "soundcloud", @@ -22,6 +23,7 @@ exports.validPlugins = [ * The list of all plugins that are active by default. */ exports.defaultPlugins = [ + "bluesky", "instagram", "openstreetmap", "spotify", diff --git a/packages/everything/package.json b/packages/everything/package.json index 3cbca094..1d40cdfe 100644 --- a/packages/everything/package.json +++ b/packages/everything/package.json @@ -25,11 +25,13 @@ "twitch", "twitter", "youtube", - "vimeo" + "vimeo", + "bluesky" ], "license": "MIT", "dependencies": { "deepmerge": "^4.3.1", + "eleventy-plugin-embed-bluesky": "workspace:^", "eleventy-plugin-embed-instagram": "workspace:^", "eleventy-plugin-embed-openstreetmap": "workspace:^", "eleventy-plugin-embed-soundcloud": "workspace:^", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9ff69fb..a7f38dd6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,11 +37,16 @@ importers: specifier: workspace:* version: link:../packages/everything + packages/bluesky: {} + packages/everything: dependencies: deepmerge: specifier: ^4.3.1 version: 4.3.1 + eleventy-plugin-embed-bluesky: + specifier: workspace:^ + version: link:../bluesky eleventy-plugin-embed-instagram: specifier: workspace:^ version: link:../instagram