Tag: vcpahumane-pet-companion

  • Building the pet companion plugin: server-rendered blocks, no build step


    I’ve been doing development work for the Venango County Humane Society since early 2025. Most of what I’ve built lives behind the scenes — a Petstablished sync plugin, some custom post types, a faceted pet browser. But earlier this month I shipped something different: a small companion plugin that lets any WordPress site embed adoptable pets from the shelter using Gutenberg blocks.

    If you’re a partner site looking to install it, I wrote a separate post for that. This one is for WordPress developers — what’s in the plugin, what the interesting decisions were, and what’s still open. The repo is at github.com/jwincek/vcpahumane-pet-companion.

    The shape

    Two blocks, no build step, no JavaScript bundler, no node_modules. PHP renders everything; JavaScript exists only in the editor (edit.js) for the sidebar controls. Both blocks talk to the source site’s standard /wp-json/wp/v2/pet endpoint with a transient cache layer in front.

    The plugin is intentionally small — a few hundred lines of PHP, a few hundred of plain JavaScript, no dependencies. I want partner sites with limited technical resources to be able to read the entire plugin in an afternoon and understand it.

    Decision 1: two blocks instead of three

    The first version had three blocks: Pet Listing Grid, Pet Card, and Featured Pets. The grid and the featured-pets blocks did almost the same thing — both rendered a card grid, differing mainly in whether the order was randomized and whether the count was big or small. After looking at them side by side for a day, I deleted the listing grid.

    The reasoning: if the partner site wants a “browse all” experience, the right answer is to link to the shelter’s own adoption catalog at
    vcpahumane.org/adopt/pets, not to rebuild that catalog locally. The shelter already has filters, pagination, and a real adoption flow over there. The companion plugin’s job is to be a friendly handoff to the shelter, not to duplicate the destination.

    That left two blocks with genuinely different jobs:

    • Featured Pets — a small rotating selection. Sidebar surface. Browse-y.
    • Pet Card — a single specific pet, embedded inside a blog post or fundraiser page. Connection-y.

    Once I framed them as “browse” and “feature,” the design choices for each became obvious in different directions.

    Decision 2: Pet Card is a profile, not a card

    The original Pet Card block looked like the cards in the Featured Pets grid — just bigger. That felt wrong as soon as I had both on a page together. A visitor encountering Featured Pets is browsing; they want a thumbnail to click. A visitor encountering Pet Card is reading a blog post about a specific animal; they’re already interested. Optimizing both for click-through made the second one feel cheap.

    So I rewrote Pet Card as a magazine-style profile: big image on the left (or top, on mobile and via a sidebar toggle), the shelter’s full writeup — not a 22-word excerpt — and a real pill-shaped CTA button that says “Start the adoption process” instead of a subtle “Meet me →” link. Same data, different surface, different intent.

    The two blocks now share almost no markup. Featured Pets uses a shared vcpa_pet_companion_render_card() helper for visual consistency across its cards; Pet Card has its own render function with its own CSS class prefix (.vcpa-pet-profile). They diverged cleanly.

    Decision 3: the adopted-state celebration

    This is the part I’m most fond of. The source plugin removes pets from the shelter’s site when they’re adopted (because they’re no longer adoptable). For Featured Pets that’s fine — the grid just shows whoever’s currently available. But for Pet Card, where a partner site has specifically embedded this one pet in a blog post, it’s a problem: the post breaks. The plugin fetches an empty result and the block disappears or shows an error.

    The fix is small but feels much bigger. When an editor picks a pet in the sidebar, the picker’s “save” handler doesn’t just store the pet’s ID — it also stores the pet’s name, image URL, and alt text as block attributes:

    function pickPet( pet ) {
        setAttrs( {
        petId: pet.id,
        petSlug: pet.slug,
        petName: pet.name,
        petImage: pet.image,
        petAlt: pet.alt
        } );
    }

    Block attributes are persisted in the post content, not in the database separately. They survive forever. So even after the source site forgets about Bagel, the block itself still knows Bagel’s name and what they looked like.

    When the live REST query comes back empty AND there’s a cached name in the attributes, the render falls into a celebration branch instead of the empty branch:

    if ( ! empty( $result['pets'] ) ) {
        // Normal profile render.
        return;
    }
    
    if ( $cached_name !== '' ) {
        // The pet existed at pick time but is gone now.
        // They were almost certainly adopted. Celebrate.
        printf(
            '<h2 class="vcpa-pet-profile__name">%s</h2>',
            esc_html( sprintf( __( '%s found a home!' ), $cached_name ) )
        );
        return;
    }
    

    The block transforms from “Bagel needs a home” into “Bagel found a home!” without anyone touching the post. A blog post written a month ago about a pet who needed adoption becomes, automatically, a blog post about a pet who found one.

    There’s no webhook, no deletion event, no inverse-sync. Just block attributes doing what they’re already designed to do: remembering things.

    Decision 4: no build step

    I made this call partly out of laziness and partly out of conviction.

    Laziness first: a build step means package.json, node_modules, @wordpress/scripts, a webpack config, watching for changes during development, remembering to commit the built artifacts (or not, and adding a CI step), making sure the build environment matches between dev and prod. For two blocks with five UI controls each, that’s a lot of yak-shaving.

    Conviction second: I wanted partner sites — small nonprofits, vet clinics, businesses — to be able to read the entire plugin without needing to understand a build pipeline. If someone wants to fix a typo in a label or adjust a spacing value, they should be able to open the file in any editor and have the change take effect on save.

    So edit.js is hand-written using wp.* globals:

    ( function ( wp ) {
        var el = wp.element.createElement;
        var registerBlockType = wp.blocks.registerBlockType;
        var InspectorControls = wp.blockEditor.InspectorControls;
        // ...
        registerBlockType( 'vcpa-pet-companion/featured-pets', {
            edit: function ( props ) {
                return el( 'div', useBlockProps(),
                    el( InspectorControls, {}, /* ... */ ),
                    el( ServerSideRender, {
                        block: 'vcpa-pet-companion/featured-pets',
                        attributes: props.attributes
                    } )
                );
            },
            save: function () { return null; }
        } );
    } )( window.wp );
    

    No JSX. No imports. Verbose wp.element.createElement calls instead of JSX angle brackets. It is genuinely uglier than the JSX version. But it runs straight from a .js file with no transformation, and that property is worth more to me than the prettier syntax.

    The gotcha that took an hour to find

    There is one nasty thing about the no-build-step approach that I want to warn other developers about, because it isn’t well-documented anywhere.

    When you point editorScript at file:./edit.js in block.json, WordPress looks for a sibling file called edit.asset.php to find out what JavaScript dependencies the script needs. That asset file is normally generated by @wordpress/scripts at build time and contains something like:

    <?php return [
        'dependencies' => [ 'wp-blocks', 'wp-element', /* ... */ ],
        'version'      => '0.2.0',
    ];
    

    If you don’t have a build step, you don’t have an asset file. WordPress reacts to this by enqueuing your script with zero dependencies. Your script will run, but window.wp is not guaranteed to be defined when it does — the load order is essentially undefined. On some sites it works because another plugin has already loaded wp-blocks; on other sites it throws Cannot read properties of undefined (reading 'element') and your blocks never register.

    The fix is to hand-write edit.asset.php next to your edit.js:

    <?php return [
        'dependencies' => [
            'wp-blocks',
            'wp-element',
            'wp-block-editor',
            'wp-components',
            'wp-server-side-render',
            'wp-i18n',
        ],
        'version' => '0.2.2',
    ];
    

    That’s it. Two-line PHP file per block, declaring the wp-* packages your script touches. WordPress reads it, enqueues those scripts as dependencies, and load order works correctly.

    I shipped 0.2.0 without these files because it worked locally. The first production install reported “blocks aren’t showing up” within an hour of the push. 0.2.1 added the asset files. If you’re going down the no-build-step road, don’t repeat my mistake.

    Decision 5: two-layer caching with split negative TTLs

    The source plugin syncs with Petstablished — the shelter’s adoption software — once a day. So the upstream data only changes once every 24 hours, no matter how often the companion plugin asks. That means the companion’s cache TTL can be generous: I set it to 6 hours by default, which gives a same-day window for adopted-pet celebrations to appear without making partner sites pay for unnecessary refetches.

    The more interesting part is the negative cache. When a fetch fails, we store a sentinel [ '__error' => $message ] value in the same transient slot, so the next request fails fast instead of paying another timeout. Without negative caching, if vcpahumane.org goes down, every visitor on every partner site pays a full 8-second timeout until the source recovers. With it, only one visitor per minute does.

    But the first version of the negative cache had a bug. It treated all failures the same way and cached them for 60 seconds. That’s appropriate for real failures (HTTP 500, malformed JSON) — those mean the source is genuinely broken and we should back off. But it’s wrong for network-layer failures, especially in the editor.

    Here’s the symptom: change a Featured Pets block’s count slider in the editor. The new value triggers a new REST request. That request hits a transient loopback timeout (Local by Flywheel’s PHP-FPM worker pool sometimes wedges on self-referential REST calls). The negative cache stores the error for 60 seconds. You change the slider again — now everything fails for a full minute, even though the source is fine, because the editor is hitting the poisoned cache slot.

    The fix is to split the negative TTL by error category:

    // Soft failure: cURL timeout, DNS hiccup, connection refused.
    // These usually resolve in seconds. Cache briefly.
    private const NEGATIVE_TTL_SOFT = 5;
    
    // Hard failure: HTTP 4xx/5xx, malformed JSON.
    // Source is genuinely broken. Cache for a minute.
    private const NEGATIVE_TTL_HARD = 60;
    

    Five seconds is long enough to absorb a hot-loop retry storm, short enough that the next user interaction succeeds. Sixty seconds for real failures still protects against an outage.

    Block style variants vs. core block supports

    WordPress 6.x gives you two complementary mechanisms for letting editors customize blocks: block supports (color, spacing, border, typography) and block styles (named visual variants registered via register_block_style()). I use both, but for different things.

    Featured Pets gets three named styles — Card, Minimal, Overlay — because those are structural differences. Card has a border and a white background; Minimal removes both; Overlay positions the title over the image with a gradient. These aren’t sliders; they’re discrete looks that an editor picks from a thumbnail strip.

    Both blocks also use core supports for color, spacing, and border so that partner sites can theme the output to match their brand. A vet clinic with a teal theme can give the cards teal borders and a teal CTA without writing any CSS.

    Pet Card doesn’t get block style variants. The reason is that “Minimal” or “Overlay” don’t really make sense for a profile — what would they be? Instead it gets a layout toggle (image left vs. image top) and a “show full story” toggle. Both are real choices an editor might want to make; neither needs to live in a styles picker.

    The general principle: block styles for genuinely discrete visual variants, sidebar controls for everything else. If you find yourself making block styles that differ only in one CSS property, those should probably be a sidebar toggle instead.

    What’s still open

    Things I deliberately didn’t build, in case anyone wants to send a PR:

    1. Taxonomy filter is OR, not AND. The wp/v2 REST API accepts comma-separated term IDs as OR filters. To do AND (“good with kids AND good with cats”) would require either a custom REST endpoint with tax_query.relation = AND or client-side filtering after a wider fetch. Not impossible; not done.
    2. No pagination on Featured Pets. The block is meant to be small (1–12 pets), so pagination feels overkill. But if someone wanted to use Featured Pets as a “browse all” surface despite my earlier reasoning, they’d need it.
    3. No gallery support. Petstablished returns multiple images per pet, but the source plugin currently only stores the first as a featured image. To surface a gallery in Pet Card, the source plugin would need to also cache the additional URLs. That’s a cross-plugin change I haven’t tackled.
    4. No editor preview JS-side fetching. Both blocks render their editor preview via ServerSideRender, which means every attribute change triggers a server roundtrip that itself makes a remote HTTP call. On slow hosts this feels sluggish. The fix is to fetch from the editor directly (using apiFetch against the source’s REST API with CORS) and render locally — but that’s significantly more code, and the current approach is good enough for the data sizes involved.
    5. No tests. The plugin is small enough that the cost of a test harness exceeds the cost of breaking changes I’d catch. If it grows, that calculus flips.

    If any of these sound interesting, the repo welcomes issues and pull requests at any size — even typo fixes and CSS tweaks. It’s a small project for a small shelter, and I’d rather have a plugin that improves in small steps than one that’s untouched because the bar to contribute feels high.

    Why this exists at all

    The companion plugin is the second smaller plugin I’ve built around the same core piece of work — a Petstablished sync plugin that lives on vcpahumane.org and handles the actual data ingestion, custom post type registration, faceted browsing, and adoption flow integration. That one is the more architecturally interesting of the two, and I’ll write about it in a future post.

    But the companion plugin is the part that spreads. The sync plugin serves one site; the companion plugin can serve any number of partner sites, each one becoming another place where adoptable pets show up when someone is looking. That’s the part that scales. And the part that scales is the part most worth doing carefully.

  • Help Venango County’s adoptable pets find homes from your own website


    The Venango County Humane Society is a small no-kill shelter in northwestern Pennsylvania with room for fewer than sixty animals at a time. Most of the dogs and cats who come through find homes with families nearby, but every so often someone drives a long way to meet a pet they first saw online — which is the whole reason this post exists.

    Every adoption starts the same way: someone, somewhere, sees a pet and stops scrolling. The more places those pets show up, the more chances they have to be seen.

    So I built a small WordPress plugin that lets any WordPress site display adoptable pets from VCHS. If you run a website — a local business, another nonprofit, a vet clinic, a personal blog — you can drop a block onto a page and visitors will see real pets currently looking for homes. Click any of them and they’ll be sent straight to the shelter’s adoption page to start the process.

    Here’s what it looks like in action. These pets are live right now:

    That’s not a screenshot. Those are actual animals at the shelter, fetched from vcpahumane.org as you loaded this page. When one of them finds a home, they’ll quietly disappear from the grid and someone new will take their place.

    What this is

    The plugin is called VCPA Humane Pet Companion and it ships two Gutenberg blocks that you can drop into any post or page. The shelter handles all the data — keeping pet listings up to date, syncing with their adoption software, writing the descriptions — and your site just displays whatever’s currently available. There’s nothing for you to maintain.

    Both blocks pull live data, so you always show animals who are still looking. No stale listings, no manual updating, no risk of featuring a pet who’s already been adopted.

    Block 1: Featured Pets

    This is the block above. It’s meant for sidebars, homepages, footer sections — anywhere you want a small rotating selection of adoptable pets to share space with the rest of your site. You can choose how many pets to show, how many columns, and whether to randomize the order. You can also filter by animal type (dogs, cats) or by traits like “good with kids” or “special needs,” so a
    cat-focused business can show only cats and a senior-pet advocate can highlight the older animals who are easiest to overlook.

    It’s the browse surface. Visitors who like what they see click through to vcpahumane.org and continue from there.

    Block 2: Pet Card

    The second block is for when you want to feature one specific pet — maybe you’re writing a blog post about a fundraiser, or a particular animal has captured your attention, or you want to give a long-stay resident a moment of extra visibility. The Pet Card block embeds a single pet as a profile, with the shelter’s full writeup and a clear button to start the adoption process.

    Joker

    Joker

    Meet Joker! This sweet boy is looking for a forever home to call his own. Joker has FeLV, or Feline Leukemia, and would need to be the only cat in the home or in a home with other FeLV+ cats. This is not contagious to humans or other species of animals. This does, however, mean his health would need to be monitored closely in cooperation with your veterinarian in order to keep on top of anything that may arise. He is very friendly, playful, and would be a wonderful companion for a family looking to open their home to a new member. If you think Joker is the right fit for your family, apply today! DOB: 8/9/2019

    Start the adoption process

    Here’s the part I’m most fond of: when a featured pet finds a home and is removed from the shelter’s listings, the block doesn’t break. It quietly transforms into a celebration card — “[Name] found a home!” — using the pet’s name and photo, which the block remembers from when you first picked them. You don’t have to update anything. A blog post you wrote a month ago about a pet who needed a home becomes, automatically, a blog post about a
    pet who found one.

    That feels like the right way to handle adoption: not as data getting deleted, but as good news.

    How to install

    Three steps, no code:

    1. Download the plugin from github.com/jwincek/vcpahumane-pet-companion (use Code → Download ZIP).
    2. Upload it to your WordPress site through Plugins → Add New → Upload Plugin.
    3. Activate it. That’s it. The blocks will appear in the editor under
      “VCPA Featured Pets” and “VCPA Pet Card.”

    There’s nothing to configure. The plugin is preset to talk to vcpahumane.org right out of the box.

    What it costs

    Nothing. No fees, no API keys, no signup, no tracking. The plugin is GPL licensed and the source is public. The shelter is a small nonprofit and this is a small contribution to making their pets more findable.

    If you have a website and even a corner of it that could host a featured pets block, please consider it. Every additional place a pet shows up is another chance for someone to stop scrolling.


    For developers: the plugin is intentionally small and built without a
    build step. If you’d like to read about how it’s put together — including the design decisions, the gotchas, and the things still left to do — I wrote a longer post about it: Building the pet companion plugin →.

    If you’d like to help, the code is on GitHub and contributions of any
    size are welcome.


    Photos and pet data courtesy of the Venango County Humane Society.