Author: Muhammad Muhsin

  • Taif 2025

    Taif 2025

    Wonderful memories.

  • Zoo Day

    Zoo Day

    Some photos from a visit to the Zoo.

  • Why I Still Bet on WooCommerce

    Why I Still Bet on WooCommerce

    Every few months, someone writes a post about why they moved off WooCommerce. Shopify is easier. Medusa is more modern. Saleor gives you a real GraphQL API.

    They are not wrong about any of those things.

    I keep coming back to WooCommerce anyway.

    Yes, I have built outside WooCommerce

    I have evaluated the alternatives. I have built headless commerce setups where WooCommerce was just a backend API. I have worked with REST endpoints that make WooCommerce’s look old.

    I am not betting on WooCommerce because it is all I know.

    I worked on WooCommerce internals

    I worked with the WooCommerce Core Extensions team at Fueled (for Automattic). The work involved diving into extensions like Composite Products and Product CSV Import Suite. Investigating bugs, writing documentation, and spending time understanding how these pieces fit together.

    I was not looking at WooCommerce from the outside. I saw the inconsistencies in CSV import behavior between extensions. I saw where integration points are maintained on one side but not the other. The codebase has rough spots.

    And I still bet on it.

    The ecosystem is the product

    WooCommerce is not just a plugin. It is a commerce ecosystem sitting on top of WordPress, which powers over 40% of the web. No standalone commerce platform can match that distribution.

    A merchant running WooCommerce can use the same WordPress skills, the same theme system, the same block editor, the same hosting. Their developer already knows the stack. Their content team already knows the admin. No second platform to learn.

    Shopify wins in many isolated comparisons. But Shopify is a walled garden. WooCommerce gives you the full code. You can fork it, extend it, host it anywhere, and own your data. For many businesses, that matters more than a nice onboarding flow.

    Extensibility is messy, but it compounds

    WooCommerce has thousands of hooks and filters. The extension ecosystem is massive. Yes, this leads to compatibility headaches. I have spent hours tracing why one extension breaks another.

    But that extensibility is also why WooCommerce can do things Shopify cannot do without building a custom app. Composite products, complex subscriptions, B2B pricing, multi-vendor marketplaces, custom checkout flows. I have seen it first-hand.

    Every extension you add builds on the same foundation. Every custom hook you write works with the same architecture. Over the years, that consistency adds up.

    What I got wrong about hosted commerce

    For a while, I thought hosted platforms like Shopify would replace self-hosted solutions for most merchants. No server management, no plugin conflicts, automatic updates. The simplicity argument is strong.

    What I underestimated was the cost of constraints. Merchants who start on Shopify eventually hit walls. Custom checkout logic that Shopify does not allow. Data export limitations. Theme restrictions. Monthly fees that scale with revenue rather than with complexity.

    WooCommerce’s self-hosted nature is a feature. You pay for hosting, not a percentage of your sales. You control the deployment. You decide when to update and what to customize.

    Gutenberg changes things for WooCommerce too

    I wrote about why I still bet on WordPress, and Gutenberg was a big part of that. The same logic applies here.

    WooCommerce is building block-based checkout, block-based product pages, and block-based cart experiences. Merchants can customize their store layout without touching code. Developers can build custom blocks that plug into WooCommerce data directly.

    The separation of structure from styling that Gutenberg brought to WordPress is now flowing into commerce. WooCommerce is inheriting the best architectural decisions WordPress has made in the last five years.

    Where WooCommerce falls short

    I am not going to pretend everything is great. The developer experience for building WooCommerce extensions is not clean enough. Documentation has gaps. Some core APIs feel dated compared to Medusa or Saleor.

    Performance out of the box is not where it needs to be for large catalogs. The admin is getting better with React-based pages, but it is still a work in progress. And the gap between WooCommerce core and its extension ecosystem creates friction merchants should not have to deal with.

    Real problems. But solvable ones.

    Why I keep coming back

    I care less about whether a platform is “modern” and more about whether it lets me ship things that work for real businesses.

    WooCommerce gives me ownership, extensibility, and the full power of WordPress underneath. I can choose when to go headless, when to build custom, and when to just install an extension and move on.

    The skills transfer to WordPress work and back. The ecosystem keeps growing. The architecture is improving. And with block-based commerce coming, the best parts of WooCommerce are still ahead.

    I do not bet on WooCommerce because it is perfect. I bet on it because it compounds.

  • Create an Image Slider using your WordPress Core Blocks

    Create an Image Slider using your WordPress Core Blocks

    This may be a bit of an unconventional take.

    With that out of the way, let’s get to creating a simple slider using the good old core/columns block.

    Let’s add a 3 / 3 / 3 columns block:

    • that has stack on mobile unchecked
    • with the inner column block having 100% width

    , and add three images from Sri Lanka’s natural beauty.

    Wrap it within a group (with class name .columns-image-slider, and wrap that in another group with a an inner width of, say 720px.

    Let us add a core HTML block, with the following content in it:

    <style>
    .columns-image-slider {
      position: relative;
      overflow: hidden;
    }
    .columns-image-slider .wp-block-columns {
      display: flex;
      flex-wrap: nowrap !important;
      gap: 0 !important;
      margin: 0;
      padding: 0;
      transition: transform 450ms ease;
      will-change: transform;
    }
    .columns-image-slider .wp-block-column {
      flex: 0 0 100%;
      width: 100%;
      max-width: 100%;
    }
    .columns-image-slider .wp-block-column > {
      padding: 0 8px;
     box-sizing: border-box;
    }
    .ts-slider-btn {
      position: absolute;
      top: 50%;
      transform: translateY(-%);
      z-index: 2;
      border: 1px solid currentColor;
      background: rgba(255,255,2559);
      color: inherit;
      padding: .5rem .75rem;
      border-radius: 999px;
      cursor: pointer;
    }
    
    .ts-slider-btn.prev{ left: 0; }
    .ts-slider-btn.next{ right: 0; }
    .ts-slider-dots{
      display: flex;
      gap: .5rem;
      justify-content: center;
      margin-top: .75rem;
    }
    .ts-slider-dot{
      width: 10px;
      height: 10px;
      border-radius: 999px;
      border: 1px solid currentColor;
      background: transparent;
      cursor: pointer;
      opacity: .7;
    }
    
    .ts-slider-dot[aria-current="true"]{
      background: currentColor;
      opacity: 1;
    }
    @media (prefers-reduced-motion: reduce){
      .columns-image-slider .wp-block-columns{ transition: none; }
    }
    </style>
    
    <script>
    (() => {
      const carousels = document.querySelectorAll('.columns-image-slider');
    
      carousels.forEach((root) => {
        const track = root.querySelector('.wp-block-columns');
        if (!track) return;
    
        const slides = Array.from(track.querySelectorAll('.wp-block-column'));
        if (slides.length <= 1) return;
    
        // Ensure initial state
        let index = 0;
    
        // Build controls
        const prev = document.createElement('button');
        prev.className = 'ts-slider-btn prev';
        prev.type = 'button';
        prev.setAttribute('aria-label', 'Previous testimonial');
        prev.textContent = '‹';
    
        const next = document.createElement('button');
        next.className = 'ts-slider-btn next';
        next.type = 'button';
        next.setAttribute('aria-label', 'Next testimonial');
        next.textContent = '›';
    
        root.appendChild(prev);
        root.appendChild(next);
    
        // Dots (insert after carousel so it sits below)
        const dotsWrap = document.createElement('div');
        dotsWrap.className = 'ts-slider-dots';
    
        const dots = slides.map((_, i) => {
          const dot = document.createElement('button');
          dot.className = 'ts-slider-dot';
          dot.type = 'button';
          dot.setAttribute('aria-label', `Go to testimonial ${i + 1}`);
          dot.addEventListener('click', () => goTo(i));
          dotsWrap.appendChild(dot);
          return dot;
        });
    
        root.insertAdjacentElement('afterend', dotsWrap);
    
        function update() {
          track.style.transform = `translateX(${-index * 100}%)`;
          dots.forEach((d, i) => d.setAttribute('aria-current', i === index ? 'true' : 'false'));
        }
    
        function goTo(i) {
          index = (i + slides.length) % slides.length;
          update();
        }
    
        prev.addEventListener('click', () => goTo(index - 1));
        next.addEventListener('click', () => goTo(index + 1));
    
        // Basic swipe support
        let startX = null;
        track.addEventListener('pointerdown', (e) => {
          startX = e.clientX;
          track.setPointerCapture?.(e.pointerId);
        });
    
        track.addEventListener('pointerup', (e) => {
          if (startX == null) return;
          const dx = e.clientX - startX;
          startX = null;
          if (Math.abs(dx) < 40) return;
          if (dx < 0) goTo(index + 1);
          else goTo(index - 1);
        });
    
        // Keyboard focus support
        root.setAttribute('tabindex', '0');
        root.addEventListener('keydown', (e) => {
          if (e.key === 'ArrowLeft') goTo(index - 1);
          if (e.key === 'ArrowRight') goTo(index + 1);
        });
    
        update();
      });
    })();
    </script>

    This is what you end up with:

    Perhaps this is not a lot, but can be a simple yet effective way to put up testimonials without requiring a whole new plugin (oh!) or come up with experiments on the same page like this.

    Thank you!

  • My 2026 Development Workflow: Claude Code Desktop + Cursor

    My 2026 Development Workflow: Claude Code Desktop + Cursor

    After months of experimentation, I’ve found my ideal AI coding setup: Claude Code Desktop for generation, Cursor for review. Here’s how it works.

    Anthropic recently added a native “Code” tab directly inside the Claude Desktop app. It is a GUI for the engine that powers the CLI. One notable feature is its support for isolated Git worktrees. This means you can have a brainstorming conversation in one tab while a “Code” session runs in another, making changes in a separate worktree that won’t touch your main working directory until you’re ready to merge.

    Why not git worktree? My workflow is simple enough that I just work directly in my main/feature branch and review everything in Cursor before committing. But if you’re juggling multiple experimental features or want that extra safety net, the worktree support is there.

    Why not the CLI? I know many developers swear by the Claude Code CLI, but I’ve found the Desktop app suits my workflow better. There’s something about having a dedicated window for my AI conversations that keeps things organized. I can easily reference previous discussions, and the interface feels more natural for the kind of back-and-forth dialogue I have when working through complex problems.

    The Workflow

    Here’s how my typical development session looks:

    1. Brainstorm with Claude Desktop

    Before writing any code, I start by talking through the problem with Claude. I describe what I’m trying to accomplish, share relevant context about my project, and bounce ideas back and forth. This conversation helps me clarify my thinking and often surfaces edge cases I hadn’t considered. It’s like having a patient colleague who’s always available to rubber duck with.

    I then ask Claude to generate a prompt for Claude Code based on this discussion.

    2. Generate Code with Claude Code

    I open Claude Code in the Desktop app and paste in the prompt generated from step 1. It would have context about my project structure, the technologies I’m using, and any constraints I’m working with. Claude generates code, explains its reasoning, and I can ask follow-up questions right there in the conversation.

    3. Review in Cursor

    Once Claude has generated the code, I switch to Cursor. This is where I put on my reviewer hat. I don’t just blindly accept what the AI produces—I read through it carefully, understand what it’s doing, and verify it aligns with my project’s patterns and standards.

    Cursor’s diff view makes this review process smooth. I can see exactly what’s being added or changed, accept individual hunks, or modify the suggestions before committing.

    4. Test, Accept, and Commit

    After reviewing and testing, I accept the changes I’m happy with and commit them with meaningful commit messages.

    Why This Combination Works (for me)

    The separation of concerns is what makes this workflow powerful:

    • Claude Desktop for Brainstorming — This is where ideas take shape. I describe the problem I’m solving, share context about my project architecture, and have a back-and-forth conversation to explore different approaches. Claude helps me think through edge cases, consider alternative implementations, and refine my requirements before writing any code. By the end of this phase, I have a clear mental model and a well-crafted prompt ready for code generation.
    • Claude Code Desktop for Generation — With the refined prompt from brainstorming, I switch to Claude Code which has direct access to my codebase. It understands my project structure, existing patterns, and dependencies. The code it generates is contextually aware—it follows my naming conventions, integrates with existing modules, and respects the architectural decisions already in place. I can iterate here too, asking for adjustments or alternative approaches.
    • Cursor for Review — This is my quality gate. I examine every diff carefully, understanding not just what changed but why. Cursor’s interface makes it easy to accept good changes, reject problematic ones, and make surgical edits where needed. This deliberate review process ensures I never ship code I don’t understand. It’s also a learning opportunity—I often discover new patterns or techniques by studying what the AI produced.

    This two-step process forces me to slow down and actually review what the AI produces. It’s easy to fall into the trap of accepting AI-generated code without understanding it. By deliberately switching tools for the review phase, I create a mental checkpoint that keeps me engaged with the code.

    Tips for This Workflow

    1. Be specific with Claude — The better your prompts, the less cleanup you’ll need in Cursor
    2. Review as much as possible — Don’t let the convenience of AI make you lazy about code review
    3. Commit incrementally — Small, atomic commits make it easier to track what the AI contributed
    4. Keep learning — Use the review phase as an opportunity to understand patterns you might not have written yourself

    Final Thoughts

    AI coding assistants are powerful tools, but they work best when you stay in the driver’s seat. My Claude Code Desktop + Cursor workflow keeps me productive while ensuring I remain the decision-maker for every line of code that ships.

    If you’ve been looking for a way to integrate AI into your development process without losing control, give this approach a try. The key is finding the right balance between leveraging AI’s capabilities and maintaining your own understanding of the codebase.

    Thanks for reading. Let me know whether you agree/disagree or have a different take.

  • Once You Go Mac — Should You Go Back?

    Once You Go Mac — Should You Go Back?

    One of my first bosses, on the day I joined the company asked “Why are we using Windows?” as they saw me struggle with setting up a project using Docker on my Windows laptop. The following day I got a MacBook Pro. I thought I would not look back.

    And look back I did. And here I am writing a blog post on a Windows desktop 7 years later.

    As the walled garden kept getting richer, the walls started climbing higher. That is not the issue really. The cost of staying within those walls increased as well.

    By December 2025, I caved in and got a Nothing Phone. Boy do I love a nice clean Android phone.

    Shortly after, I realized what started with an innocent Mac purchase had sucked me in deep in to the ecosystem. Time to get in control, I decided.

    That was when I realized I suddenly wanted a PC. In this economy, you may ask. Well, several friends advised that it is better to get it TODAY than to wait another day. So, by the last week of December, I went and purchased a PC with 32GB memory and a GeForce 5060 TI Graphics with 8GB video memory. Plan is to try and run some local LLMs and hoping it speeds up development work.

    Now, Should You Go Back?

    I am still seeing some issues around npm modules installing on Windows. Either that or I need to figure how to do it the Windows™ way.

    At this point in time, I am giving myself a few months – iPhone on left pocket and Android on the right. MacBook on the desk and a Windows tower on the ground. Hoping to go like this and see where it takes me.

    Here are my past writing on this topic:

    Let me know your thoughts on the whole ecosystem war!

  • A Decade with WordPress: What I’ve Learned (and Built) Along the Way

    A Decade with WordPress: What I’ve Learned (and Built) Along the Way

    I logged in to this site today, and was pleasantly surprised to be welcomed by this notification.

    I figured today, is as good as any day to reflect on my time using WordPress and what I had been doing with it.

    2015: The Beginning

    Created a blog at mtwoblog.wordpress.com thereby creating a free account on WordPress.com. Wrote about my experiments with Arduino. Little did I know my nephews would pick up that hobby almost a decade later.

    2016: It Gets Real

    Moved from a free hosting to a self-hosted site with a custom domain. Hosting was provided by my friend who now runs GuardKite. Learned about custom plugins and themes as well as about the WP repository.

    Started frequenting the WordPress Colombo Meetup group. Met some amazing folks who I am in touch with until today.

    Also, founded a WordPress agency based in Colombo.

    2017: Community First

    Helped organize the first WordCamp in my country.

    Explored the WordPress REST API and GraphQL; two technologies that would define a large part of my career.

    2018: Going Global

    Wrote 3 articles for Smashing Magazine – a leading web publications for those working on the web.

    Spoke at my first large online conference; another aspect that would go on to define my life and career.

    2019: The Big League

    Joined rtCamp – a top WordPress company in the world as a React Dev working remotely.

    Represented at WordCamp US – my first flagship event – albeit remotely, as I did not get to physically be there.

    2020: Adapting

    COVID happened. I continued working remotely and writing on various topics, such as lessons learned which were not strictly about work or tech. This category of my writing resonated a lot with you all and I love to write about it too.

    2021: Going Headless

    Worked on a production headless WordPress project with GraphQL and Gutenberg. Continued speaking at a few events including the first WordCamp India.

    It’s been an incredible decade. What started as a simple Arduino blog on WordPress.com has evolved into a career, a community, and a way of life.

    2022: Deepening Roots

    Continued deepening my expertise in headless WordPress and the block editor. The ecosystem was maturing rapidly, and I found myself at the intersection of traditional WordPress and modern JavaScript frameworks.

    2023: Full Site Everything

    Expanded my speaking engagements and continued contributing to the WordPress community. The rise of Full Site Editing brought new opportunities to explore and share knowledge about the evolving platform.

    2024: Building Forward

    A year of consolidation and growth. WordPress continued to evolve, and so did my role within the ecosystem. The community remained as vibrant as ever, with new faces joining and veterans continuing to contribute.

    2025: A Decade In

    And here we are – 10 years since I first created that Arduino blog. WordPress has been more than just a platform; it’s been a gateway to incredible opportunities, lasting friendships, and continuous learning.

    Looking Back, Moving Forward

    Ten years ago, I couldn’t have imagined where this journey would take me. From experimenting with Arduino projects to writing for Smashing Magazine, from attending local meetups to speaking at international conferences, from running a small agency to working with one of the top WordPress companies in the world – every step has been shaped by this remarkable open-source community.

    WordPress taught me that technology is ultimately about people. The code we write, the sites we build, the content we create – it all comes back to connecting with others and making their lives a little bit better.

    Here’s to the next 10 years. 🎉

    Conclusion

  • Why I Still Bet on WordPress

    Why I Still Bet on WordPress

    Every few months, someone declares that WordPress is dead.

    I keep building outside it — and still coming back.

    Not because I’m stuck.

    Not because I don’t know better tools.

    But because after a decade of shipping real products, WordPress continues to win in places most comparisons ignore.

    Yes, I’ve built outside WordPress

    I’ve pulled Gutenberg out of WordPress entirely to build editor experiences. I’ve shipped headless setups where WordPress is nothing more than a content API. I’ve built React applications that never touch PHP. I’ve worked on projects where WordPress would have been the wrong choice — and I knew it.

    I’m not betting on WordPress because it’s all I know.

    What I got wrong about headless & decoupled frontends

    For a long time, I believed headless and fully decoupled frontends were the inevitable future — and not without reason. I chased cleaner architectures, React-first workflows, and the promise of total flexibility. In many cases, they delivered exactly that.

    What I got wrong was treating this approach as a default instead of a tool.

    The hidden cost wasn’t performance or technical complexity — it was friction. Previews broke. Editorial workflows slowed down. Simple changes required deploys. Teams gradually lost autonomy in ways that were easy to ignore at first.

    Headless isn’t the problem; uncritical adoption is. Over time, I learned that most projects don’t need maximum flexibility. They need reliability, ownership, and a stack that doesn’t fight back.

    That realization is a big part of why I still bet on WordPress today.

    What Gutenberg quietly got right

    Gutenberg’s value isn’t the editor — it’s the architecture underneath.

    Blocks are portable UI contracts. A block defines its structure, its attributes, its allowed variations. That definition travels with the content, not the theme. Switch themes, the content survives. Query blocks programmatically, the structure is already there. This is what years of shortcode chaos never gave us.

    Then there’s the shift toward declarative configuration. block.json and theme.json moved WordPress away from scattered PHP hooks toward predictable, tooling-friendly JSON. You can lint it, version it, generate it. It’s not perfect, but it’s a direction that finally aligns with how modern development works.

    The separation of structure from styling matters more than people realize. Classic WordPress tied content to presentation so tightly that switching themes meant rebuilding pages. Blocks broke that coupling. Structure lives in the content layer; styling lives in the theme. Long-term projects benefit from this in ways that aren’t obvious until year two or three.

    Blocks also scale better than their reputation suggests. Patterns, variations, and InnerBlocks let you build component systems without page builder bloat. Agencies are shipping block libraries now — not plugins full of shortcodes, but composable pieces with real contracts.

    Gutenberg is misunderstood by both its critics and its fans. Critics see it as a slow page builder chasing Elementor. Fans oversell it as a design tool for non-developers. Both miss the point. The real value is that WordPress finally has a content model that isn’t just HTML in a database column. That’s a foundation you can build on for years.

    Why I still bet on WordPress

    At this point in my career, I care less about architectural purity and more about leverage.

    WordPress gives me distribution, mature editorial workflows, and a content model that has finally caught up with modern development. It lets me choose when to decouple, when to go headless, and when to keep things boring — without forcing that decision upfront.

    Most platforms push you toward extremes. WordPress lets you stay in the middle longer, and that’s where most real projects live.

    I don’t bet on WordPress because it’s perfect. I bet on it because it compounds. The skills last. The ecosystem lasts. The content lasts. And with Gutenberg, the architecture finally does too.

  • DIY Breadcrumbs in WordPress with Full Site Editing

    DIY Breadcrumbs in WordPress with Full Site Editing

    Let’s look at how you can use Gutenberg and Full Site Editing to create Breadcrumbs for your WordPress site.

    You would need to install the latest version of WordPress and a plugin that has FSE (Full Site Editing) support.

    You would also require the The Icon Block by Nick Diego.

    Here’s the code. You can copy it and paste it in your Single Post template.

    Once copied and saved, the frontend of the template would look something like this:

    Enjoy!