Category: General

  • Building a CSS Carousel WordPress Block

    Building a CSS Carousel WordPress Block

    Today I want to introduce you to the latest addition to CSS, specifically the CSS Overflow 5 specification. I don’t usually write about CSS features, but this one is particularly intriguing because it allows for pure CSS-based carousels — no JavaScript required! Why is this significant?

    The last few years have seen great improvements in the web performance space. We have been democratizing performance for WordPress and beyond. This trend continues also in 2025. New web platform features make it easier than ever to build websites with great performance, accessibility, and developer experience.

    For example, invoker commands enable building interactive UIs declaratively using only HTML attributes. Similarly, there are new enhancements in the works to easily build tooltips. They use a combination of popover="hint", CSS anchor positioning, and interest invokers. Full customization of <select> elements is possible too. And now, CSS carousels.

    According to HTTP Archive, over 54% of all WordPress sites use a JavaScript slider library (source). These libraries are often quite heavy, negatively impacting performance. For example, Swiper is a whopping 140KB (minified). With CSS carousels, there is no need for such libraries anymore!

    Why CSS carousels are superior

    Here’s why I am excited about this new web platform addition:

    1. Great performance
      Less JavaScript means better performance for your website. This CSS-powered solution also won’t block the main thread like some of these JavaScript libraries. That means it will always perform better than any JavaScript solution.
    2. Strong accessibility
      Carousel best practices such as keyboard support are handled by the browser, so you don’t have to worry about it. Sure, you might be able to build an equally accessible carousel yourself. However, it would cost you a lot more time (and code). Plus, CSS carousels work even with JavaScript disabled.
    3. Excellent developer experience
      No external dependencies, strong accessibility, and performance out of the box. CSS carousels make maintenance a bliss, saving you lots of valuable time now and in the future.
    CSS carousel demo from the Chrome for Developers blog

    Right now this feature is available in Chrome (as of version 135). Other browsers will probably follow soon. Meanwhile, they could fall back to existing JavaScript solutions.

    Key ingredients for a CSS-only carousel

    At the core of a basic carousel is a scrolling list of items (either horizontal or vertical). Ideally combined with some scroll snapping (where content “snaps” into place as you scroll) to ensure that the items are always nicely visible. It could look something like this:

    <ul class="carousel">
      <li><img ...></li>
      <li><img ...></li>
      <li><img ...></li>
    </ul>Code language: HTML, XML (xml)
    .carousel {
      display: grid;
      grid-auto-flow: column;
      grid-auto-columns: 100%;
      overflow-x: auto;
      overscroll-behavior-x: contain;
      scroll-snap-type: x mandatory;
      anchor-name: --carousel;
    
      > li {
        scroll-snap-align: center;
      }
    }Code language: CSS (css)

    Carousels often have arrows on the side to go forward and backward (or up and down). With CSS Overflow Level 5, these arrows can now be easily added using the new ::scroll-button() pseudo-elements. For example:

    .carousel {
      /* for the left/right arrows */
      /* Note: they are actually siblings of .carousel */
      &::scroll-button(*) {
        position: fixed;
        position-anchor: --carousel;
      }
    
      &::scroll-button(inline-start) {
        position-area: inline-start center;
        /* emoji/icon with alt text for accessibility */
        content: "⬅️" / attr(data-previous);
    }
    
      &::scroll-button(inline-end) {
        position-area: inline-end center;
        content: "➡️" / attr(data-next);
      }
    }Code language: CSS (css)

    Similarly, if you want to add “dots” below the carousel, you can do so using the new ::scroll-marker (the dot) and  ::scroll-marker-group (wraps all the dots) pseudo-elements. It could look like this:

    .carousel {
      scroll-marker-group: after;
    
      /* Note: this is actually a sibling of .carousel */
      &::scroll-marker-group {
        display: grid;
        grid-auto-columns: 20px;
        grid-auto-flow: column;
        gap: 20px;
      }
    
      /* This will add one dot for each <li> */
      & > li::scroll-marker {
        content: "⭕";
        cursor: pointer;
        aspect-ratio: 1;
    
        &:target-current {
          content: "🔴";
        }
      }
    }Code language: CSS (css)

    Finally and optionally, you could use CSS inertness to hide any “hidden” carousel elements from the accessibility tree. This of course depends on your use case. The behavior is the same as when using the HTML inert attribute, but defined using CSS. Example:

    .carousel {
      > li {
        container-type: scroll-state;
    
        > .card {
          @container not scroll-state(snapped: x) {
            interactivity: inert;
            opacity: .25;
          }
        }
      }
    }Code language: CSS (css)

    These are all the basic components you need to create a carousel using modern CSS. The result is pretty impressive:

    If you are using Chrome 135 or later, you should see a carousel complete with arrows, dots, and keyboard support. In other browsers you simply get a swipeable set of images. Here’s a screenshot:

    A simple CSS carousel with arrow navigation and dots below the carousel
    The combined CSS in action to build a fully functioning carousel. Note the filled dot and the disabled arrow on the last slide.

    But CSS carousels can do a lot more. If you would like some inspiration, I highly recommend checking out the Carousel Gallery site with tons of incredible demos. Each demo covers a real use case found on the internet. They all show how to orchestrate scroll buttons and markers with scroll-driven animations, scroll-state() queries, and much more. Here’s one of the demos of a e-commerce product image gallery using a CSS carousel:

    You can build a gallery of product images for your online store using only CSS

    Want to get more hands on and dive right in? Try the Carousel Configurator to help visualize the capabilities of CSS-only carousels.

    Building a CSS carousel WordPress block

    As I mentioned earlier, over 54% of all WordPress sites use at least one of the popular slider libraries. So it’s no wonder that there have been discussions about introducing a first-party carousel block for many years. One of the main blockers was of course the reliance on JavaScript.

    In 2025, building a performant, accessible, and maintainable carousel using only CSS finally becomes a breeze. To prove it, let’s apply these new features in WordPress by building a custom carousel block!

    Tip: If you want to see the final result, check out this GitHub repository.

    Setting up a carousel block variation

    For the sake of simplicity, I am not going to build a completely new block from the ground up. Instead, I am leveraging the Block Variations API to make a carousel a variation of the gallery block. This saves a lot of boilerplate code.

    The nice thing about block variations is that I can set them up entirely using PHP. I only need some JavaScript to update the block’s HTML class attribute for properly targeting the carousel variation using CSS. Here’s an excerpt:

    /**
     * Adds carousel variation to gallery block.
     *
     * @param array    $variations List of all block variations.
     * @param WP_Block $block_type Block type instance.
     * @return array Filtered list of block variations.
     */
    function css_carousel_block_add_carousel_variation( $variations, $block_type ) {
    	if ( 'core/gallery' !== $block_type->name ) {
    		return $variations;
    	}
    
    	$variations[] = array(
    		'name'        => 'carousel',
    		'title'       => __( 'Carousel', 'css-carousel-block' ),
    		'description' => __( 'A carousel', 'css-carousel-block' ),
    		'scope'       => array( 'block', 'inserter', 'transforms' ),
    		'isDefault'   => false,
    		'attributes'  => array(
    			'displayType' => 'carousel',
    		),
    	);
    
    	return $variations;
    }
    
    add_filter( 'get_block_type_variations', 'css_carousel_block_add_carousel_variation', 10, 2 );Code language: PHP (php)

    Check out the GitHub repository for the full code.

    CSS carousels in the editor

    The block markup on the frontend and in the editor is very similar. That means we can use the same CSS in both places, making for a true WYSIWYG experience. Additional bonus: we do not have to use any janky JavaScript library in the block editor that messes with React. The result:

    CSS carousel block variation in the editor
    CSS carousel block variation in the editor.

    While the CSS carousel already works great in the editor, there are some minor differences to the frontend.

    For example, the translated, human-readable labels for the scroll markers (arrows) are added via data-* attributes on the frontend. This way, they can be used in the content CSS property. Unfortunately, I couldn’t find a suitable filter in the editor to achieve the same.

    Second, in the editor we need to cater for the gallery block’s inner image blocks and their block toolbar. It looks odd when the toolbar stays in place when swiping through the carousel. This would be a great opportunity to use new scroll snap events such as scrollSnapChange. We can hide the toolbar when swiping and automatically select the next block upon slide change.

    Further iterating on this CSS carousel block

    Turning a basic gallery block into a feature-rich carousel is surprisingly easy. But of course this is a mere proof of concept. There is a lot more that could be done.

    For instance, the scroll buttons and markers in this demo are positioned using the new CSS anchor positioning API. This requires each carousel to have its unique anchor name. That’s something I haven’t implemented yet in this proof of concept.

    In addition to that, cross-browser support is an important consideration when implementing such new features. Since CSS carousels are currently only supported by Chrome, there could be a fallback solution using JavaScript in other browsers. This is very easy to implement. Check out this pull request for the Twenty Fourteen theme for an example.

    It’s also worth mentioning that work on CSS carousels is still ongoing. For example, work is already being done to let you add your own components for scroll buttons and markers. This makes it easier to add custom SVG icons or use Tailwind classes. Another highly requested feature is support for cyclic scrolling. Right now this still requires JavaScript.

    Share your feedback

    Long story short, CSS carousels are really powerful. WordPress users and contributors have been wanting a carousel block for years. This proof of concept could be the start of it. And if you are a developer who builds carousels for clients’ websites, why use this new feature in your next project? That’s why I would love for you to try it out, provide feedback, and help make it happen.

  • Enabling AI-Powered WordPress Development with WP-CLI and the Model Context Protocol (MCP)

    Enabling AI-Powered WordPress Development with WP-CLI and the Model Context Protocol (MCP)

    Last week I participated at the CloudFest Hackathon, where I was leading a project to bring the Model Context Protocol (MCP) to WordPress. I was intrigued by the challenge to try to bring this “USB-C port for AI applications” to the space I know best.

    The goal of this project was to enable AI-powered WordPress development by implementing MCP in the WordPress ecosystem and exposing it through WP-CLI, which is the official command line interface for WordPress. So for once I was working on server-side AI and not Web AI.

    WordPress development workflows currently lack seamless integration with AI capabilities, particularly during local development. While REST API endpoints enable AI interactions with live sites, developers working with local WordPress installations have limited options for AI-assisted content creation and site management.

    This project transforms WordPress into an MCP Server and WP-CLI into an MCP Host through a new package, enabling direct AI interactions with WordPress installations during development. This approach provides developers with powerful AI capabilities without requiring a live site or REST API endpoints.

    I also tried explaining it in simpler terms in my pre-event interview with Christian Taylor:

    MCP in action

    Despite MCP being bleeding edge technology, the team managed to implement a very powerful server implementation that exposes any REST API route and WP-CLI command as function calling capabilities. A separate MCP client then is able to connect to this and other servers. I say bleeding edge because MCP is brand new and lacks thorough documentation and tooling in anything that’s not Python or JavaScript. The PHP MCP SDK we used is just an AI-generated clone of the Python version and could definitely be improved. Also, the protocol itself is still undergoing big changes, as indicated by this highly relevant RFC, which was published right after the Hackathon concluded.

    In addition to the MCP part, we implemented and further refined an LLM-agnostic CLI command for natural language processing. The project team used the AI Services plugin for this, but LLPhant would be a similar alternative. And ideally this would support local models too.

    In short, we set out to enable use cases such as $ wp ai "Summarize my latest GitHub issues in a new blog post", where the GitHub tool could be offered by one MCP server, and the blog post tool by another.

    The team even implemented image editing capabilities using the brand new Gemini 2.0 Flash native image generation that was just released days before the event. Here you can see it in action:

    Hat tip to Marco Chiesi for creating this nice demo minutes before the Hackathon deadline

    Hackathon experience

    It was my fourth time at the CloudFest Hackathon and the second time leading a project, but the first time doing it myself. Before the event I set up a basic GitHub repository as a starting point, but didn’t have much time to do prepare more (e.g. by creating some issues or diagrams). Then, after the project patch at the start of the hackathon, I was overwhelmed by the interest in this project. We ended up being 10 people from diverse backgrounds, which was really nice!

    The project team after the hackathon. Missing: Joost. Photo by Roan de Vries.

    The hackathon team:

    Thanks to these wonderful people we won the social media master award thanks to our outreach during the event (special thanks to Milana for a ton of great tweets!), and overall won second place!

    The project team on stage after winning the social media master award. Photo by Roan de Vries.

    Conclusion

    I am really excited about the future potential of the project and keen to continue working on it in some form or another. Especially the thought of implementing the MCP server in WordPress core or in a plugin is very promising.

    People interested in the project are encouraged to check out the GitHub repository to collaborate.

  • How I built plugintable.com and why I did not use WooCommerce

    How I built plugintable.com and why I did not use WooCommerce

    A couple of weeks ago I officially launched the Periodic Table of WordPress Plugins. The website, plugintable.com, showcases the most popular plugins in a unique way. Its accompanied by a small e-commerce store where one can buy the whole thing as a poster. In my previous post I already hinted at how it works, but today I want to provide more details.

    So, how did I build plugintable.com, and why did I not use WooCommerce?

    The Backend

    Let’s look at the main website first. Behind the scenes there is a PHP CLI script that powers it all. It uses Rarst’s WPOrg Client library to fetch the most popular plugins from WordPress.org. After that it makes names and descriptions more readable and does an initial sort.

    Here’s also where I add the “element” symbols for every plugin. This requires a ton of manual overrides, because developers like adding tons of buzzwords to their plugin names (for SEO?). However, that decreases readability on a project like this. Plus, space on each “card” on the plugin table is limited, so sometimes I need to shorten the names.

    The Frontend

    On the frontend, I wanted to keep things simple as well. It is mostly static HTML, sprinkled with some PHP to loop over all the plugins and assembling their markup. The layout itself then uses CSS grid and custom properties to make styles as reusable as possible.

    I added JavaScript only where necessary, for example to make the tooltips work when clicking on an individual plugin. The tooltip animations are actually the result of using the View Transitions API.

    The responsive design probably took the most time to get right. Given the nature of the project, it really works best on a desktop screen. However, I still wanted to make it as good as possible on mobile. Due to limited space, I initially hide some information and make the tooltips fill the whole screen when opened. This actually requires a bit of a JS / CSS hack to prevent scrolling.

    In general, I wanted to make the site as fast as possible, which meant cutting back on adding unnecessary features. All CSS is minified and inlined and the fonts are self-hosted and size-adjusted to prevent layout shifts.

    A lot of these optimizations would have been easier with a JavaScript stack. A lot of the frontend tooling is written in JS after all. With a JS-powered site I also would not have any issues with excessive DOM size. Because then I would update the DOM on the fly and not construct everything beforehand in PHP. However, for such a simple site I did not want to build everything with a SSR-powered React framework or something. Keep it simple, stupid.

    In the end I managed to get good performance grades regardless, which is what ultimately counts.

    Screenshot of the PageSpeed Insights results for plugintable.com, showing a 100/100 performance score
    Screenshot of the PageSpeed Insights results for plugintable.com

    The Poster

    Once I had the website itself more or less finalized, it was time to think about the poster version. Printing the Periodic Table of WordPress Plugins on a poster to hang it on my wall has been a dream of mine for a long time.

    Since I am not a graphic designer and proficient with InDesign or similar, I tried to build upon what I already had: HTML & CSS. Using some print-specific styles such as tweaked font sizes and @page at-rules, I was able to build a poster version in no time. Now I just needed to print it!

    It was clear to me from the beginning that I want to build this not only for myself, but also for others around to world. That’s why I began researching print-on-demand providers, where I can print this only when needed without having to turn my apartment into storage space for paper.

    There are tons of them out there, all with different features, quality, and prices. In the end, I chose Printful. I liked their quality, the sizes and options (wooden frames!). Plus, they handle everything for me—from printing, taxes, shipping.

    Printful itself is not a shopping platform, so I still needed an online store to get these out of the door. Luckily, they have tons of integrations.

    A framed poster of the Periodic Table of WordPress plugins on a wall above a desk.
    Printful automatically creates these mockup pictures for the products I designed, very cool!

    The Store

    The requirements for the store were quite simple:

    • I have this poster that I want for myself and maybe a few others out there.
    • It’s basically only 1 product, so it doesn’t need to be a complex solution.
    • It’s a very time-sensitive offer given the WordPress 20th anniversary. So I need something quickly, but it’s not super long-living.
    • I don’t want to spend a lot of money on it.

    As a WordPress person, of course I looked into WooCommerce. However, it’s clearly not the right tool for the job. I didn’t have time to set up a new a new WordPress site with WooCommerce, find a reasonable theme, maybe buy some extensions, wrangle with payments, and deal with all the legal aspects of selling things online.

    So what else is out there?

    Initially, I stumbled upon Lemon Squeezy, which looked really cool. Their pricing is simple (0$/month + 5% per transaction) and the brand really appealing. The one big downside was that third-party integrations with providers like Printful is pretty far down on their roadmap, so that was a dealbreaker for me.

    Shopify was also on my list to explore as I used their developer platform once in the past. It’s not cheap (typically 39$/month), but they also have a starter plan at $5/month and a Printful integration.

    When I briefly tried it, I was able to get up and running very quickly. The amount of things you can and should customize is a bit overwhelming, but in the end I managed to put together something usable—shop.plugintable.com was born.

    The Experience

    As I said at the beginning, it has been only a few weeks since I launched plugintable.com. It’s too early to draw conclusions, but it definitely has been an interesting experience so far.

    Once the dust settles, however, I plan on writing another review post with some stats about the whole project. Until then, why not buy a poster? 😉 Remember, all proceeds will be donated back to the community!

  • Using Bento Components for Gutenberg Block Development

    Using Bento Components for Gutenberg Block Development

    Last week saw the launch of Bento, an easy to use component library that helps you achieve a great page experience.

    Today, I would like to share some thoughts on how Bento components can be used in Gutenberg in order to reduce development and maintenance costs while at the same time ensuring great user experience (UX) and page experience (PX).

    In addition to the core blocks available natively in Gutenberg, developers can create custom blocks for use in the editor. Typically, with Gutenberg you have to implement the same functionality twice, first by creating a block’s Edit component in React and then by re-implementing the same look and feel without React for the frontend. This causes a lot of duplicate work and additional maintenance burden, and potentially even some disparity between the two versions.

    This is where Bento comes into play.

    Developed by the AMP project, Bento offers well-tested, cross-browser compatible and accessible web components (aka custom elements) that can be used on any website. They are highly performant and contribute to an excellent page experience.

    These components are not only available as custom elements, but also as React and Preact components with the same features and API. That makes them an ideal candidate for use in Gutenberg.

    You can read more about it on the new Bento blog.

    Building a Bento Gutenberg Block

    Since Bento and Gutenberg make such a great couple, there is even a dedicated guide on how to create Gutenberg blocks using Bento.

    It is the result of a proof-of-concept Bento-powered Gutenberg block I have been working on over the last couple of months. In that article you learn how to apply these learnings yourself. You may also skip this part and jump straight to the repository containing the final code of that tutorial.

    I hope these examples give you a better sense of Bento’s potential for Gutenberg block development and I am looking forward to sharing more examples and solutions in the future as the component library evolves.

  • Safely Using Strings Containing Markup in React with DOMParser

    Safely Using Strings Containing Markup in React with DOMParser

    For the Web Stories WordPress plugin I came up with a solution to parse strings containing markup in a React application by leveraging the DOMParser interface. This is especially useful when dealing with translations where you would want to avoid any string concatenation.

    I’ve previously written quite a bit on JavaScript internationalization in WordPress 5.0+. However, one aspect I did not address at the time was how to use these new features with translations containing markup. That’s because it was simply not possible until recently, unless you would use a dangerous function like dangerouslySetInnerHTML. But since that would pose security risks, it is not advisable for use in this case.

    Thankfully, a new createInterpolateElement function was introduced to Gutenberg late last year that solves the problem in a safe way. It does so by sanitizing the input string using a simple parser that removes any unwanted markup. Here’s an example:

    import { __ } from '@wordpress/i18n';
    import { createInterpolateElement } from '@wordpress/elementt';
    import { CustomComponent } from '../custom-component.js';
    
    const translatedString = createInterpolateElement(
      __( 'This is a <span>string</span> with a <a>link</a> and a self-closing <custom_component />.' ),
      {
        span: <span>,
        a: <a href="https://make.wordpress.org/"/>,
        custom_component: <CustomComponent />,
      }
    );Code language: JavaScript (javascript)

    Any tag in the translated string that is part of the map in the second argument will be replaced by that component. So span will be replaced by an actual span tag, for example. If the translation contains any other markup not in the map, let’s say a some <img onClick={doBadStuff()} />, it would simply be discarded. Awesome!

    Now, as you can see from the example above, this utility function is part of the @wordpress/element package and not @wordpress/i18n, as one might have expected. But what if you don’t want to use the former, or perhaps can’t?

    An Alternative to createInterpolateElement

    To answer this question, I looked at the implementation of createInterpolateElement under the hood. It’s actually quite neat, but also a bit complex using a regex-based tokenizer. I wanted something simpler.

    The requirements were straightforward:

    • It needs to be fast at parsing strings with some simple markup
    • It needs to be secure
    • It needs to work in modern browsers (no IE support)

    My research quickly led me to the DOMParser interface, which allows parsing XML or HTML source code from a string into an HTMLDocument. It is supported by all major browsers. But does it also work for this use case? I was keen to find out!

    From DOMParser to React

    Specifically, I looked into using DOMParser.parseFromString() to parse a given string into an HTMLDocument, traverse through that document and create actual React elements (using React.createElement and React.cloneElement) for every found HTML element based on the provided map. Text elements could just be used as-is. This worked incredibly well from the get-go. Here’s an excerpt of the final code:

    const node = new DOMParser().parseFromString(children, 'text/html').body
      .firstChild;
    
    // Loops through the document and calls transformNode on each node.
    transform(node, mapping).map((element, index) => (
      <Fragment key={index}>{element}</Fragment>
    ));
    
    function transformNode(node, mapping = {}) {
      const { childNodes, localName, nodeType, textContent } = node;
      if (Node.TEXT_NODE === nodeType) {
        return textContent;
      }
    
      const children = node.hasChildNodes()
        ? [...childNodes].map((child) => transform(child, mapping))
        : null;
    
      if (localName in mapping) {
        return React.cloneElement(mapping[localName], null, children);
      }
    
      return React.createElement(localName, null, children);
    }Code language: JavaScript (javascript)

    You can find the full code including documentation and tests on GitHub.

    A key difference to createInterpolateElement is that elements missing from the map won’t be simply discarded, but inserted without any props/attributes being set, mitigating any security risks. It also means that void elements such as <br> can be used in the translatable strings, which can come in handy at times.

  • Client-Side Video Optimization

    Client-Side Video Optimization

    With Web Stories for WordPress, we want to make it easy and fun to create beautiful, immersive stories on the web. Videos contribute a large part to the immersive experience of the story format. Thus, we wanted to streamline the process of adding videos to stories as much as possible. For instance, the Web Stories editor automatically creates poster images for all videos to improve the user experience and accessibility for viewers. One key feature we recently introduced in this area is client-side video optimization — video transcoding and compression directly in the browser.

    New to Web Stories? You can learn more about this exciting format in my recent lightning talk.

    The Problem With Self-Hosting Videos

    The Web Stories format does not currently support embedding videos from platforms like YouTube, which means one has to host videos themselves if they want to use any. And here’s where things get cumbersome, because you have to ensure the videos are in the correct file format, have the right dimensions and low file size to reduce bandwidth costs* and improve download speed.

    A typical use case for a story creator is to record a video on their iPhone and upload it straight to WordPress for use in their next story. There’s just one problem: your iPhone records videos in the .mov format, which is not supported by most browsers. Once you realize that, you might find some online service to convert the .mov file into an .mp4 file. But that doesn’t address the video dimensions and file size concerns. So you try to find another online service or tutorial to help with that. Ugh.

    We wanted to prevent you from having to go down the rabbit hole of figuring this all out.

    * Aside: To reduce bandwidth costs, we are actually working on a solution to serve story videos directly from the Google CDN, which is pretty cool and will help a lot to reduce costs for creators!

    Alternatives

    Of course, there are some alternatives to this. For example services like Transcoder or Jetpack video hosting. These solutions will transcode videos on-the-fly during upload on their powerful servers. So you upload your .mov file, but you receive an optimized .mp4 video. However, that requires you to install yet another plugin. Plus, these services won’t optimize the video to the dimensions optimal for stories. So there’s still room for improvement.

    We wanted a solution without having to rely on third-party plugins or services. Something that’s built into the Web Stories plugin and ready to go, requiring zero setup. And since hosting providers don’t typically offer any tools for server-side video optimization, we had to resort to the client.

    Making Video Optimization Seamless

    In our research, we quickly stumbled upon ffmpeg.wasm, a WebAssembly port of the powerful FFmpeg program, which enables video transcoding and compression right in the browser. Jonathan Harris and I did some extensive testing and prototyping with it until we were comfortable with the results.

    The initial prototype was followed by multiple rounds of UX reviews and massive changes to media uploads in the Web Stories editor. In fact, I basically rewrote most of the upload logic so we could better cater for all possible edge cases and ensure consistent user experience regardless of what kind of files users try to upload.

    The result is super smooth: just drop a video file into the editor and it will instantly get transcoded, compressed and ultimately uploaded to WordPress. Here’s a quick demo:

    Client-side video optimization in the Web Stories editor in action

    Technical Challenges

    FFmpeg Configuration

    A lot of our time fine-tuning the initial prototype was spent improving the FFmpeg configuration options. As you might know, there’s a ton of them and you can easily shoot yourself in the foot if you’re not familiar with them (which I personally wasn’t). We tried to find the sweet spot with the best tradeoff between video quality, encoding speed, and CPU consumption.

    The FFmpeg options we currently use:

    OptionDescription
    -vcodec libx264Use H.264 video codec.
    -vf scale='min(720,iw)':'min(1080,ih)':
    'force_original_aspect_ratio=decrease',
    pad='width=ceil(iw/2)*2:height=ceil(ih/2)*2'
    Scale down (never up) dimensions to enforce maximum video dimensions of 1080×720 as per the Web Stories recommendations, while avoiding stretching.

    Adds 1px pad to width/height if they’re not divisible by 2, to prevent FFmpeg from crashing due to odd numbers.
    -pix_fmt yuv420pSimpler color profile with best cross-player support
    -preset fastUse the fast encoding preset (i.e. a collection of options).

    In our testing, veryfast didn’t work with ffmpeg.wasm in the browser; there were constant crashes.
    FFmpeg configuration used in the Web Stories WordPress plugin

    Cross-Origin Isolation

    ffmpeg.wasm uses WebAssembly threads and thus requires SharedArrayBuffer support. For security reasons (remember Spectre?), Chrome and Firefox require so-called cross-origin isolation for SharedArrayBuffer to be available.

    To opt in to a cross-origin isolated state, one needs to send the following HTTP headers on the main document:

    Cross-Origin-Embedder-Policy: require-corp
    Cross-Origin-Opener-Policy: same-originCode language: HTTP (http)

    These headers instruct the browser to block loading of resources which haven’t opted into being loaded by cross-origin documents, and prevent cross-origin windows from directly interacting with your document. This also means those resources being loaded cross-origin require opt-ins.

    You can determine whether a web page is in a cross-origin isolated state by examining self.crossOriginIsolated.

    In addition to setting these headers, one also has to ensure that all external resources on the page are loaded with Cross Origin Resource Policy or Cross Origin Resource Sharing HTTP headers. This usually means having to use the crossorigin HTML attribute (e.g. <img src="***" crossorigin>) and ensuring the resource sends Access-Control-Allow-Origin: * headers.

    Now, if you have full control over your website, setting up cross-origin isolation is relatively easy. But the Web Stories editor runs on someone else’s WordPress site, with all sorts of plugins and server configurations at play, where we only control a small piece of it. Given these unknowns, it was not clear whether we could actually use cross-origin isolation in practice.

    Luckily, Jonny was able to implement cross-origin isolation in WordPress admin by output buffering the whole page and adding crossorigin attributes to all images, styles, scripts, and iframes if they were served from a different host.

    This won’t catch resources that are loaded later on using JavaScript, but that’s quite rare in our experience so far. And since we only do this on the editor screen and only when video optimization is enabled, there are less likely to be conflicts with other plugins.

    Other Use Cases

    Over time, we have expanded our usage of FFmpeg in the Web Stories editor beyond mere video optimization during upload. For example, users can now optimize existing videos as well and we also use it to quickly generate a poster image if the browser is unable to do so. But there are two other clever uses cases that I’d like to highlight:

    Converting Animated GIFs to Videos

    Did you know that the GIF image format is really bad? Animated GIFs can be massive in file size. Replacing them with actual videos is better in every way possible. So we tasked ourselves to do exactly this: convert animated GIFs to videos.

    Today, in Web Stories for WordPress, if you upload a GIF, we detect whether it’s animated and silently convert it to a much smaller MP4 video. To the creator and the users viewing the story, this is completely visible. It still behaves like a GIF, but it’s actually a video under the hood. Instead of dozens of MB in size, the video is only a few KB, helping a lot with performance.

    This feature was actually inspired by this issue my colleague Paul Bakaus filed for Gutenberg. It would be super cool to have this same feature in the block editor as well.

    Muting Videos

    Often times, creators upload videos to their stories that they want to use as a non-obtrusive background. For such cases, they’d like the video to be muted. But just adding the muted attribute on a <video> still sends the audio track over the wire, which is wasteful.

    For this reason, when muting a video in the story editor, we actually remove any audio tracks behind the scenes. It’s one of the fastest applications of FFmpeg in our code base because the video is otherwise left untouched. So it usually takes only a few seconds.

    What’s Next?

    I am really glad we were able to solve cumbersome real-world issues for our users in such a smooth way. Even though it’s quite robust already, we’re still working on refining it and expanding it to other parts of the plugin. For example, we want to give users an option to trim their videos directly in the browser.

    We can then use our learnings to bring this solution to other areas too. For example, it would be amazing to land this in Gutenberg so millions of WordPress users could take advantage of client-side video optimization. However, implementing it at this scale would be inherently more complex.

  • Web Stories in Swiss German

    Web Stories in Swiss German

    Grüezi!

    When I talk about my current projects in public, I usually do so in German or in English. Recently, I got the chance to speak about Web Stories in Swiss German as part of Chrome’s International Mother Language Day celebration series.

    If you want to learn about Web Stories and learn Swiss German, here’s the perfect video for you:

  • Web Stories Lightning Talk

    Given my involvement with Web Stories for WordPress, I’ve previously presented at AMP Fest 2020 and also participated in the Search Off The Record podcast. Now I had the honor of recording yet another video on Web Stories, this time for the Google Search Central Lightning Talk series.

    This video serves as a great introduction to the format for bloggers, site owners, or online marketers. It helps to better understand how to create stories and how to really make the most out of them.

    Not only does it cover story editors, but also tools and techniques to improve stories, monetization possibilities and and ways to measure your success.

  • I’m a Guest on the Search Off the Record Podcast

    I’m a Guest on the Search Off the Record Podcast

    I totally forgot to blog about this, but last month I had the honor of being a guest on Search Off the Record, a new podcast that takes listeners behind the scenes of Google Search and its inner workings. It was great chatting with the three hosts from the Search Relations team — John, Martin, and Gary — after months of not seeing them in person.

    Recording audio for the Search Off the Record podcast. Depicted is a microphone.
    My setup is definitely not as fancy as in this photo by Kate Oseen on Unsplash

    I really enjoyed talking to them about web stories and in particular the Web Stories for WordPress project, and what this all means for webmasters. Oh, and we also talked about sheep! But to know what these two topics have in common you’ll have to tune in yourself 😉

    Web stories is a new content format on the open web with its own unique features. With stories being featured prominently on Google, this of course has huge potential for webmasters. It’s something I want to highlight more in the future, beyond this Search Off the Record episode.

    In the meantime, don’t forget to subscribe to the podcast if you liked it:

    Also, there is a transcription of the whole episode available as well.