 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, over the last twelve months. No client is named on this page.

πŸ“§ DoveWebConsulting@gmail.com πŸ“ž 913-439-0166 🌐 DoveWebConsulting.com πŸŽ“ M.A., Cultural Anthropology, University of Tennessee, 2016
297keywords in the top 10
C→A+security grade
9 of 10recorded leads were phone taps
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.

πŸ“ˆ A measured search baseline, and what it does not yet show

Average position 27.4 Β· 64 keywords in the top 3 Β· 297 in the top 10

The problem

Impressions were high and clicks were not. Over a trailing ninety days the site was shown 315,407 times and earned 310 clicks, a click-through rate of about a tenth of one percent. Nothing on the site recorded a lead, so there was no way to tell a good month from a lucky one.

What I did

The bulk of it was consolidation rather than creation. 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 separated the orphaned URLs that were already resolving correctly from the block that was deliberately left alone. Title tags and the internal link structure were rebuilt around what the business actually sells.

What the baseline shows

Average position 27.4 across the account, with 64 keywords already in the top three and 297 in the top ten, and the money cluster still on page three. This is one measurement, taken on 22 July 2026 over a trailing ninety days. It is a starting line and not a result, and it is quoted that way on purpose.

πŸ” How it can be checked: Google Search Console, baseline pulled 22 July 2026 over a trailing ninety days, recorded in the project tracker’s KPIs sheet. One measurement, stated as one measurement.

πŸ›‘οΈ 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 mislabeling every Bing lead as Google

Present since March, invisible until one deliberate test
πŸ” WHY EVERY BING LEAD CAME BACK LABELED 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 mislabeled, 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 optimizing 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 optimizes 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 recognizes returning customers, and put it live

Answers the real business line Β· confirms identity in 3.8 seconds
πŸ€– HOW THE PHONE AGENT RECOGNIZES 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 program.

πŸ“§ Email and reactivation programdesigned 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 programpricing, tiers and economics modeled

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 beforeoptimization.πŸ› οΈ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.