๏ Spring Hill KS, 66083

๏€ƒ  dovewebconsulting@gmail.com

Digital Marketing Strategist ยท measurement, technical SEO, marketing engineering

I work where marketing meets engineering: the tracking that shows what is working, the technical faults that quietly cost money, and the systems that keep running after the person who built them is gone. Selected work from a home-services client, Plumbing, a Kansas City plumbing company, over the last twelve months.

๐Ÿ“ง DoveWebConsulting@gmail.com ๐Ÿ“ž 913-439-0166 ๐ŸŒ DoveWebConsulting.com ๐ŸŽ“ MS, University of Tennessee, 2016
54.8โ†’22.0avg Google position
Cโ†’A+security grade
2.2ร—search click rate
2,233documented decisions
๐Ÿ” Every number on this page can be checked. The ranking figures come from the Google Search Console API, the security grades from Mozilla Observatory and a live DNS lookup anyone can repeat, and the engineering work from a dated ledger with the verification attached to each entry. Where the data did not support a claim, the claim is not here. There is no traffic growth headline on this page, because the honest series does not show one.

๐Ÿ“ˆ Lifted average Google position from 54.8 to 22.0 in twelve months

60% better ranking ยท CTR 2.2ร— ยท clicks +25% on 42% fewer impressions
๐Ÿ“ˆ GOOGLE SEARCH POSITION ยท lower is better Average position across all queries, a home-services client site, pulled from Search Console on 23 August 2026 20304050 54.8 22.0 25-0825-1025-1226-0226-0426-06 โœ… 54.8 to 22.0 over twelve months, a 60 percent improvement, while impressions were deliberately cut 42 percent

The problem

Average position sat at 54.8, which is page six of the results. Impressions were high and worthless: 196,183 in a single month producing 217 clicks, a click-through rate of 0.11 percent.

What I did

The bulk of it was consolidation rather than creation. 41 legacy location pages were folded into their service hubs behind 301 redirects, which removed a set of scaled doorway pages that were a genuine penalty risk while keeping their equity. A redirect audit found 129 orphaned URLs already resolving correctly and a further block deliberately left alone. Alongside that, title tags and the internal link structure were rebuilt around what the business actually sells. The strategy was deliberately to shed impressions that were never going to convert.

The result

Average position 54.8 to 22.0. Clicks 217 to 271, up 25 percent, while impressions fell 42 percent from 196,183 to 112,784. Click-through rate went from 0.111 percent to 0.240 percent, more than double.

๐Ÿ” How it can be checked: Google Search Console API, monthly, August 2025 through July 2026. The chart above is that series, unedited.

๐Ÿ›ก๏ธ Took the site from a security grade of C to A+ and locked down email spoofing

Mozilla Observatory C 55/100 โ†’ A+ 105/100 ยท DMARC none โ†’ reject
๐Ÿ›ก๏ธ SECURITY POSTURE ยท before and afterMozilla Observatory, the same public scanner anyone can run against the domain today๐ŸšซBEFORE ยท grade C55 out of 1007 of 10 tests passingno enforced email policyanyone could spoof mailfrom the domainโœ…AFTER ยท grade A+105 out of 1009 of 10 tests passingDMARC p=reject at 100%CSP nonce per request,no unsafe-inline or eval๐Ÿ” The one remaining failure is Subresource Integrity, left off deliberately because the scripts are Google hosted and change without notice

The problem

The site scored C, 55 out of 100, on Mozilla Observatory, passing 7 of 10 tests. The domain had no enforced email authentication, so anyone could spoof mail from it.

What I did

Worked the header stack one item at a time, each gated on a measurement first. The last step was removing the unsafe-eval relaxation from the Content Security Policy, which I tested by deploying a report-only copy alongside the live one and confirming zero violations across real page loads before enforcing it. For email I parsed 223 DMARC aggregate reports covering 12 to 19 August, confirmed every legitimate source was passing, and only then moved the policy to reject.

The result

Observatory A+, 105 out of 100, 9 of 10 tests. The one remaining failure is Subresource Integrity, deliberately not used because the scripts are Google hosted and change without notice. DMARC now p=reject at 100 percent.

๐Ÿ” How it can be checked: Verifiable from outside right now: the live response headers carry HSTS with includeSubDomains, a per-request CSP nonce with no unsafe-inline and no unsafe-eval, and a DNS lookup of _dmarc.a home-services client site returns p=reject.

๐Ÿ” Found a five month old attribution bug that had been mislabelling every Bing lead as Google

Present since March, invisible until one deliberate test
๐Ÿ” WHY EVERY BING LEAD CAME BACK LABELLED GOOGLEThe tag read the correct source, then threw it away. One guard clause, live since 9 March.๐Ÿ“ฑ1 ARRIVESutm_source=bingcorrectly in the URL๐Ÿ‘2 TAG READS ITsees bing,holds it briefly๐Ÿšซ3 GUARD CLAUSEa stored Google click idfrom the last 90 daysexists, so DISCARD๐Ÿ“Š4 RECORDED ASgoogle / cpcwith a campaign andkeyword never in the URLโœ… Fixed so the newest tagged arrival wins and the stale click id is cleared with its cookie. Shipped as three separately reversible versions.

The problem

Bing spend looked like it produced almost nothing. A single lead in five months carried a Bing source, while GA4 recorded 112 paid Bing sessions and 16 paid Bing form starts on the contact page over the same period. Those two numbers could not both be true. GA4 only labels a session bing / ppc when the tracking tags are physically present in the address, because it does not infer paid Bing from a referrer and does not read the Microsoft click id, so those 112 sessions were arriving correctly tagged and something downstream was overwriting the label.

What I did

Ran one controlled test: arrived through a Bing-tagged link and watched what the lead record said. It came back google / cpc, carrying a campaign, keyword and ad id that had never been in the address. The cause was a guard clause in the tag that read an explicit source and then discarded it whenever a stored Google click id from the previous ninety days existed. A trailing branch existed to merge them but was unreachable.

The result

Rewrote it so the newest tagged arrival wins and the stale click id is cleared along with its cookie. Verified on both hostnames before publishing. Shipped as three separately reversible versions so any one could be rolled back alone.

๐Ÿ” How it can be checked: Recorded in the ledger with the before and after tested in the same browser on the same day. I also measured the historical damage honestly and revised my own first estimate down: 66 real lead emails since March, best estimate ten to fifteen mislabelled, stated as an estimate rather than a measurement.

๐ŸŽฏ Caught the primary Google Ads conversion counting phone taps as form leads

On the day it was found, 9 of 10 recorded leads were phone taps
๐ŸŽฏ ONE TAG, TWO TRIGGERS ยท what the bidding was actually optimising forThe lead event that Google Ads imports as the Primary conversion for form leads๐ŸšซBEFORE ยท one tag, two triggers๐Ÿ“ form submit โ†’ generate_lead๐Ÿ“ž phone tap โ†’ generate_leadone number, two meanings9 of 10 recorded leads were tapsโœ…AFTER ยท one trigger each๐Ÿ“ form submit โ†’ generate_lead๐Ÿ“ž phone tap โ†’ button_click_calltwo numbers, two meaningsphone tracking left untouched๐Ÿ” Proved in Tag Manager Preview on the live site, then confirmed on real traffic the next day

The problem

One tag was attached to two triggers: the contact form and the click-to-call button. Both wrote the same event name. That event is imported into Google Ads as the Primary conversion for form leads, so the number the bidding algorithm optimises against was mostly phone taps.

What I did

Split them at the trigger rather than touching any Google Ads conversion tag, because those are what the bidding runs on. Proved the split in Tag Manager Preview on the live site: tapping a real call button fired the phone tags and did not fire the lead tag.

The result

Once that day's analytics finalised, 9 of the 10 recorded leads were phone taps and 1 was a real form. Verified again on live traffic the following day: a genuine phone tap produced a call event and zero lead events, and the only lead recorded was a real form submission.

๐Ÿ” How it can be checked: Before and after read out of Tag Manager version history, so both states are on the record rather than asserted. Rollback is republishing the previous version.

๐Ÿค– Built an AI phone agent that recognises returning customers, and put it live

Answers the real business line ยท confirms identity in 3.8 seconds
๐Ÿค– HOW THE PHONE AGENT RECOGNISES A CALLERIt confirms rather than asserts, because a phone number is a household and not a person๐Ÿ“žCALLER IDthe number arriveswith the call๐Ÿ”CRM LOOKUPmatched against5,717 customersโš–๏ธONE MATCH?154 numbers map tomore than one person๐Ÿ’ฌASKS, NEVER TELLSAm I speakingwith Robert?at 3.8 seconds๐Ÿšซ If the number maps to more than one customer it refuses to guess, and it never reads an address aloud

The problem

The AI receptionist treated returning customers as strangers, asking again for a name and address they had already given. Callers hung up.

What I did

Added a lookup against the CRM on caller id. It asks to confirm rather than asserting, because a phone number is a household and not a person. It refuses when one number maps to more than one customer, which is 154 numbers out of 5,717, and never reads an address aloud. Built the whole thing on a cloned shadow assistant with its own test number first, and only promoted it after a live call proved it.

The result

On the proving call the lookup fired and the agent asked to confirm the caller's name at 3.8 seconds with no dead air.

๐Ÿ” How it can be checked: Promotion recorded with a configuration hash before and after, and a one command rollback documented alongside it.

โ˜๏ธ Migrated the whole stack off cloud infrastructure in a budget cut, with nothing lost

4,010 customer profiles rebuilt ยท 3,567 historical transcripts preserved
โ˜๏ธ MIGRATING OFF THE CLOUD WITHOUT LOSING THE HISTORYThe server was switched off in a budget cut. Three years of call transcripts existed nowhere else.๐Ÿ’€SERVER OFFprofile system,attribution bridge,reporting all on it๐Ÿ’พWHAT SURVIVEDthe source data wasalready on disklocally๐Ÿ”งDATA LAYER ONLYscoring, segmentationand pitch logic leftcompletely untouchedโœ…REBUILT4,010 profiles, 59 fields4,016 transcripts,3,567 back to 2023๐Ÿ” A structural diff ran against the previous version before anything was saved. Verifier exit code zero.

The problem

The cloud server running the intelligence stack was switched off. The customer profile system, the attribution bridge and the reporting all lived on it, and three years of call transcripts existed nowhere else.

What I did

Rebuilt the customer profile generator against data already held locally rather than restoring the server. Kept the scoring, segmentation and pitch logic untouched and replaced only the data layer. Treated the historical transcripts as the thing that must survive and merged rather than replaced.

The result

4,010 profiles with all 59 fields, and 4,016 call transcripts carried forward including 3,567 going back to 2023. A structural diff against the previous version ran before anything was saved.

๐Ÿ” How it can be checked: Verifier exit code zero, with profile count, field count and transcript count all checked against the prior file rather than eyeballed.

๐Ÿ—๏ธ Built and running, waiting to be used

Not everything I built was switched on. These are complete systems that exist on the site or in specification today, held back by budget or by a decision that was never taken rather than by anything technical. They are here because the foundation is the part that takes the skill, and because it explains why the traffic curve on this page is flat while the ranking curve is not.

โœ๏ธ Blog engine2,517 lines across 9 snippets, all live

A complete publishing system sits on the site today: styled article templates, automatic read-time calculation, a table of contents, search with autocomplete, archive and popular-post components, analytics events wired for every scroll and click, and the redirect map for the URL structure. It is switched on and running. What it has never been given is a content programme.

๐Ÿ“ง Email and reactivation programmedesigned against a real 4,010 profile base

A campaign system and a reactivation strategy built on top of the customer profile work: segmentation by recency, value and service history, with the message logic and the do-not-contact handling already specified. The audience exists and is loaded. The sends were never approved.

๐Ÿ’ฐ Membership programmepricing, tiers and economics modelled

A paid membership plan with tier structure, pricing and the margin arithmetic worked through, written to be handed to an owner as a decision rather than a suggestion. It has not been launched.

๐Ÿ“ Citation and local listing correctiongaps identified, fixes specified

An audit of business listings across directories, including the ones search surfaces that the usual tools miss, with the single highest-impact fix identified: an opening-hours conflict where the site schema says one thing and the public profile says another. Specified and queued, not executed.

๐Ÿ“ˆ Conversion event architecturebuilt, and then corrected

The event layer that tells the ad platforms what a lead is. This one did ship, and then I found it was counting phone taps as form submissions and fixed it. The wider event plan beyond leads was never implemented.

๐Ÿ’ก The pattern worth noticing: the work that shipped is measurable and improved. The work that did not ship is built and ready. Neither of those is a story about effort, they are stories about which decisions got made.

๐Ÿ”ง How I work

Most of what follows was learned by getting something wrong first and writing down what would have caught it.

๐Ÿงญ
The tool that made the change never confirms it

A script reporting success is not evidence. A file edit gets re-read, a tag gets checked in Preview, an endpoint gets curled, a deploy gets fetched from outside. Every claim in this portfolio was checked from somewhere other than where the change was made.

๐Ÿ“‘
A written correction log, not just a win log

Every mistake gets recorded with what it cost and what check would have caught it, and that check gets automated. There are 82 entries, and the most recent one is about this document. This portfolio itself was scoped down twice because the data did not support the first version of a claim.

๐Ÿ•
Numbers carry their window and their source

A zero over one day and a zero over thirty days are different claims. Most false alarms in analytics are a date range left on the wrong setting, and most fake discrepancies are two different populations being compared.

โ†ฉ๏ธ
The undo is written before the change

Version number, previous value, or the single command to reverse it, recorded in the same breath as shipping. Nothing goes live without a way back.

๐Ÿ”ง HOW I WORKThe same five steps whether it is a ranking problem, a broken conversion, or a phone system.๐Ÿ”MEASURE FIRSTFind what the numbersactually say beforetouching anything.Most emergencies area misread.๐ŸงฉFIX THE SIGNALIf the measurement iswrong, everythingbuilt on it is wrong.Attribution beforeoptimisation.๐Ÿ› ๏ธBUILDShip it small, behinda preview, with arollback writtenbefore the changegoes out.โœ…PROVECheck from a differentvantage than the onethat made the change.Receipts, notnarration.๐Ÿ“šWRITE IT DOWNSo the next person,or the next me, doesnot rediscover it.2,233 entries andcounting.๐Ÿ’ก The differentiator is not the tooling. It is that every number above can be checked by someone who does not trust me.

๐Ÿ› ๏ธ What I work with

๐Ÿ“Š Measurement

GA4 + Data APITag ManagerSearch ConsoleGoogle AdsMicrosoft AdvertisingMetaoffline conversion importserver side events

๐Ÿ› ๏ธ Build

PHPWordPress at hook levelJavaScriptPythonREST integrationCloudflare WorkersBricksServiceTitan APIHousecall Pro API3CX telephonyVapi voice agents

๐Ÿงฉ Practice

technical SEOsite architectureconversion tracking designattribution debuggingsecurity headersemail authenticationdocumentation a stranger can use
๐Ÿ“š The work is documented, not just described. Alongside the results above sits a 2,233 entry engineering ledger, reference documentation for twelve platforms written from live systems rather than vendor pages, and a teaching cookbook that maps every piece of code running on the site function by function, so the next person does not have to reverse engineer it.