Bluesky is still the new kid on the social media block, but I’ve been using it for a while now, mainly for my motorcycling interests and thoughts. I liked the idea of embedding my posts directly onto my blog, much like people do with Twitter (or X, if you insist). So, I went digging to see how it could be done.

Now, I’m not the world’s greatest coder. I can get by in Python, CSS, and a bit of JavaScript, but I’m no full-stack wizard. Still, with a little trial and error, some help from the right sources, and a healthy dose of pigheadedness, I got it working. And since I figure others might want to do the same, here’s how I did it.

You can see it live in action at Two Wheels In. (Or more easily, just to the right of this post.)


Step 1: Adding the Bluesky Embed Code

For my site, I build with Bootstrap, which makes layout and responsiveness easier. With a little searching, I found that Vincent Will (Vincenius) had already put together a Bluesky embed script. Why try to reinvent the wheel, I asked myself. It’s simple, but it does the job.

To add it to a standard HTML page, I placed the script at the bottom of the file, just before the </body> tag. Then, where I wanted the posts to appear, I inserted this:

<bsky-embed
    username="twowheelsin.com"
    mode="light"
    limit="3">
</bsky-embed>

Put the code down the bottom of the html site and that’s it—Bluesky posts now show up on the page. But there was a problem: the entire posts was being delivered in full, which made the layout messy and looong. (I talk a lot). I needed a way to truncate the posts.


Step 2: Truncating the Posts

This is where things got tricky. JavaScript isn’t my strong suit, but after some trial and error, I came up with a script (bluesky-truncate.js) to trim down the posts and add a “Read More” button:

document.addEventListener("DOMContentLoaded", function () {
    console.log("✅ Bluesky Truncate Script Loaded");

    customElements.whenDefined("bsky-embed").then(() => {
        console.log("✅ bsky-embed component is ready!");

        const embeds = document.querySelectorAll("bsky-embed");
        if (!embeds.length) {
            console.error("❌ No Bluesky embeds found.");
            return;
        }

        function truncatePosts(embed) {
            if (!embed.shadowRoot) {
                console.error("❌ bsky-embed shadowRoot not found.");
                return false;
            }

            const posts = embed.shadowRoot.querySelectorAll('[id^="post-"]');
            console.log(`🔍 Checking for posts... Found: ${posts.length}`);

            if (posts.length > 0) {
                console.log("✅ Bluesky posts found!");

                posts.forEach(post => {
                    if (!post) return;

                    if (post.querySelector(".truncated-content")) return;

                    const contentWrapper = document.createElement("div");
                    contentWrapper.className = "truncated-content";
                    contentWrapper.style.overflow = "hidden";
                    contentWrapper.style.maxHeight = "120px";
                    contentWrapper.style.transition = "max-height 0.3s ease-in-out";

                    while (post.firstChild) {
                        contentWrapper.appendChild(post.firstChild);
                    }

                    post.appendChild(contentWrapper);

                    const readMore = document.createElement("span");
                    readMore.className = "read-more";
                    readMore.textContent = "Read More";
                    readMore.style.cursor = "pointer";
                    readMore.style.color = "#ff4500";
                    readMore.style.display = "block";
                    readMore.style.marginTop = "5px";

                    readMore.addEventListener("click", () => {
                        if (contentWrapper.style.maxHeight === "120px") {
                            contentWrapper.style.maxHeight = "none";
                            readMore.textContent = "Show Less";
                        } else {
                            contentWrapper.style.maxHeight = "120px";
                            readMore.textContent = "Read More";
                        }
                    });

                    post.appendChild(readMore);
                });

                return true;
            }

            return false;
        }

        embeds.forEach(embed => {
            let attempts = 0;
            const interval = setInterval(() => {
                if (truncatePosts(embed) || attempts >= 20) {
                    clearInterval(interval);
                    if (attempts >= 20) console.error("❌ Bluesky posts did not load in time.");
                }
                attempts++;
            }, 500);
        });
    });
});

To make it look decent, I added some CSS:

.bsky-embed .read-more {
    color: #ff4500;
    cursor: pointer;
    text-align: right;
    margin-top: 5px;
}

Step 3: Getting It to Work in WordPress

This part took a little more effort. WordPress keeps its content files in /wp-content/, and I’d already written a custom theme for my site, so I added a sidebar to display the embedded posts.

1. Creating a sidebar:
I created sidebar.php inside my theme folder:

<?php if (is_active_sidebar('sidebar-1')) : ?>
    <aside id="secondary" class="widget-area">
        <?php dynamic_sidebar('sidebar-1'); ?>
    </aside>
<?php endif; ?>

2. Registering the sidebar in functions.php:

I added this code to the function.php.

function twi_register_sidebar() {
    register_sidebar(array(
        'name'          => __('Sidebar', 'twowheelsin'),
        'id'            => 'sidebar-1',
        'description'   => __('Widgets in this area will be shown in the sidebar.', 'twowheelsin'),
        'before_widget' => '<div class="widget %2$s">',
        'after_widget'  => '</div>',
        'before_title'  => '<h3 class="widget-title">',
        'after_title'   => '</h3>',
    ));
}
add_action('widgets_init', 'twi_register_sidebar');

3. Including the sidebar in single.php:

Then I added this code to single.php

<div class="col-lg-4">
    <aside class="blog-sidebar">
        <?php if (is_active_sidebar('blog-sidebar')) : ?>
            <?php dynamic_sidebar('blog-sidebar'); ?>
        <?php else : ?>
            <p>Add widgets in the WordPress admin panel.</p>
        <?php endif; ?>
    </aside>
</div>

I also needed to include links to the scripts for Vince’s Bluesky script and my truncation script in footer.php.

Once all that was in place, my Bluesky posts were finally embedded in the sidebar of my WordPress blog.


Final Thoughts

A little involved? Sure. But worth it. I’m sure someone will roll out a WordPress plugin for this soon enough, but until then, this works just fine. Feel free to build on this or rewrite the code for your own purposes. But don’t message me with questions. Do what I did and figure it out. Or ask ChatGPT.

(Also, don’t message me if it fucks up your website. I spent a lot of time getting this to work and learned a lot about WordPress and JavaScript on the way. I screwed up a couple of things and had to reinstall from a backup once. It’s a learning curve for me, too.)