Iceland Review: Stopping the Bot Attack Behind Weeks of 502 Errors

Iceland Review is one of Iceland’s best-known English-language publications, with daily news, culture and travel stories. I’ve looked after the site since rebuilding their editorial platform, so when readers started regularly getting error pages instead of articles, it landed on my desk.

The hosting company’s explanation was that traffic spikes were exceeding the server’s limits. They switched the PHP mode, sent a list of the busiest URLs and user agents, and suggested their premium CDN. Meanwhile their automatic scaling added 7 GB of extra RAM in two days, billed for a month.

Their list pointed at the RSS feed and at Chrome browsers. Both turned out to be red herrings. The feed was already served from cache, and the “Chrome browsers” were scripts pretending to be Chrome.

Reading the logs instead of the summary

I downloaded a full day of raw server logs and analyzed them myself, request by request. Three things stood out:

  • Old URLs were eating the server. The site had once had German and other language versions, plus an older website before that. Bots were requesting those long-gone addresses, especially an old German news section, with random parameters attached so that every request missed the cache and forced WordPress to build a full page. These requests alone used about 45% of all server time, at around 11 seconds each.
  • One scraper used more than 23,000 IP addresses in a day. It sent over 25,000 page requests, roughly one per IP, all disguised as the same Chrome browser on a Mac. That’s why blocking IPs was hopeless. The tell: only 1% of those IPs ever loaded a stylesheet, script or image. A real browser does that on every page.
  • The existing protection wasn’t switched on. The domain was in Cloudflare, with “Under Attack Mode” enabled, but the DNS records were set to “DNS only”, so all traffic bypassed Cloudflare entirely. The caching plugin, NitroPack, had been left in test mode, so visitors weren’t getting optimized pages either.

Google was crawling the same junk: about 361,000 of these old URLs sat in Search Console as “Crawled, not indexed”. In Google Analytics, “Page not found” had become the #1 most-viewed page on the site, at 11.5% of all views, almost entirely bots.

Layer 1: answer old URLs before WordPress wakes up

The site is English-only now, so none of the old addresses needed to work. I added rules to the server configuration that answer every old German, multi-language and old-site URL with “410 Gone” directly from the web server. WordPress never loads, so each request costs a fraction of a millisecond instead of 11 seconds, and Google is told plainly that the pages are permanently removed. The one old link that still mattered, the German subscription page, redirects to the English one.

Layer 2: a custom security plugin, “Traffic Guard”

I built a small WordPress plugin to manage the defenses from the WordPress admin:

  • Bot blocking written into the server configuration, so known scrapers are refused before PHP starts. A built-in allow-list means Google, Bing, Apple and social-media crawlers can never be blocked by mistake.
  • A 24-hour diagnostics report that samples which visitors and which background requests actually use the server. This is how the later bots in this story were caught early.
  • Feed caching and lighter background requests, to trim the remaining load.
  • “Cloudflare-only” mode, which refuses any request that didn’t come through Cloudflare, closing the side door for bots that try to skip it. A built-in safeguard means it can’t lock the site out.

Layer 3: Cloudflare, finally in front

With the site switched to Cloudflare’s proxy, attackers are now stopped at Cloudflare’s edge, before they reach the server:

  • A repaired blocking rule. The account already had a “Block Bad Bots” rule, but it was disabled and written so it could never match anything. Rewritten, it has blocked over 12,000 requests.
  • An automatic challenge for browsers that don’t add up. Every scraper in this attack disguised itself as a browser, and the disguises shared the same tell-tale inconsistencies. Instead of chasing each new disguise, one rule now challenges that whole category. Real people pass without noticing. Bots can’t, and only 0.06% of the 38,000 challenged requests got through.
  • Settings that would have broken things. “Under Attack Mode” would have shown every reader a waiting screen, “Bot Fight Mode” would have blocked the caching service and RSS readers, and “Hotlink Protection” was blocking the site’s images in Google Images and news readers. Each was adjusted and tested.

Before switching, I checked SSL on both hostnames and redirects for loops, and tested logins, feeds, ads, the caching service and real visitor IP addresses. A 30-minute monitoring run afterwards showed no errors.

The performance side

Stopping the bots was half the job. The rest made the legitimate traffic cheaper:

  • NitroPack taken live and set to work alongside the host’s cache, with article images that load straight away instead of waiting for the reader to scroll.
  • Scheduled tasks moved from visitors’ page loads to a real server cron job.
  • robots.txt corrected. A Google-specific section had been overriding every crawl rule, so Googlebot was crawling query-string and pagination URLs nobody needed. It now follows the same rules as other crawlers, while keeping access to the CSS and JavaScript Google needs to render pages.
  • Ad slots reworked at the publisher’s request: static slots that rotate advertisers by percentage, chosen in the visitor’s browser so page caching doesn’t freeze one ad in place, with no extra server load.

Monitoring caught what testing couldn’t

The first fixes were only the start, and the monitoring paid for itself:

  • A crawler found a way around Cloudflare. It’s blocked on the server too now, and it’s what prompted the Cloudflare-only mode.
  • The same operator kept changing disguise, which led to the category-wide challenge rule instead of a new block for every new name.
  • The site’s view counter had been failing quietly. Two settings in the theme’s view counter were incompatible, so every count crashed. That was only visible in the server logs, and it’s fixed now.

Results

Comparing a full day of server logs from the attack (October 6) with a full day a week later (October 12):

Metric Attack day A week later
Server work for the day 318 worker-hours 48 worker-hours (−85%)
Average uncached request 5.1 s 0.69 s
Requests slower than 5 s 11,117 145
Busiest 10 minutes ~116 requests at once ~6
Gateway timeouts (504) 5,306 0
Requests handled 232,900 259,100

The site handled more requests with 85% less work. Cached pages now leave the server almost instantly, and “Page not found” has dropped out of Google Analytics’ top pages entirely, so the reports show real readers again.

What I’d tell any publisher

  1. Read the raw logs. Hosting dashboards summarize, and summaries hide attacks. The answer here was in the details: which URLs, how long each took, and whether the “browser” ever loaded an image.
  2. Old URLs are an attack surface. Removed language versions and old site sections don’t disappear. Bots and search engines keep asking for them. Answer them cheaply, at the server, before your CMS wakes up.
  3. Check that protection is actually on. Cloudflare in DNS-only mode, a caching plugin in test mode, a security rule that can’t match: all three looked like protection and weren’t.
  4. Block categories, not signatures. Attackers rename themselves every few days. Rules that target behavior, such as browsers whose signature doesn’t add up and requests that skip the proxy, keep working when the names change.

New Life Spaces: Nature-Derived Color Palettes

New Life Spaces is a design-build remodeling company in Upstate New York, and biophilic design sits at the center of how they work. The owner wanted to show visitors that biophilic design is much more than putting plants inside a house: nature can inform color, texture, material and light, and the relationships between them. He had seen short social videos that pull a few colors out of a photo of a bird or a flower, and asked whether the Biophilic Design page could do something similar, but properly, in the company’s own aesthetic.

His brief was specific and generous at the same time. Six to eight striking nature photographs. Hover or tap one and a palette of four or five colors emerges from it. Then take it a step further and show how New Life Spaces would translate that palette into an interior. He gave three examples in his own words, such as Adirondack Forest: hemlock green, moss, bark, granite, winter light, translated into oak or walnut, sage cabinetry, soapstone and warm plaster. Longer term, he wanted it to become part of the design conversation itself: instead of asking clients which paint colors they like, ask them “Which of these places feels most like home to you?”

What I built

A new section on the live Biophilic Design page, placed after “How we approach it” and built to match the page exactly: the same display and body typefaces, the same forest, moss, sage and clay palette, the same small tracked labels and hairline rules. It carries its own small stylesheet so it doesn’t depend on the page’s precompiled Tailwind build, and it slots into the existing page template through a single new action hook, so nothing else on the page changed.

Eight nature cards sit under the owner’s own heading and intro: his three examples, plus five more chosen to feel local to the region, a birch stand in snow, a common loon, a northern cardinal, a monarch on milkweed and river stones. Each card shows its photograph with a soft shade at the foot. On hover or keyboard focus, the palette rises over the photo as a row of rounded color pills, one after another, the same gesture as the examples he sent, refined. On touch screens, where there is no hover, the colors are simply shown.

The Color, Borrowed From the Living World section: heading, intro, the question, eight nature cards and the Your own place band.
The section on the live Biophilic Design page: the owner's heading and question, eight nature cards with the loon's palette risen over its photo, and the "Your own place" band.

From the landscape, into the house

Choosing a card fills a panel underneath with the translation the owner described. On the left, From the landscape: the place’s colors as large swatches, each with its name and color code. On the right, Into the house: a small material board, one tile per material, built from the company’s own words. Each panel ends with an action, so the section never leads nowhere.

The Adirondack Forest panel: hemlock green, moss, bark, granite and winter light, translated into oak or walnut, sage cabinetry, soapstone and warm plaster.
Adirondack Forest: hemlock green, moss, bark, granite and winter light, turning into oak or walnut, sage cabinetry, soapstone and warm plaster.

Your own place

The owner’s long-term idea was to let people bring their own sense of home into the conversation, so the section ends with a Your own place band. A visitor uploads or takes a photo of anywhere they love: a camp on the lake, a garden, a view from a porch.

The photo never leaves their device for this step. It is read in the browser, scaled down on a canvas, and five colors are drawn from it with a small k-means clustering pass, seeded across the brightness range so dark and light tones both survive. Each color gets a plain, readable name (“Deep amber”, “Muted slate”) from its hue, saturation and lightness, and no two colors in one palette share a name.

Their photo isn’t one of the company’s places, so its materials come from the closest of the eight. That match is measured in CIE Lab, the color space built around how people actually perceive difference, in both directions (their colors to the place, and the place to theirs), with vivid colors weighted more heavily, because a sky blue or a cardinal red is what makes a palette recognizable. As a sanity check, every one of the eight photos matches its own place.

A sunflower photo turned into five named colors, matched to the Lake at Sunset palette and its materials, with buttons to see it in a room or send the palette.
A visitor's sunflower photo, read in the browser: five named colors, matched to the Lake at Sunset palette and its materials.

Connected to the Room Visualizer, and to the team

The section isn’t a standalone toy. It plugs into the Room Visualizer, the AI chat assistant I had already built for New Life Spaces.

  • See it in your room opens the assistant with that palette already chosen. The visitor uploads a photo of their own room, and the AI redesigns it from the palette’s colors and materials, next to a real New Life Spaces project of the same room type. On this page the assistant asks “Which of these places feels most like home to you?” instead of offering the usual design styles.
  • Send my palette to New Life Spaces turns a visitor’s own palette into an inquiry without needing a room photo: their details, the room, a line on what drew them to that place, and a summary with their color swatches before it sends. The team receives the palette as swatches with codes and names, the closest company palette and the note, in WordPress and by email.

Safe by design

A visitor’s own palette reaches the AI as data, never as text. The server accepts only three to five color codes and the key of one of the company’s active places, and it names the colors itself, so nothing a visitor types can find its way into the AI prompt. I tested it the obvious way: a palette with a sentence slipped in among the color codes is refused outright. Everything else, the bot protection, the one-free-design allowance and the daily cost cap, is inherited from the Room Visualizer.

Edited on the page itself

The owner edits everything on the Biophilic Design page in the WordPress editor, through an Advanced Custom Fields box laid out in four tabs, the same approach the rest of his Design Philosophy pages already use:

  • Section: who can see it (Everyone, Administrators only, or Nobody), the link anchor, and every line of text, including the heading with its italic accent.
  • Places: up to eight, drag to reorder. Each has a name, a photo from the Media Library (its alt text is used on the page), five colors with color pickers and names, and the “How we’d build it” materials. An Auto-pick colors from photo button fills the five colors from the chosen image, using the same method as the public upload, and leaves the naming to him.
  • Your own place: switch the upload on or off and edit its wording.
  • Buttons & labels: every button and label in the section.

The section, the assistant’s list of places and the server’s allow-list all read these same fields, so there is one source of truth. The “Administrators only” setting let us launch it on the real page, in front of the owner, before visitors saw it, and a link anchor means the team can link straight to it.

SectionThe Color, Borrowed From the Living World box in the WordPress page editor, Section tab: visibility, link anchor, label, heading, intro and question.
PlacesThe Places tab in the page editor: a forest photo with an Auto-pick colors from photo button, five color pickers with names, and the materials field.
The box in the page editor: the Section tab, and a place with its photo, Auto-pick button, five named colors and materials.

Where it stands

The section is live and public on New Life Spaces’ Biophilic Design page. It launched with eight demo photographs from Unsplash so the idea could be seen working on the real page, ready to be swapped for the company’s own photography from the page editor at any time. The owner’s three example palettes are in his own words; the other five carry suggested wording for him to refine.

Heimkoma: Bilingual Static Site Migration

Heimkoma sells fractional ownership of Icelandic homes: a Reykjavík apartment, a holiday home, or a share in three properties. Its website ran on WordPress with a page builder, and every English page was generated at request time by a translation plugin, so none of the English existed as a real page Google could index properly. Content changes meant opening the builder, and anything structural meant a developer.

Two pages had already moved off that model: a Reykjavík landing page and a press page, both published from GitHub, and both working well. The job was to bring the whole site onto the same footing without breaking the parts that already ran independently.

What I built

A full migration from WordPress to a bilingual static site, with deployment and measurement built around it:

  • A bilingual static site with 82 pages live, including 41 authored English pages paired to their Icelandic originals in place of machine translation
  • Push-to-publish deployment: the client edits content as files in the repository, and a commit publishes the site with automated checks
  • One deployment model covering the main site, the Reykjavík page and the press page, replacing four separate arrangements
  • Consent-gated embeds for 3D tours, maps and video, with a notice in the visitor’s own language when consent is declined
  • Conversion tracking for enquiries, calendar bookings, phone and email clicks, each wired as a distinct event and verified firing on the live site

The interesting problems

Most of the risk in a migration isn’t building the new site. It’s the old site’s hidden behaviour that only shows up when something stops serving it.

The whole cutover turned on one line

The server had no DirectoryIndex set, so WordPress’s index.php won at every directory root, and any static index.html placed alongside it was simply never served. Adding DirectoryIndex index.html index.php switched the site over, and removing it switched it back. Because copying the new files onto the server changed nothing on its own, everything could be staged and inspected in place before going live, and rollback was a thirty-second operation rather than a restore.

Two traps that only showed up in testing

Deactivating the translation plugin didn’t remove its rewrite rules, so the English site would have stayed dead behind them. And one redirect existed only inside a WordPress plugin, in no repository file; it would have started returning errors the moment WordPress stopped serving pages. Neither was visible from reading the code. Both were found by testing, and fixed before the switch.

A privacy defect found by testing the refusal

The property pages embed 3D tours, maps and video. Checking the cookie banner with consent actively declined showed those embeds loading anyway, contacting third parties for a visitor who had said no. The fix moved each embed behind the consent layer already on the site and added a short notice in the visitor’s language, so a declining visitor sees an explanation rather than an empty rectangle. Eighteen embeds across ten pages were tested in both states before the change went near the live site.

Measurement that counts things that happened

Analytics was deliberately left switched off through launch, so data collection would begin only once events were confirmed to be arriving correctly. Wiring the events surfaced two existing problems: a conversion that counted page views of a thank-you page rather than confirmed bookings, and an automatic form-capture setting that would have recorded every enquiry twice once tracking went on. Both were reported before they could contaminate a single figure.

A deploy built so a bad day stays small

Automation that can reach everything is a liability, so the deploy was designed around what it must never be able to touch. When the client pushes a change, this runs without anyone watching:

  1. Refuse a draft build. A draft carries no sitemap and a no-index flag, and publishing one would remove the site from Google, so the deploy stops instead.
  2. Upload to a staging directory over SSH, using a write-only key that has no shell access and cannot read anything back.
  3. Publish directory by directory, so deletions stay inside folders the site owns. WordPress, the uploaded media, the guest handbooks and the cleaning checklist are unreachable by a deploy, even after a mistaken commit.
  4. Smoke-test the live site by requesting key pages and a redirect, so a failure surfaces in the deploy log rather than in someone’s browser a week later.
  5. Report a green tick, or a named failure with the step that caused it.

Before the switch, a full backup of files, database and server configuration was restored in rehearsal rather than trusted on faith. The routing file the entire site depends on now lives in version control, not only on the server.

Testing

A migration is judged by what survives it. Every address the previous site published was requested against the new one and accounted for, including media files and Icelandic property pages such as /sameign-i-husafelli/ and /haldid-og-sleppt/. That meant 37 legacy addresses reconciled with none broken, 24 redirects configured (including the one found only inside WordPress), 102 retired machine-translation prefixes sent somewhere useful, and 53 post-launch checks, all passing.

WordPress stays installed and running quietly, because the cleaning-checklist form still posts to it. It no longer serves a single public page.

New Life Spaces: Room Visualizer AI Chat Assistant

New Life Spaces is a design-build remodeling company in Upstate New York built around light, nature and craftsmanship, and a lot of their work starts from a gutted basement or a room down to bare studs, not a finished space. The hardest part of selling that kind of project isn’t persuasion, it’s imagination: most homeowners can’t look at exposed joists and a concrete floor and picture a finished room in one of the company’s own design styles. I built Room Visualizer, a custom WordPress plugin, to close that gap directly on their site: a chat assistant that turns a visitor’s own photo into an AI redesign of their room, shown next to a real New Life Spaces project of the same room type, with the conversation ending in a qualified lead in the team’s inbox.

What I built

Room Visualizer is a self-contained WordPress plugin, about 2,450 lines of hand-written PHP, JavaScript and CSS, with its own REST API namespace, its own tabbed settings screen, and its own private lead-storage post type. No page builder, no third-party chatbot embed. A visitor opens the chat, answers a few questions, uploads a photo of their room, gutted, mid-remodel or already finished, picks one of four design styles, and within about a minute sees an AI redesign of their own room, next to a real New Life Spaces before-and-after of the same kind of room.

A New Life Spaces basement kitchen, finished: butcher-block counter spanning the original support columns, black stools, ribbed pendant lamp and oak floor.
One of the real New Life Spaces projects the widget shows: a basement kitchen, finished. The counter was built to span the existing support columns rather than work around them.

Prompting for a real remodel, not a showroom render

Calling OpenAI’s image-edit endpoint was the easy part. Making the result look like a job New Life Spaces would actually build was the real work. Each of the four styles on offer, Japandi, Biophilic, Nordic Simplicity and Living Materials, carries its own prompt written around the specific materials the company builds with: solid oak and hickory floors, butcher-block counters, live-edge shelving, matte black fixtures, shoji-style sliding screens, not just a style name handed to the model. And because most of the photos this widget actually receives are from a construction site rather than a finished room, every room type also carries its own “what finished looks like” checklist. For a bathroom, that means a vanity, a toilet, and a shower or tub placed where the existing plumbing already is. The prompt tells the model exactly what “done” means for that specific room before it ever touches the photo, and handles the opposite case too: if the room is already finished, it restyles the finishes and furnishings instead of trying to rebuild a room that doesn’t need it.

BeforeA New Life Spaces basement bathroom corner before work: concrete block walls, a small window, exposed plumbing and insulation.
AfterThe finished New Life Spaces bathroom: a soaking tub with black stone surround, tiled niches holding plants, and a bamboo shade at the window.
The kind of change the widget is built to show: a real New Life Spaces basement bathroom, before and after, the same corner and the same window.

Built so it can’t run away with the client’s OpenAI bill

Every AI design costs New Life Spaces real money, so the plugin treats every anonymous visitor as a potential script until proven otherwise. A signed session token, tied to the visitor’s IP and issued when the chat opens, has to be presented with every request and is refused if it’s used too quickly after the chat opened or too long after it expires. A honeypot field catches simple bots outright. Uploaded photos are checked by their actual bytes, not their declared file type, and capped at 8MB. Each visitor gets one free AI design, tracked by IP and by email so switching one doesn’t reset the other, and that allowance is claimed before the paid call goes out and only released if the call fails, so two requests in parallel can’t both slip through. A site-wide daily cap on paid renders sits underneath all of it as a hard stop on cost.

Real work, always on screen, even when the AI isn’t

If OpenAI is slow, over quota, or simply not connected yet, the widget never shows an error. It falls back to a client-side color preview, generated instantly in the browser from the same palette each style already defines, so a visitor always sees something happen. The AI result and the real project photo are both shown in the same drag-to-compare slider, not a static side-by-side, so comparing “your room” to “our work” feels the same either way.

Everything editable without touching code

The team runs this from an ordinary WordPress admin screen: six tabs covering general visibility, the OpenAI connection (with a free “test connection” check that verifies the API key without generating an image), the chatbot’s wording and colors, the four design styles, the five rooms and their example photos, and lead handling. Each tab saves independently, so editing the welcome message can’t accidentally blank the API key. Every lead lands in a private post type only administrators can see, with the room, style and note laid out in a table and an email sent straight to the team with Reply-To set to the visitor’s own address.

Where it stands

Room Visualizer is live in Live mode, real AI designs, real cost per render, on New Life Spaces’ Design Philosophy pages, the pages a visitor is already on when they’re picturing a style for their own home. It’s a deliberate, scoped rollout rather than a site-wide flip: the plugin’s per-page targeting keeps it off the rest of the site for now, so the team can watch real designs and real leads come in from its best-fit pages before switching it on everywhere.

Rank Analyzer: AI SEO Platform & Chatbot

Rank Analyzer is an AI-powered SEO and content analysis tool I built and run myself, aimed at marketers and SEO teams navigating the shift to generative search: Google’s AI Overviews, ChatGPT search, and the other AI answer engines that now sit between a search and a click. It needed a marketing site that could explain a genuinely technical product in plain language, and a way to answer the questions every visitor asks (what’s included, what does it cost, how is this different from a normal audit) without me answering each one by hand.

What I built

The site itself runs on WordPress and Elementor. Behind the chat bubble in the corner is a custom WordPress plugin I wrote rather than a third-party embed, a guided assistant that actually knows the product’s plans and pricing instead of deflecting to a contact form.

The homepage sets out the product in three parts: live research across major AI search engines compiled into a dataset-type scoring table, a content recipe (headings, proportions, schema markup) built to match what those engines reward, and an eight-week cluster plan with multimedia ideas to build topical authority beyond a single article.

The chatbot

The custom AI chatbot widget on Rank Analyzer, open and showing its quick-reply topics
The chatbot open on the homepage, offering guided topics rather than a blank message box.

It knows the actual pricing, not just “get in touch”

Asking the bot about the flagship offer doesn’t route to a contact form. It names the plan, the price, and exactly what’s included: a comprehensive report, on-page and technical audit, dataset scoring with competitor insights, and keyword-gap opportunities. Most chat widgets bolted onto a marketing site can’t do this, because they’re not actually wired to the product.

Knowing when to stop guessing

I tested it with an open-ended question it hadn’t been given a script for (how this compares to a normal SEO audit tool). Instead of improvising an answer it couldn’t stand behind, it acknowledged the question and pivoted into collecting a name and contact details for a real follow-up. A wrong guess from a chatbot costs more trust than an honest handoff, so the bot is built to know the difference between a question it can answer on the spot and one that needs a person.

Testing

I test the chatbot the same way a real visitor would: working through each quick-reply path (SEO analysis and strategy, pricing and plans, getting a quote, existing-customer support) and then trying to break it with questions outside the script, checking that it either answers correctly or hands off cleanly rather than inventing something.

Sacks.ie: DPD Ireland Shipping Integration

Sacks Online Ltd run a busy Irish e-commerce store shipping bulk bags and sandbags nationwide, and every parcel meant re-typing a customer’s address into DPD’s portal by hand before printing one label at a time. No existing plugin fit: WooCommerce shipping plugins on the market target DPD UK and DPD Netherlands, entirely different platforms from DPD Ireland’s API, and the only off-the-shelf route was paid middleware sitting between the shop and the courier. Since DPD Ireland’s API is just three endpoints (authorise, create consignment, track), building directly was cheaper and cleaner, and the client owns the result outright with no recurring fee.

What I built

A self-contained WooCommerce plugin providing:

  • One-click label creation from the order screen, with parcel count and weight prefilled from the order’s own data
  • Bulk label creation and printing: select a batch of orders, create every consignment, and print them all as one job
  • A label icon on the orders list showing the consignment number, so a label can be reprinted without opening the order
  • Automatic consignment creation on a chosen order status, running in the background so checkout is never delayed
  • Customer-facing tracking links on the order and in notification emails
  • A test and live environment switch, with the interface making it unmistakable which one is active

The interesting problems

Most of the value in an integration like this isn’t the happy path. It’s the details that quietly produce wrong outcomes months later.

Northern Ireland is not the UK for customs

Northern Ireland addresses arrive in WooCommerce with a GB country code and a BT postcode, and the obvious mapping, GB means UK means a customs declaration, would have been wrong for a large share of orders. Under the Windsor Framework, Ireland to Northern Ireland stays inside the EU customs area for goods, while Ireland to GB mainland doesn’t, so the plugin classifies destinations on postcode as well as country. I verified this against the courier’s live routing rather than the documentation: the test label came back reading “NORTHERN IRELAND” despite the GB code we sent.

DPD test shipping label for a Northern Ireland address, showing NORTHERN IRELAND as the destination
Test label for a BT postcode address. The destination reads Northern Ireland even though WooCommerce sent a GB country code.
DPD test shipping label for a Republic of Ireland address
The equivalent label for a Republic of Ireland address, for comparison.

A rejected shipment can look exactly like a success

The courier’s API returns HTTP 200 with a status of “OK” even when it has rejected a consignment, with the rejection buried in a per-consignment error field, and the rejected record still carries a tracking number. Reading the tracking number and assuming success would have recorded failed shipments as booked, so the parser checks the overall status, then the per-consignment error, and only then reads the tracking number, covered by tests built from the API’s own rejection samples.

Three more details worth getting right

  • The label lives behind a second request. The API returns a URL rather than the label itself, and since that URL arrives inside a response body, it’s validated against the courier’s whole domain before being fetched, guarding against a classic server-side request forgery.
  • One special character, one silently deleted field. PHP’s DOMDocument::createElement() parses rather than escapes its value, so a customer trading as “Caffè & Sons Ltd” produced an empty business name element with no error or warning. A test built from hostile input caught it before it reached a real address.
  • The label format followed the printer, not the spec. ZPL looked obvious for the warehouse’s Zebra printer until it turned out to be USB-connected to one PC, where a browser can’t push raw ZPL without extra software. The courier’s PDF prints straight through the Windows driver, so batching moved into the plugin instead, merging each run into one multi-page PDF.

Testing

Courier APIs are awkward to develop against: every call is stateful, and on a live account every consignment costs money. A headless test harness runs the plugin’s real classes with WordPress replaced by lightweight stubs, exercising the whole pipeline from the command line against DPD’s pre-production environment, backed by around seventy unit assertions built from the API’s own sample payloads. Credentials stay out of the database in favour of configuration constants, and every logged request is scrubbed of credentials and session tokens.

Need DPD Ireland integration for your store?

I build and fix DPD Ireland integrations for WooCommerce, Shopify and custom stores. See how it works and request a quote: DPD Ireland integration for WooCommerce and Shopify.

What’s On: Reykjavík Events Platform

What’s On has been Reykjavík’s event guide since 1994, in print and online, so their site needed to carry that same reliability. The brief was another straight Figma-to-build: take a finished Figma design and implement it faithfully in WordPress and Elementor, matching every layout and type detail without shortcuts.

The centrepiece is the events calendar, which doesn’t run on hand-entered listings. It pulls live event data from an external events API and renders it as a continuously updated, filterable grid, browsable by week, complete with venue, category and imagery for each listing. When an event’s details change upstream, the site reflects it automatically, with no one on the What’s On team re-typing a single listing.

MRC Recruiting: Live Job Board Integration

MRC Recruiting runs on Recruit CRM to manage every open role, but their WordPress site had no live connection to it. Job postings would have needed manual copy-pasting to stay current, with a real risk of closed roles sitting on the public site for weeks.

We built a custom plugin that pulls job data directly from the Recruit CRM API into WordPress through two shortcodes: a searchable job list and a full job detail page. Search filters (keyword, location, location type, job category) are built dynamically from live job data, so new locations and categories appear on their own as new roles are posted. Closed, on-hold, and application-disabled roles are filtered out automatically at fetch time, and a short cache window keeps the site refreshed against the API within seconds, not the multi-minute delays the earlier caching approach allowed.

Iceland Review: Editorial Platform Rebuild

Iceland Review is Iceland’s leading English-language news portal and travel adviser. It’s part daily news for readers following the country, part travel guidance for visitors planning a trip. Almost all of its income comes from display advertising, so this rebuild couldn’t treat ad placements as an afterthought. They needed to be part of the layout from day one.

We rebuilt the platform on a custom WordPress theme with Advanced Ads managing every placement: banner units the editorial team can position and swap without touching a line of template code, sized to stay visible and viewable (which is what actually drives ad revenue) without slowing the site down or burying the articles underneath them.

Around that ad infrastructure, we restructured navigation for the site’s two real audiences: News for readers following Iceland day to day, and Travel for visitors planning ahead. We also cleared years of accumulated technical SEO debt and set up an ongoing webmaster relationship that’s still running today.

Real Search Console data over the past 3 months:

Google Search: 61.5K clicks, 5.44M impressions over 3 months
Google Search: 61.5K clicks, 5.44M impressions over 3 months
Google Discover: 1.55K clicks, 60K impressions, including a single-day spike
Google Discover: 1.55K clicks, 60K impressions, including a single-day spike
Google News: 1.22K clicks, 38.2K impressions at a 3.2% CTR
Google News: 1.22K clicks, 38.2K impressions at a 3.2% CTR

Líparít: Advertising Agency

Líparít is a small but well-established branding and packaging design studio working out of Reykjavík and Akureyri, with a client list that includes Arna, Freyja, ORA, Klaki and TourDesk. Their own site needed to hold up to the same design standard they deliver for clients, so the brief was a straight Figma-to-build: take their finished Figma design and implement it in WordPress without deviating from it.

We built the site on Elementor (on top of the Hello Elementor base theme), translating the isometric brand mark, typography and spacing from the Figma file into a fully responsive layout, pixel by pixel. The centrepiece is a self-service portfolio grid: dozens of real packaging and branding projects, from Easter confectionery for Freyja to dairy branding for Arna to conference identity work for Norðanstormur, built so the two-person studio can add new case studies themselves without touching code or calling a developer.

elegantweb studio Get Started ↗
Email