Blog

  • How to Update WordPress Core Without Opening an Attack Window

    How to Update WordPress Core Without Opening an Attack Window

    WordPress 7.1.4 is a security release, and the flaw it fixes is already being exploited in the wild. The uncomfortable part is that on an unpatched site, the update itself is the attack window: WordPress unpacks the new files into a folder inside your web root and finishes installing them while still running the old, vulnerable code. Two habits close that window — take a backup, and block web access to wp-content/upgrade before you click Update.

    The flaw is CVE-2026-119977, a time-of-check to time-of-use race in WordPress’s core upgrade process. It affects versions 4.7 through 7.1.3. WordPress shipped the fix on October 28, and CISA has since added the flaw to its known-exploited list after confirming attacks in the wild; Wordfence said it blocked 13,095 attempts against it in a single 24-hour stretch. This is not a theoretical hardening post.

    The short version

    • CVE-2026-119977 can give an unauthenticated attacker code execution during a core update. It is fixed in WordPress 7.1.4.
    • Versions 4.7 to 7.1.3 are affected, and the patch was backported to older branches, so check what you are actually running.
    • Blocking /wp-content/upgrade/ at the web server kills the attack without stopping the updater.
    • Order matters: back up, block, update, verify. Blocking after the update is too late for that update.
    • The plugin queue is bigger than the core queue — 549 plugin vulnerabilities were disclosed in a single week.

    What the flaw actually does

    When WordPress updates core with a full package, it downloads a complete copy of the new version and unpacks it into a temporary folder inside wp-content/upgrade/. That copy includes the installer. The folder lives under the web root, so the web server will happily serve files from it.

    The temporary folder used to have a name an attacker could predict or find. So the sequence is: you start the update, WordPress writes the new files into a reachable folder, and an attacker who is polling for that folder loads the installer and completes an install before WordPress deletes the copy. That is code execution with no account and no password. The 7.1.4 fix gives the temporary folder a random name, so there is nothing left to guess.

    Worth being honest about the shape of the risk: it is a narrow window and a high-complexity attack. It is not a drive-by. But narrow windows are exactly what automated scanners are built to hit, and “high complexity” stops meaning much when the scanning is free.

    Who is actually exposed

    • Sites on 4.7 to 7.1.3 that have not updated since October 28. That is the whole affected range, including old installs people forgot they own.
    • Shared hosting, most of all. When WordPress cannot write core files directly, it falls back to the full-package upgrade — the exact code path this bug lives in.
    • Anyone who turned off auto-updates and then stopped checking. Turning off auto-updates is a legitimate choice; turning off the calendar reminder that goes with it is not.

    If you are already on 7.1.4 or newer, the flaw does not apply to you, but the habit still does. If your host patches for you, ask for the core version number rather than for reassurance — the answer is a version, not a feeling.

    The five-minute update order

    Do these in order. The block has to exist before the update runs, not after.

    1. Back up the database and the files. A database export plus a tarball of the site directory. If you have never restored one, test the restore before you need it.
    2. Block the upgrade folder at the web server. On nginx, add a location block that returns 403 for ^~ /wp-content/upgrade/. On Apache or LiteSpeed, add a rewrite rule in the site’s root .htaccess. This only refuses HTTP requests; PHP can still write to the folder, so the updater keeps working.
    3. Update core. Dashboard → Updates → Update now, or from the command line with WP-CLI.
    4. Verify. Confirm the version, verify core checksums, then load the homepage and the login page while logged out.
    5. Leave the block in place. It costs nothing and closes the same trick in the next security release.
    # 1. Back up before anything
    wp db export ~/backup-$(date +%F).sql
    tar czf ~/files-$(date +%F).tar.gz public_html
    
    # 2. Block the upgrade folder (nginx), then reload nginx
    #    location ^~ /wp-content/upgrade/ { return 403; }
    #
    #    Apache / LiteSpeed: put this above the WordPress block
    #    in the site root .htaccess:
    #    RewriteRule ^wp-content/upgrade/ - [F,L]
    
    # 3. Update and verify
    wp core update
    wp core version
    wp core verify-checksums
    
    # 4. Plugin queue
    wp plugin list --update=available
    wp plugin update --all

    If a host control panel runs updates for you and something breaks while the folder is blocked, remove the rule, run the update, and put it straight back. What you do not want is an update that starts, fails halfway, and leaves a half-unpacked copy sitting in a web-reachable directory while you go looking for the cause.

    The plugin queue is bigger than the core queue

    Wordfence’s weekly report for October 26 to November 1 counted 549 vulnerabilities across 351 plugins and 4 themes, including 17 critical and 132 high-severity disclosures. Forty-one had no patch at the time of disclosure. You cannot update your way out of those — the only move is to deactivate and delete.

    A separate batch documented on November 9 covers 13 plugins, and four of them are common enough to check by name:

    Plugin Installs Problem Action
    LatePoint 100,000 Auth bypass, CVSS 9.1, through 5.7.6 Update now
    TranslatePress 400,000 Two stored XSS via comments, CVSS 7.2, through 3.3.8 Update now
    Amelia 90,000 SQL injection plus missing authorisation, CVSS 7.5, through 9.9.1 Update now
    wpForo 20,000 SQL injection and subscriber-to-admin escalation, CVSS 7.5, through 3.2.2 Update now

    None of these plug straight into core, so they are not today’s headline. They are the reason “I updated WordPress” is not the same statement as “I patched my site”. A booking plugin with an authentication bypass is a worse day than a narrow core race condition, and it does not announce itself.

    How to tell whether someone hit you during an update

    Three checks, five minutes, no plugins required:

    • Access logs. Search for requests to /wp-content/upgrade/, especially POSTs to an installer file from an address that is not yours. Reading access logs is the same skill as spotting a brute-force run, pointed at a different string.
    • Administrator accounts. List the administrators and compare the registration dates against the day you ran the update. A new admin you did not create is the whole answer.
    • Unexpected files. Anything ending in .php inside uploads, mu-plugins, or a leftover upgrade folder that you did not put there. This is part of the five-minute infection check.

    If two of the three come back dirty, stop tidying and treat the site as compromised. Cleaning the symptom without finding the entry point is how a site gets reinfected a week later; the recovery order is: isolate, preserve evidence, rebuild from a known-good backup, then rotate every credential that touched the site.

    The checklist I actually run

    • Core version is 7.1.4 or newer on every site I am responsible for, including the one I forgot about in September.
    • /wp-content/upgrade/ returns 403 from the public internet.
    • A backup from the last seven days exists, and one restore has been tested this year.
    • No active plugin has a published, unpatched vulnerability — those get deleted, not “monitored”.
    • Access logs checked for upgrade-folder requests in the last 30 days.
    • One admin account per person who actually needs it, and nobody’s old developer still on the list.

    None of this is exciting, which is the point. Sites rarely get hurt because the owner lacked a clever security stack. They get hurt because a three-week-old security release looked like a maintenance chore, and the update window stayed open while nobody was watching.

  • How to Fact-Check AI Drafts Before Publishing

    How to Fact-Check AI Drafts Before Publishing

    A benchmark reported yesterday by Alibaba researchers should be pinned above every content calendar. They collected 37,463 real requests from 26,314 coding sessions where people had struggled to get an AI assistant to finish a job, refined them into 273 tasks with 2,331 hidden acceptance criteria, and ran ten leading models through them. The best model, OpenAI’s GPT-6 Astra, met an average of 69.9% of the criteria per task. It fully solved 26.4% of the tasks. On multi-file repository edits, that figure fell to 17.9%.

    Coding is not blogging. But the shape of the failure is the same: the draft looks finished, most of it is right, and the last quarter is where the damage lives. A wrong version number, an invented quote, a link that goes nowhere, a confident sentence about something the model cannot know.

    So I do not publish an AI draft until it has been through a review pass that treats every factual sentence as a claim to be checked. Not a vibe check. A source check. Here is the exact pass I use.

    The short version

    • Assume every number, name, date and quote is wrong until you have opened a source.
    • Check the link, not just the claim. A dead or mismatched link is a factual error with a URL attached.
    • Delete any first-person experience the draft invented, and replace it with one you actually had.
    • Soften what you cannot verify, and say so plainly instead of sounding certain.
    • Read the finished piece aloud once. Confident nonsense survives silent reading and dies at speaking speed.

    Why AI drafts fail in the same five places

    After enough drafts, the errors stop being random. They cluster in five predictable places.

    1. Numbers without a source. A percentage appears with three decimal places and no origin. It may be real, remembered from somewhere, or invented to make the sentence sound researched. You cannot tell by looking, which is the point.
    2. Citations that do not exist. Titles, authors, journals and years that read plausibly. Ask for the URL and you get a 404, or a page that says something else entirely.
    3. Stale product facts. Pricing, limits, version numbers and feature lists change faster than training data. A sentence can be true in the model’s memory and false on the vendor’s site today.
    4. Borrowed experience. “When I tested this last month…” from a model that has never opened the tool. This is the most damaging one, because readers forgive a wrong number far more easily than a fake story.
    5. Confident predictions. “This will kill X” is an opinion wearing a fact’s clothes. It needs to be labelled as a bet, not delivered as a finding.

    Google’s documentation update this month makes the same point from the other side: it now tells publishers to fact-check and review AI-generated content before publishing, and warns against manufactured authorship. The compliance bar is no longer “did a human touch it?” It is “can you stand behind what it says?”

    The five-minute pass

    Five minutes is not enough for a 3,000-word research piece. It is enough for a 1,200-word blog post if you work in the right order. I keep the draft on the left and a browser window on the right, and I do not edit prose until the facts are settled.

    1. Mark the claims. Read the draft once and highlight every proper noun, number, date, quote and superlative. Do not fix anything yet. You are making a list, not editing.
    2. Open the primary source. Not a blog quoting a blog. For a study, the paper. For a product claim, the vendor’s own documentation or changelog. For a quote, the transcript or the recording. If the best you can find is a summary of a summary, the claim is not verified, it is repeated.
    3. Click every link. Check that it loads, that the title matches what you said it says, and that the date is current. A link that resolves to the right domain but the wrong page is worse than no link, because it looks like evidence.
    4. Rewrite from the facts, not from the draft. Once a claim is confirmed, write the sentence yourself using the source in front of you. This one habit removes most accidental plagiarism and most of the model’s smug tone at the same time.
    5. Read it aloud. Slowly. The sentence you skated past silently is usually the one that says nothing, or says something you cannot defend.

    A claims table you can keep

    When a piece has more than a handful of facts, I keep a small table beside the draft. It turns “I think this is right” into a yes or no. Mine has four columns, and the last one is the only one that matters.

    Claim type Looks like Check against If unverified
    Number “42% of sites…” The original study or dataset Cut the number or say “many”
    Quote “…” — named person Transcript, video, original interview Paraphrase the idea, not the words
    Product fact “Version 7.2 supports…” Vendor changelog or docs Say “check current docs” or cut it
    Personal story “When I tested this…” Your own notes or screenshots Cut it or run the test for real
    Link “Source: example.com” Open it; read title and date Delete the link and the claim

    The prompt I add before the first draft

    Most cleanup can be avoided by asking the model to expose its uncertainty instead of smoothing it over. I paste this after the brief, before any drafting starts. It does not replace the check. It makes the check shorter.

    Before you write, list the five factual claims this article will depend on. For each one, give the primary source you would use and mark it certain, uncertain, or unknown. Do not invent sources to fill the table.
    
    Then write the draft, and put [VERIFY] after every number, name, date, quote or product detail you are not certain about. Do not round over gaps.
    
    At the end, list the three sentences most likely to be wrong, and why.

    Two things to note. First, the model will still miss claims, especially ones it believes are common knowledge. The [VERIFY] tags are a floor, not a ceiling. Second, a source URL from a model is a lead, not proof. Open it. Read it. If it does not say what the draft says it says, the draft is wrong.

    What “verified” means before I press publish

    My standard is one question: if a reader emailed “where did you get this?”, could I answer in one sentence with a link or a specific experience? If the answer is a shrug, the sentence is not ready.

    That standard changes how the piece is written. A fuzzy number becomes “roughly” or disappears. A prediction becomes “I think” or gets cut. A product detail gets a date stamp: “as of November 2026” tells the reader when the fact was true, which is often more honest than pretending it is permanent.

    It also keeps the publishing workflow boring, which is what you want. The 90-minute post workflow leaves a real slot for this pass instead of pretending the draft is the finish line. The monthly planning afternoon is where the topics come from, so the writing day starts with decisions already made. And repurposing happens only after the facts survive, because a wrong number copied into five formats is five times the problem.

    Pre-publish checklist

    • Every number has a source open in a tab, or it is gone.
    • Every quote is verbatim from a primary source, with the speaker named correctly.
    • Every link loads and matches the claim it supports.
    • No first-person experience that I did not actually have.
    • No prediction stated as a fact; bets are labelled as bets.
    • Product names, versions and prices checked today, with a date stamp.
    • The whole piece read aloud once, start to finish.

    Next time you have a draft that feels finished, set a timer for five minutes and try to disprove it. Open the first number. Click the first link. Read the first quote. If everything survives, publish with confidence. If something does not, you just caught the 74% before your readers did.

  • How to Plan a Month of Content in One Afternoon

    How to Plan a Month of Content in One Afternoon

    Planning a month of content does not take a month. It takes one afternoon, and most of that afternoon is not writing.

    My version produces four titles, four skeletons, four hero images and a calendar with two dates per post. I do it in one block of roughly four hours on a Sunday, and then I stop thinking about content strategy until the next month. The writing still happens on weekdays, in 90-minute blocks, the way I described in how to write a blog post in 90 minutes.

    Planning and writing use different parts of your head. Planning is judgement: what is worth writing, in what order, with what promise. Writing is focus: one section at a time, no decisions. Mixed together, I make bad decisions and shallow drafts in the same hour.

    The short version

    • Block four hours once a month and do not spread planning across the week.
    • Build the topic list from questions you have already answered before inventing new ideas.
    • Leave the afternoon with four titles, four skeletons and four images — not twelve half-drafts.
    • Put two dates on every post: publish day and repurpose day. One date means the post dies after a week.
    • Keep one slot empty for the story that turns up mid-month.

    What the afternoon actually produces

    Not articles. The output of a planning afternoon is a folder of decisions that a tired version of you can execute later without thinking.

    At the end of four hours I want these in one place: four titles I would defend, four skeletons with headings and the asset each one needs, four hero images at final size, and eight calendar entries. Nothing else. If I end the afternoon with two finished drafts and no plan, I have solved this week and borrowed against next week.

    The plan has to survive a bad Tuesday. A skeleton promises my future self that the thinking is done; a half-written draft still forces me to decide what the second half argues.

    Hour one: mine the topic bank you already own

    Most people start this hour by brainstorming, which is the slowest method available. I start with four sources that working already generated:

    1. Search Console queries where I rank between roughly position 5 and 20. Those pages Google already understands but has not promoted, so a more precisely aimed post has a reason to exist. Search Console is the only free tool that tells me what strangers typed to find me; the setup steps are in how to set up Google Search Console properly.

    2. Questions I answered twice in email or chat. The rule is mechanical: if I typed the same explanation twice, it becomes a topic. I keep a plain text file called questions.md and paste the question in as it arrives, not the answer.

    3. Comments and replies that pushed back. Disagreement is a better topic source than praise, because the person is telling me exactly which part of my argument is unclear.

    4. Posts that got replies but no clicks. Usually the title promised one thing and the article delivered another. The fix is a rewrite or a follow-up that answers the question the title implied.

    At the end of the hour I want 8 to 12 candidate lines, each a single sentence that starts with the reader’s problem, not my subject. “Why does my WordPress site show a 500 after I edited functions.php” is a topic. “PHP error handling” is a category.

    One filter: if a topic needs numbers I do not have — revenue, traffic, client results — it goes to a parked list until I actually have them.

    Hour two: four titles and four skeletons, no drafts

    Four is not magic: it is roughly how many posts I can hold in my head, and it leaves room for the unplanned post that always appears. Twelve planned posts become debt.

    I write each title first, because the title is the promise the article has to keep. Two shapes do most of the work:

    • How to X without Y — the reader gets a method and a constraint at the same time.
    • What X actually means (and what to do about it) — good for error messages, policies and anything where the reader is confused before they are curious.

    Then the skeleton: five lines maximum — three to five headings plus the one concrete asset the post needs. I write it in a plain file:

    title: How to X without Y
    promise: reader can do X by the end, with Y still in place
    asset: one table comparing the options
    h2: What X actually needs
    h2: The three steps
    h2: Where it breaks
    h2: Checklist
    links: /write-a-blog-post-in-90-minutes/, /google-search-console-setup/
    repurpose: newsletter + 2 short videos

    Six minutes each, and the article has a shape before I start writing: at 7am on a Wednesday I can read six lines and type, instead of deciding what the piece is about.

    Hour three: batch the parts that do not need your brain

    Hero images, slugs, excerpts, categories and internal links are small decisions that interrupt writing one at a time, so I make them all at once at planning time.

    I generate the four hero images from final titles with my own script, so the card text is already correct and alt text gets written in the same minute, while I remember what the article argues. Slugs and excerpts go into the same file as the skeleton.

    The internal links are the part people skip, and it is the part that compounds. Every planned post gets two links back to older articles and one forward link to a post scheduled later in the month. That forward link only exists if you plan a month at a time, and it keeps the archive getting crawled instead of going quiet after week two.

    Hour four: two dates per post, not one

    Every post gets a publish date and a repurpose date two or three days later. The second date is not optional. Publishing without a repurpose slot is how a good article reaches forty people and disappears.

    The repurpose block is about 45 minutes and it is mechanical: pull the promise for the newsletter, the headings for the video beats, the steps for a forum answer, and one line for social. I wrote the full set of cuts in my content repurposing workflow, and none of it requires a new idea.

    Hour Output Done when
    1 8–12 candidate topics at least four you would actually write
    2 Four titles, four skeletons each has headings and one named asset
    3 Images, slugs, excerpts, link map files exist with final titles and alt text
    4 Eight calendar entries publish and repurpose dates both set

    Where this plan falls apart

    Four failure modes I have hit:

    Planning too many posts. Twelve planned posts feel productive and produce nothing, because each one is a decision I have to re-open. Four keeps a weekly rhythm with slack.

    Planning posts that need data. A topic like “what my first month of hosting actually earned” cannot be planned before the month exists. If a skeleton has a number I cannot fill in today, it is parked.

    Skipping the repurpose date. The article ships, the newsletter does not, and the month looks busy while the audience stays flat.

    Treating the plan as a contract. If a story lands that fits the blog, it takes the empty slot, not a fifth squeezed in on top. The plan removes decisions; it does not forbid judgement.

    The test of a good afternoon

    If I can open the folder on a Wednesday morning, read one skeleton, and be typing within two minutes without deciding anything, the afternoon worked. If I open the folder and start reorganising the list, it did not — that is planning as procrastination.

    The checklist I actually run before closing the file:

    • Four titles, each a promise a reader can repeat out loud.
    • Four skeletons with three to five headings and one named asset.
    • Four hero images generated from the final titles, with alt text.
    • Eight dates: four publishes, four repurposes.
    • One empty slot left deliberately blank.

    Block the next four hours before you close this tab. The month is cheaper to plan than to improvise, and improvising is what makes publishing feel like a second job.

  • My Content Repurposing Workflow: One Post, Five Formats

    My Content Repurposing Workflow: One Post, Five Formats

    One blog post should leave the house as five pieces of content, and the trick is that you never write any of them from scratch. After the article is published I spend about 45 minutes cutting the same material into a newsletter, a short video script, a social post, and a forum answer. Each format pulls from a different layer of the post, which is why the words come out fast and still sound like they were written for that place.

    I covered the writing half of this in How to Write a Blog Post in 90 Minutes. This is the second half: what happens to the file after I hit publish.

    The short version

    • Treat the finished post as raw material, not a monument. Every other format is a cut of it.
    • Each cut takes a different layer: the promise, the skeleton, the proof, or one quotable sentence.
    • Give each format one job. Newsletter for relationship, video for reach, social for distribution, forum for credibility.
    • Do all four cuts in one sitting, while the thinking is still in your head.
    • Five formats is a ceiling, not a quota. Skipping one is a decision, not a failure.

    Start from layers, not from a blank page

    Most repurposing fails because people open a new document and try to write a post about the article. That is a rewrite with extra steps. The faster path is to accept that the post contains everything you need, just packed for one reader in one mood.

    Before I cut anything, I break the article into four layers:

    • The promise — the sentence in the intro that says what the reader gets. It becomes the newsletter subject line and the first line of the social post.
    • The skeleton — the H2 headings. They become the video beats, because they are already an ordered argument.
    • The proof — the steps, commands, settings, or examples. They become the forum answer.
    • The quotable line — the one sentence I would underline. It becomes the opening line of the video and the hook of the social post.

    Once those four things exist, repurposing is selection, not creation. That is the whole trick.

    The five cuts, in order

    This is the shape I work to. The times are what the cutting takes me once the article is live, not the time to write the article.

    Format Cut from Time Job
    Blog post (source) Outline and draft 90 min The asset you own and can rank
    Newsletter Promise and skeleton 10 min Relationship with people who opted in
    Short video script Skeleton and proof 10 min Reach on a platform you do not own
    Social post Quotable line and link 5 min Distribution to people who will never subscribe
    Forum answer Proof section 15 min Credibility where the question is already being asked

    The newsletter: send the promise, not the post

    The mistake here is pasting the whole intro and calling it a newsletter. People subscribed to hear from you, not to get a second copy of a page they can read for free. I send 150 to 250 words: the promise, three bullets from the skeleton, and one link. If I cannot explain the value in three bullets, the article probably has more than one idea in it.

    Subject: [the promise, rewritten as a question or a number]
    
    One sentence on why this matters this week.
    
    - The problem, in the reader's words
    - The step most people skip
    - What to do first
    
    Full post: [link]
    
    That is the whole email. No preamble, no signature block longer than one line.

    Two rules: the subject line describes what is inside, and the bullets are useful on their own. A newsletter that only works if you click is an ad.

    The video script: your headings are already the beats

    A 30 to 45 second script does not need a scriptwriter. It needs three beats, and the article already has them. I take the problem from the intro, the method from the first two or three H2s, and the proof from whatever concrete example the post contains. Then I write it as spoken lines, one idea per line, eight to ten words each.

    The shape that works for me:

    • Beat 1, problem (0–5s): the viewer’s own complaint, said plainly. No greeting, no “in this video”.
    • Beat 2, method (5–25s): the two or three steps, in order, with the reason attached to each one.
    • Beat 3, proof (25–35s): the specific result from the post — the log line, the setting, the thing that changed.
    • Close (35–40s): one action, one place to go.

    If a beat needs a diagram to make sense, it belongs in the post.

    The social post: lift one paragraph and stop

    Social is not a summary. A summary of a long article is boring in every format. What travels is one opinionated sentence, stated without hedging, followed by enough context to be useful and a link for the people who want the rest.

    My routine: pick the most contrary sentence in the article, paste it first, delete every word that softens it, then add one line of value and the link. On X the limit is 272 weighted characters and a link counts as 23, so the real text budget is around 250. No hashtags there; they read as spam. On LinkedIn or Facebook I add a line break before the link so the post does not end in a wall of URL.

    What I never do is announce that I wrote a blog post. Nobody owes me a click because I was productive. The sentence has to earn it.

    The forum answer: answer first, link second

    Forums are where the same question gets asked every week, and the easiest place to be useful if you resist advertising. I find a thread where I actually know the answer, then write the answer as if my site did not exist. Only at the end, if there is more detail than a forum reply should carry, do I mention that I wrote it up in full.

    Practical version: lead with the direct answer in the first two lines, give the steps in the order you would do them, and name the one thing that usually goes wrong. Strip the brand voice completely. A forum reader can smell a marketing line in the first sentence, and then the answer is dead no matter how correct it is.

    What I skip, and why five is a ceiling

    Not every post deserves five cuts. Some topics are useful and quiet; they earn a newsletter mention and nothing else. Others are already saturated on video and do better as a written reference. I decide based on where the question lives, not on a content calendar that says every post gets five assets.

    Things I skip by default: Instagram carousels, because the design time eats the writing time; podcast appearances, because they are a different skill; and old forum threads. Things I only do when the post is genuinely strong: a second video, a longer LinkedIn version, and a follow-up article that goes deeper on one section.

    Two weeks after publishing, I check which pieces earned impressions and which earned nothing, then put the next month’s effort where the numbers actually moved. That is a Google Search Console habit, and I wrote the setup for it in How to Set Up Google Search Console Properly. Repurposing without that check is just making more things.

    The 45-minute checklist

    1. Copy the promise sentence and the H2 list into one scratch file.
    2. Write the newsletter from those two things only. No new ideas.
    3. Turn the H2 list into three video beats and one closing action.
    4. Lift one paragraph for social, cut it until it fits, add the link last.
    5. Find one real question and answer it with the proof section, brand voice off.
    6. Schedule all four and log them in a single row of a spreadsheet: date, post, formats, links.
    7. Set a reminder for two weeks out to check impressions before deciding what to repeat.

    The post is not finished when it is published. It is finished when the same idea has met five different readers in the five places they already spend time, and none of them had to be told it was the same idea.

  • How to Write a Blog Post in 90 Minutes

    How to Write a Blog Post in 90 Minutes

    A blog post takes me 90 minutes from blank page to published. The trick is not typing speed — it is doing one job at a time instead of all four at once. Research, outline, draft and edit each need a different kind of attention, and every switch between them costs you the thread you were holding. Ninety minutes is enough for a 900 to 1,200 word post that answers one question properly. It is not enough for a guide that tries to answer ten.

    The short version

    • Five blocks: 15 minutes research, 10 outline, 35 draft, 20 edit, 10 publish.
    • Research ends when the timer ends — three sources, then start writing.
    • The outline is a list of questions a reader would ask, not a list of headings.
    • The first draft is allowed to be bad. Fixing it is a separate job done later.
    • Editing is three passes in a fixed order: structure, sentences, facts.

    The five blocks, with the one rule that keeps each one from eating the next:

    Block Minutes The job The rule
    0:00 – 0:15 15 Collect what one question needs Three sources, then stop
    0:15 – 0:25 10 Turn the question into sub-questions If you cannot answer it in one line, cut it
    0:25 – 1:00 35 Write the whole thing, badly No editing, no new research
    1:00 – 1:20 20 Structure, sentences, facts Three passes, in that order
    1:20 – 1:30 10 Image, links, slug, live check Done beats perfect

    Minutes 0–15: research with a stop rule

    Open a scratch document and write the post’s question the way a reader would type it into a search box. Not “content marketing tips” — “how long should a blog post be before people stop reading”. One question. If you cannot get it down to one, you are about to write two posts badly instead of one post well.

    Then collect three things and nothing else:

    • What you already know from doing the work. For a migration post, that is the step order and the one step you always forget.
    • One authoritative source — the official documentation, the changelog, the pricing page. Copy the exact sentence and the URL into your scratch document so you never have to find it twice.
    • One real example: a screenshot you took, a command output, a before-and-after you can describe from memory.

    If a fact is missing when the timer rings, do not go looking for it. Type a placeholder — [CHECK VERSION] — and keep moving; you will verify it in the edit block or cut the sentence. Research is where time disappears, because every tab feels useful and usually is not. The post needs three things, and you already have two.

    Minutes 15–25: turn the outline into questions

    Most outlines are a list of nouns: “Introduction, Why it matters, Best practices, Conclusion.” That produces a post that says nothing, because “Best practices” does not tell you what to write underneath it. Replace each heading with the question a reader would actually ask, then write the one-line answer. If there is no one-line answer, you have found the section that will take an hour — cut it or split it into its own post.

    This is the outline I type, every time:

    TITLE (the reader's question, in their words)
    
    1. What do I need before I start?
       -> one line:
    
    2. What does it look like when it is done?
       -> one line:
    
    3. What goes wrong, and what do I do about it?
       -> one line:
    
    4. What is the checklist?
       -> one line:
    
    STILL OWED: [ ] screenshot  [ ] version number  [ ] link

    Four questions is a 900-word post; six is about 1,200. More than that is a series, and it will read like a list of everything you know instead of an answer to the title.

    Minutes 25–60: draft straight through

    The draft block has one rule: do not stop to fix anything. Not the sentence you just wrote, not the fact you are unsure of, not the comma you can feel is wrong. Every stop is a context switch, and the switch back costs more than the fix saved.

    Practical version of that rule:

    • Write the opening as three blunt sentences that answer the question, then leave it alone until the edit block. Do not polish it now, and do not write a warm-up paragraph first.
    • Fill in each outline question with paragraphs. When a paragraph is not working, type the sentence you are trying to say in capitals and move to the next one.
    • No new research. If you need a fact you did not collect, write a placeholder.
    • No formatting, no image hunting, no plugin fiddling. Those belong to the publish block.

    A thousand words in 35 minutes is under thirty words a minute — slower than you type. The time goes into deciding what the next sentence says, which is the actual work.

    Minutes 60–80: edit in three passes

    One long read-through finds typos and nothing else, because you cannot judge structure and commas at the same time. Three short passes, in this order:

    Pass one, structure. Read only the subheadings and the first sentence under each one. Does the post answer the question in the title? Is any section saying the same thing as the one above it? Cut it, even if you like it. Move the strongest section to the top.

    Pass two, sentences. Read each paragraph once, out loud in your head. Split anything over about thirty words. Delete the phrases that only exist to fill space — “in today’s fast-paced world”, “it is important to note that”, “let’s dive in”.

    Pass three, facts. Every number, date, product name and version gets checked against the source you saved, or deleted. This is where credibility is decided, and it is the pass people skip when they are running late. If you cannot verify a claim, say plainly that you have not tested it. A shorter post with true sentences is worth more than a long one with one invented figure.

    Minutes 80–90: publish, then stop

    The publish block is mechanical, so it gets ten minutes:

    • Slug: the question, shortened, words separated by hyphens. Set it before you publish — I cover the permalink settings in the WordPress settings I change on every new site.
    • Image: resize it before upload, and write alt text that describes what is in the picture rather than repeating the title. Oversized uploads are one of the reasons WordPress sites end up slow.
    • Internal links: two or three, each pointing at a post that answers the reader’s next question. Click every one before publishing.
    • Excerpt or meta description: two sentences that answer the question again, in different words from the title.
    • Check: open the live URL. Does it load, does the title match, does the image appear, does the page fit a phone screen?

    Then stop. The temptation is to keep tweaking, and that is how a 90-minute post becomes an afternoon. Tomorrow’s post is the next thing, not this one.

    Where the 90 minutes actually goes wrong

    Four failures account for almost all of it. Research expands to fill the time available, so set an alarm. Editing starts during the draft, so the draft never gets finished. The outline is written as nouns instead of questions, so the writing block has nothing to aim at. And the post tries to be comprehensive, when comprehensive is three posts and the reader came for one answer.

    One honest exception: a post that depends on data you must gather — dashboard screenshots, a test to run, numbers to collect — is not a 90-minute post. Give it three hours or split it; forcing it into the block means publishing half the evidence.

    Start smaller than it sounds: take the question people ask you most, set a timer for 15 minutes, and do only the research block. When it rings, write the four questions and stop if you want. And if you are choosing what to write next, the fastest source of real questions is the Performance report in Google Search Console: the queries your pages already appear for are the questions you are closest to answering well.

  • How to Set Up Google Search Console Properly

    How to Set Up Google Search Console Properly

    Search Console is the only place Google tells you what it actually did with your site — which pages it indexed, which queries showed you, and whether a human reviewer took action against you. The setup that matters is short: one Domain property verified by DNS, one sitemap submitted, alerts going to an address you read, and a ten-minute check once a week. Everything else in the interface can wait until you have traffic worth analysing.

    The short version

    • Create a Domain property, not a URL-prefix property. One property covers http, https, www and every subdomain you own.
    • Verify with a DNS TXT record and leave the record in place. Google re-checks it, and deleting it costs you access.
    • Submit the sitemap WordPress actually generates — /wp-sitemap.xml on a stock install, /sitemap_index.xml with Yoast or Rank Math.
    • Three reports are worth opening: Performance, Page Indexing, and Security & Manual Actions.
    • Data lags a couple of days and new pages take weeks to be crawled. Check weekly, not hourly.

    Pick the Domain property, not the URL prefix

    Google offers two kinds of property. A URL-prefix property is tied to exactly one protocol, one hostname and one path: https://example.com does not include https://www.example.com, and neither includes http://. A Domain property covers all of them, plus every subdomain, because ownership is proved at the DNS level for the bare domain.

    Property type Covers Choose it when
    Domain Every protocol and subdomain: example.com, www., shop., http and https You control the DNS, which is the normal case for a site you own
    URL prefix One scheme, host and path, exactly as typed You cannot edit DNS, or you are monitoring a section of a domain you do not own

    In the add-property screen, choose Domain, type the bare domain without https:// and without a trailing slash, and continue. Do not also create a URL-prefix property “just in case”: the two keep separate data and separate emails, so you end up watching two half-empty dashboards instead of one complete one.

    If your DNS lives at Cloudflare, the record goes in the same place as everything else — the Cloudflare setup walkthrough covers the settings that break sites.

    Verify with a DNS record and leave it alone

    Google gives you a TXT value that looks like this. Copy it exactly, including the google-site-verification= prefix.

    Type:  TXT
    Name:  @            (the bare domain)
    Value: google-site-verification=AbC123dEf456...
    TTL:   Auto or 3600

    In cPanel that is Zone Editor → Manage → Add Record → TXT. In Cloudflare it is DNS → Records → Add record, type TXT, name @. Save, wait a few minutes, then press Verify back in Search Console. When verification fails it is almost always one of three things: the record went on www instead of the root, the value was pasted with a smart quote or a line break from an email, or the DNS change has not propagated yet. Give it an hour before you start changing things.

    The mistake I see most often is cleanup. Six months later someone tidies the DNS zone, deletes the “random TXT record”, and the property breaks. Google re-checks ownership periodically; when the record is gone, access goes with it. If you move nameservers, recreate the TXT record at the new provider before you flip the delegation.

    The HTML-tag and Google Analytics verification methods are fine, but both are fragile in WordPress: a theme change can strip the tag from the header, and a plugin that removes the analytics snippet takes the property with it. DNS survives theme updates, plugin audits and migrations, which is why it is the one I use.

    Submit the sitemap WordPress already builds

    You do not need a sitemap plugin to start. Since WordPress 5.5 there is a built-in sitemap at /wp-sitemap.xml; if you run Yoast or Rank Math it is /sitemap_index.xml instead. Open the URL in a browser first — you should see XML, not a 404 and not a redirect to the homepage.

    Then go to Indexing → Sitemaps, paste the full URL, and submit. The status column should read Success within a day or two. Couldn’t fetch usually means the URL redirects, robots.txt blocks it, or the response is HTML; fix that before submitting again, because repeated failures do not speed anything up.

    Two more things worth doing once: add a Sitemap: line to robots.txt pointing at the same file, and open the sitemap index itself. A stock WordPress sitemap splits into posts, pages, categories and users, and if one of those child sitemaps errors the parent reports it as a warning. A sitemap is a hint, not a command: it tells Google where the URLs are and nothing more. Submitting the same file every day does not push a page into the index.

    Read three reports and ignore the rest

    Performance is the report people open first and the one most often misread. It shows queries, pages, countries and devices for a chosen date range, three months by default. Two cautions: the data lags by a couple of days, so yesterday always looks like a cliff, and “average position” is an average across every impression, not the position you see when you search from your own desk. Filter by page to see which queries actually feed one article.

    Page Indexing is the honest one. It lists every URL Google knows about and why it is or is not indexed. The reasons are not equally urgent:

    • Discovered – currently not indexed: Google knows the URL exists and has not crawled it yet. On a small site this is often just queue time. Internal links from pages that are already indexed help more than anything else.
    • Crawled – currently not indexed: Google fetched the page and chose not to keep it. That is usually thin or near-duplicate content. Improve the page instead of requesting indexing every morning.
    • Excluded by noindex: a setting, not a bug. Check the page source for the tag, then find the plugin or theme option that adds it.
    • Alternate page with proper canonical tag: normal on paginated and tagged archives. Not an error.
    • Duplicate, Google chose a different canonical: look at your canonical tags; on WordPress the usual culprits are archives and query strings.

    Security & Manual Actions holds two sections that look similar and mean different things. A manual action is a person at Google deciding your site breaks the spam policies; a security issue is Google believing your site is hacked or serving malware. Both are emailed to verified owners and both are urgent. If either list is not empty, stop reading about search optimisation and deal with it — the five-minute infection check is the fastest first pass, and the recovery walkthrough is the order I would work in.

    The weekly ten-minute check

    Search Console emails you about manual actions and security issues. It will not email you when a page quietly drops out of the index, so put a recurring slot in the calendar and work through the same five items:

    1. Page Indexing — did the “not indexed” count jump by more than a few pages? Open the reasons and look for a new one.
    2. Performance — compare the last 28 days with the previous 28. Find one query or page that lost clicks and write down why.
    3. Security & Manual Actions — confirm both sections are empty.
    4. Sitemaps — status Success, with a recent “last read” date.
    5. Settings → Users and permissions — once a quarter is enough; remove accounts you no longer control.

    That is the whole routine. The reason to keep it boring is that the signals that matter — a page dropping out of the index, a query collapsing, a manual action — are visible within a week if you look, and invisible for months if you do not.

    If you only do one thing this week: create the Domain property, paste the TXT record at your DNS provider, submit /wp-sitemap.xml, and put the ten-minute check on your calendar. Then go back to writing. Search Console rewards patience far more than it rewards fiddling, and once the property is verified and the sitemap is in, the only thing left to improve is the site itself.

  • Email Deliverability Basics for Small Sites: SPF, DKIM, DMARC

    Email Deliverability Basics for Small Sites: SPF, DKIM, DMARC

    Three DNS records decide whether your email reaches the inbox: SPF lists the servers allowed to send as your domain, DKIM signs every message so the receiver can tell it was not altered on the way, and DMARC tells the receiver what to do when either check fails. Get one wrong and small sites do not see an error — invoices, password resets and replies just land in spam or vanish. Here is the whole setup, in the order I do it.

    The short version

    • SPF is one TXT record per domain listing the servers allowed to send. Two SPF records is not redundancy, it is a broken domain.
    • DKIM needs a private key on the mail server and a public key in DNS at selector._domainkey.yourdomain. A DNS row without a signing server is decoration.
    • DMARC is a TXT record at _dmarc that ties the From address to SPF and DKIM and sets a policy. Start at p=none, read the reports, then tighten.
    • Test by sending to a real Gmail address and reading the headers, not a checker that only reads DNS.
    • If WordPress sends through PHP mail(), fix that before touching DNS. Hosting IPs are the ones receivers distrust.

    What each record actually does

    They fail separately. SPF reads the envelope sender — the Return-Path, which your customer never sees. DKIM reads a cryptographic signature over the headers and body. DMARC reads the From header your customer does see, and asks whether SPF or DKIM passed for that domain.

    Record Lives at What it proves Usual mistake
    SPF (TXT) @ Which servers may send as you Two SPF records, or over ten lookups
    DKIM (TXT) selector._domainkey The message was signed by your key Key in DNS, server never signs
    DMARC (TXT) _dmarc What to do on failure, and where reports go p=reject before anything was verified

    Where the records go, and what is live now

    The records belong in the DNS zone, where the A record lives. In cPanel that is Zone Editor; in Cloudflare it is DNS → Records → Add record, type TXT, TTL Auto. If your nameservers sit at the registrar while the site sits on hosting, the mail records still go at the registrar: email DNS follows the nameservers, not the web server. On Cloudflare, mail records must be DNS only, never proxied — the same rule as in the Cloudflare setup guide.

    Look at what is published before you change anything:

    # SPF
    dig +short TXT example.com | grep -i spf
    
    # DKIM — use the selector your provider gives you
    dig +short TXT default._domainkey.example.com
    
    # DMARC
    dig +short TXT _dmarc.example.com
    
    # Windows, no dig:
    nslookup -type=TXT example.com

    Two lines of v=spf1 means the record exists twice — worse than missing.

    SPF: one record, ten lookups, and the ending that matters

    An SPF record lists who may send, plus a qualifier for everyone else. For a site sending from its own hosting and one outside provider:

    v=spf1 a mx include:_spf.provider.example ip4:203.0.113.10 -all

    Read it left to right: a allows the server behind your A record, mx your incoming mail hosts, each include: another domain’s SPF list, ip4: a single address. Replace the example IP with yours.

    Two rules break more SPF records than anything else. First: one SPF record per domain name. If your host adds one and a marketing tool tells you to add another, merge them into a single line. Second: the ten-lookup limit in the SPF specification. Every include, a, mx, ptr and exists counts toward it, and going over returns a permanent error that receivers read as “no valid SPF”. MXToolbox and dmarcian both report the lookup count; check it before adding another include.

    End with -all once you are confident, ~all while you are still finding senders you forgot. Soft fail is a test mode; it protects nothing.

    DKIM: the key has to exist on the server

    DKIM is the record people copy correctly and still get wrong, because the DNS entry is only half of it. The public key goes in DNS; the private key stays on the machine that sends your mail, and that machine must be configured to sign. Publish the record without enabling signing and every message fails DKIM while you edit DNS for nothing.

    The right order: enable DKIM in your hosting panel first — in cPanel it is Email Deliverability — then copy the exact record it generates. Do not generate your own key pair and hope the panel picks it up. The selector is part of the record name and your provider chooses it; default, mail and google are common.

    If your DNS host splits a long TXT value into quoted chunks, that is normal; resolvers join them again. Breaking the value yourself is not.

    DMARC: monitor first, enforce later

    DMARC is the policy layer. Start with this:

    v=DMARC1; p=none; rua=mailto:dmarc@example.com; adkim=r; aspf=r; pct=100

    p=none means “deliver as normal, just tell me what happened”. It protects nothing by itself, and that is exactly why it is the right first step: your reports will show every legitimate sender you forgot, including the contact form on your own site. Point rua at a mailbox you actually read, or at a reporting service — the reports arrive as XML and nobody reads them by hand.

    After a few weeks of clean reports, move to p=quarantine and then p=reject, in steps with pct if you send volume. Alignment matters more than it looks: DMARC passes only if SPF or DKIM passes and the domain in that result matches the From domain. A host that sends as yourdomain.com but authenticates as its own domain passes SPF and still fails DMARC — the most common cause of “all my records are green but DMARC fails”.

    Test from a real mailbox, then fix the sending path

    Send a message from the site’s contact form and one from webmail to a Gmail address you own. Open it, use the three-dot menu, choose Show original, and read the authentication results near the top. You want three passes: SPF, DKIM with your selector, DMARC. Send the same message to an Outlook address too — Microsoft’s filters weigh reputation differently and will catch a problem Gmail forgives.

    If SPF passes and DKIM fails, the server is not signing or the selector does not match. If both pass and the mail still lands in spam, the problem is reputation, not authentication: a domain that has never sent mail looks exactly like a spammer’s first domain. Send consistently, keep volume modest at the start, and make sure every message is one somebody asked for.

    The WordPress part trips up more small sites than DNS does. PHP mail() sends from the web server’s IP, shared with every other site on that host — and that is the reputation your mail inherits. Configure SMTP to your real mailbox or a transactional provider (WP Mail SMTP and Post SMTP both do it), and check that outbound port 25 is not blocked, which many VPS providers do by default; port 587 exists for that reason. If the host’s IP reputation is the problem, no DNS record saves you, which is worth weighing when you compare cheap hosting against good hosting.

    Forwarding is the last trap: a forwarder re-sends with a new envelope sender, so SPF fails at the destination while DKIM usually survives. And when you change hosts, do not touch MX and SPF in the same hour — the migration checklist covers the sequencing so mail does not bounce.

    The checklist before I call it done

    • Exactly one SPF record, ending in -all, under ten lookups, covering every tool that sends for you.
    • DKIM signing enabled on the server, selector in DNS matching the signature, confirmed by a real message.
    • DMARC at _dmarc with p=none and a rua mailbox someone reads.
    • Test messages to Gmail and Outlook, all three checks passing in the headers.
    • WordPress sending over SMTP on port 587, not PHP mail().
    • Reverse DNS set for the sending IP if you run your own server.

    Do it in that order today: SPF, DKIM, DMARC at p=none, then one test message to your own Gmail and read the headers. It tells you more than any dashboard, because it measures what your customer receives.

  • How to Choose a Domain Name You Won’t Regret in 3 Years

    How to Choose a Domain Name You Won’t Regret in 3 Years

    The domain name is the only part of a website you cannot quietly swap later. Hosting, theme, even the platform can change in an afternoon. A domain carries every inbound link, every email address and every mention anyone ever wrote about you. Pick it the way you would pick a business name — slowly, and with year three in mind.

    My rule is simple. A name is safe to buy when it passes three tests: someone can hear it once and type it correctly, the renewal price does not jump after the first year, and no trademark or established business is already sitting on it. Everything else is taste.

    The short version

    • Say it out loud to a real person before you check availability. If they ask you to repeat it, the name is wrong.
    • Take the .com unless you are deliberately local. Local is a strategy, not a consolation prize.
    • Judge the price at renewal, not the first-year promo. That is where registrars earn.
    • Register the domain in your own account, with your own email, separate from your hosting.
    • Run four checks before you pay: trademark, past use, social handles, and who controls the DNS.

    The phone test comes before the availability check

    Most people open a registrar first and start typing names into a search box. That is backwards. The registrar will tell you what is available; it will never tell you what is usable.

    Read the name to someone over the phone, or send it in a voice note. If they ask you to spell it, or read it back wrong, you have your answer before you spend anything. A name that needs a sentence of explanation is a name you will explain for years.

    What I avoid, in the order it usually bites:

    • Doubled letters at the join. Two words that end and start with the same letter merge when spoken.
    • Numbers. “4” or “2” forces a digit-or-word decision on every visitor.
    • Hyphens. People drop them, so assume half your visitors type the unhyphenated version.
    • Plurals and possessives. If the name needs an apostrophe or a final “s”, people will leave it out.

    Two words, three syllables, no explanation. If you can say it in a noisy restaurant and get it typed back correctly, it survives a business card and a customer telling a friend.

    Brand or keyword: decide what the name has to do in three years

    Keyword domains describe what you sell. Something like klplumber24.com tells a stranger exactly what you do and roughly where. That is a real advantage for a local service business that will still be doing the same work later.

    The cost is that the name becomes a ceiling: add a second service, move city, or sell the business, and the name works against you.

    Brandable names — a short invented or semi-invented word — cost you clarity up front and repay it later. You can add services, change cities, launch products under the same roof. My compromise for most small sites: a short brandable root plus a plain-language tagline in the site title.

    One honest exception: if the business is local and service-specific with no ambition to change, the keyword name is the better marketing decision. Buy it knowing it is a permanent label.

    The extension is a trust decision, not a fashion decision

    Use .com when you can get it. Not because it ranks better, but because it is the default in people’s heads. When someone half-remembers your name, they type .com first.

    Country extensions — .com.my here in Malaysia, .co.uk, .com.au — buy local credibility. If your customers are all in one country and you compete with local businesses, that signal is worth the extra cost and the registration rules.

    New generic extensions are fine and often cheap, but they carry a spelling tax and a trust question. Some corporate mail filters treat unfamiliar extensions with suspicion. I use them for campaign sites, not for the address that goes on the invoice.

    The renewal price is the real price

    Registrars advertise the first year and earn on the rest. Before you check out, find the renewal price for the same domain and multiply it by three. If the promo year is cheap and the renewal is not, you have not found a bargain — you have found a discount on the least important year. It is the same trick as cheap hosting that looks fine until you read the fine print.

    Cost line What to check What it costs if you skip it
    Renewal price The price in year two, not the promo A bill you did not budget for, every year
    Transfer and redemption Cost to move out, cost to recover a lapse Redemption fees are punitive by design
    Privacy and email Is WHOIS privacy included or an upsell? Your home address in a public database

    The redemption line matters more than it looks. Let a domain expire and you do not simply re-buy it at the normal price — you pay a recovery fee, and sometimes you lose the name to an auction while you decide. Which is why auto-renew and a card on file are part of the purchase.

    Four checks before you pay

    This takes about twenty minutes and has saved me from names I would have regretted.

    1. Trademark. Search the national register — MyIPO in Malaysia — and a plain web search for the name plus your industry. You are looking for an existing business in the same field, not for any use of the words anywhere. A trademark dispute is decided by who registered first and in what class, not by who bought the domain first.

    2. Past life. Check the Wayback Machine and search the name. A domain that previously hosted spam, a pharmacy or a link farm carries that history. Search engines remember longer than registrars do, and cleaning it up is slow, uncertain work. A cheap expired domain with backlinks is usually cheap for a reason.

    3. Handles. Look up the same name on the social platforms you will actually use. A name that is free as a domain but taken everywhere else means every profile needs a variant, and you spend years explaining which one is you.

    4. DNS and ownership. Confirm you get full DNS control and that the registrant will be you. If a host or a developer offers to register it “for you”, decline politely. The account holder is the owner in every dispute that matters.

    whois yourname.com | grep -iE "registrar|creation|expiry"
    dig +short NS yourname.com
    dig +short MX yourname.com

    Read the output, not just the exit status. A creation date from years ago on a name you thought was free means it was dropped and re-registered. Name servers pointed somewhere unexpected mean it was in use recently.

    Register it for the person you will be in three years

    Buy in your own registrar account, using an email address on a different domain. If your only recovery address is the domain that just lapsed, you cannot receive the recovery message. Turn on two-factor authentication too — domain theft happens through registrar logins, not DNS attacks.

    Keep the domain and the hosting in separate accounts, ideally separate companies. When you move a site to a new host, the domain should stay exactly where it is and only the DNS records should change. I keep DNS at Cloudflare and treat the registrar as a billing relationship, so a migration is a nameserver edit rather than a transfer.

    Register for several years when the price is locked, and keep auto-renew on regardless. The renewal is a rounding error next to what losing the name would cost.

    Before you click buy

    • Say the name to one real person. Fix it if they hesitate.
    • Search the trademark register and the name itself.
    • Check the year-two renewal price and the transfer fee.
    • Check the social handles you will actually use.
    • Confirm the registrant will be your own account, with 2FA.
    • Set auto-renew and a calendar reminder thirty days before expiry.

    If you do only one thing from this article: check the renewal price before the first-year price. The name you keep for three years is the one you can afford to keep, and the best time to find that out is before you print it on anything.

  • Anatomy of a Hacked WordPress: What Attackers Leave Behind

    Anatomy of a Hacked WordPress: What Attackers Leave Behind

    When a WordPress site gets hacked, the visible symptom is usually the last thing to arrive. The real footprint is a handful of files, a couple of database rows, and one user account you did not create. If you know the six places to look, you can tell a hacked site from a broken one in about ten minutes — and you can do it without installing a security plugin.

    Clean up a few of these and the pattern repeats: the same four directories, the same three tables, the same two persistence tricks. Attackers reuse what already works.

    The short version

    • Most infections live in wp-content/plugins, wp-content/mu-plugins, wp-content/uploads, and the active theme’s functions.php.
    • mu-plugins is the favourite hiding place: files there load automatically and never appear in the Plugins screen.
    • Core files usually get one or two lines changed, so run wp core verify-checksums before you trust anything else on the site.
    • Check users and cron next. A rogue administrator or a scheduled event calling an outside URL is persistence, not just a symptom.
    • Cleaning files without finding the entry point means you will be back here in three weeks.

    Start with the file system, not the scanner

    A scanner matches signatures; a hand-edited theme file that includes a remote script usually passes. These three structural checks are faster and do not depend on a signature database being current.

    Run them from the WordPress root over SSH, or through your host’s file manager terminal.

    # PHP files inside uploads — there should be none
    find wp-content/uploads -type f -name '*.php'
    
    # Everything modified in the last 30 days, newest last
    find wp-content -type f -name '*.php' -mtime -30 -printf '%TY-%Tm-%Td %TH:%TM  %p\n' | sort
    
    # Any PHP file that is world-writable, a common attacker trick
    find . -type f -name '*.php' -perm -o+w

    The first command should print nothing. Uploads is for images and documents; a .php file sitting next to your photos was put there by someone, and it is a backdoor.

    The second command is a timeline: compare it with the dates you actually ran an update.

    mu-plugins: the folder that loads and hides

    This is the trick I see most often, and the one most site owners have never heard of. Anything in wp-content/mu-plugins loads on every request with no activation step and no entry in the Plugins list. Good hosts use it for a cache loader or an object-cache drop-in. Attackers use it for a single line of PHP.

    ls -la wp-content/mu-plugins/

    If you do not recognise a filename, read the first twenty lines. A real mu-plugin has a comment header and actual code. A malicious one is often a lone include of a file elsewhere, or one very long base64 string passed to eval, gzinflate or create_function. There is no legitimate reason for a plugin to decode a string and execute it.

    Do not just delete the file. Note its name and look for the same string in the theme and in wp-config.php — a mu-plugin loader is usually called from two or three places so that removing one copy does nothing.

    Fake plugins with real-looking headers

    A dropped plugin differs from a real one in ways that survive a quick glance. It has a proper header block — Plugin Name, Version, Author — because that header is what makes the file a plugin at all. What it lacks is history: no changelog, no readme in the format WordPress expects, and an author URL on a recently registered domain.

    wp plugin list --fields=name,status,version,update
    ls -la wp-content/plugins/

    Compare the two: the command lists what WordPress knows about, the directory shows what is on disk. A folder with no matching row was never installed through the admin — someone dropped it there. A plugin whose files all share one timestamp while the header claims an old version is not one anyone maintained.

    Core files: one line is enough

    Injections into core are small on purpose. A single line at the top of wp-settings.php or index.php, often wrapped in a check for a specific user agent or a cookie, is enough to serve a payload to Googlebot and nothing to you.

    WordPress ships with checksums for every file in every release, and WP-CLI can compare your installation against them:

    wp core verify-checksums

    Clean output means no core file has been edited; warnings name the files that differ. Do not fix it by unpacking a fresh copy over a live site — read the diff first, then replace that one file from the official zip.

    The database footprint: users, options, cron

    Files get you access. The database is what keeps it. Three tables carry almost all of the persistence: wp_users, wp_options, and wp_usermeta with the serialised cron array.

    wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
    wp cron event list --fields=hook,next_run,recurrence
    wp option get siteurl
    wp option get home

    On the user list, look at the registration date, not the name. Attackers pick names that blend in — wp_support, admin_backup, editor2 — but they appear on a date when you were not working. Every administrator should be a person you can name.

    On the cron list, a hook with no plugin behind it is the tell. WordPress will happily schedule an event pointing at a function that a malicious mu-plugin defined; the event itself looks ordinary. If you cannot trace a hook to a plugin you installed, treat it as hostile.

    For options, siteurl and home should be identical and should be the URL you expect. A mismatch is the classic way to keep an admin logged in on one hostname while sending visitors elsewhere.

    Redirects and injected sitemaps

    Some infections never add a PHP file. They edit .htaccess and add a rule that fires only for mobile user agents, or only for traffic arriving from a search engine. The page looks completely normal to you and redirects every visitor from Google to a casino.

    Read .htaccess from the top. Rules inside <IfModule mod_rewrite.c> that reference HTTP_USER_AGENT, HTTP_REFERER, or a specific country code are not part of WordPress — the stock rules only rewrite to index.php.

    The sitemap is the other quiet channel. Check /sitemap.xml and /wp-sitemap.xml against your post list. URLs you never published mean the injection is generating pages on the fly, usually only for a crawler carrying a Google referrer. That is exactly what happened when my own site was hit with Japanese keyword spam — the recovery steps are here.

    Where to look, at a glance

    Where it hides What you see How to confirm
    uploads a .php file beside images find wp-content/uploads -name '*.php'
    mu-plugins unrecognised file, no Plugins entry ls -la wp-content/mu-plugins/
    plugins folder with no matching wp plugin list row diff the list against the directory
    core one edited line in a core file wp core verify-checksums
    users admin registered on a quiet day wp user list --role=administrator
    cron hook that belongs to no plugin wp cron event list
    .htaccess / sitemap redirects or URLs you never published read the rules, compare sitemap to posts

    Removing files is half the job

    The other half is the entry point: an unupdated plugin, a reused password, or a theme that bundles “nulled” premium code. Locking down wp-login closes the front door; it does not remove a key already copied.

    So after you clean the files: change every administrator password, change the database password, regenerate the WordPress salts in wp-config.php (which invalidates every existing login cookie), and revoke any FTP or SSH key you do not recognise. Then check your hosting account for extra FTP users — a leftover FTP account survives a full WordPress reinstall.

    Be careful with the restore: if the last backup already contained the backdoor, restoring it puts you back where you started. Compare files against a known-good copy, not a snapshot from an infected week.

    Then read the access log for the day the files changed — the request that wrote them is in there, with the IP and the path it used. That is how you find the entry point instead of guessing at it.

    What to do today

    Run the three file-system commands and wp core verify-checksums, and save the output to a file on your own machine. That file is your baseline: next time something feels wrong, run the same commands and compare. Ten minutes now beats an afternoon of wondering.

  • How to Read Your Access Logs and Spot a Brute-Force Attack

    How to Read Your Access Logs and Spot a Brute-Force Attack

    A brute-force run against WordPress looks like the same request to wp-login.php repeating hundreds of times from one address, usually with a scripted user agent and a response that never changes size. You do not need a security plugin to see it. You need the raw access log, three shell commands, and about five minutes.

    I open the access log before any dashboard, because a dashboard tells you what it chose to record and the log tells you what the server received.

    The short version

    • Log fields are positional. Field 1 is the IP, field 7 is the requested path, field 9 is the status code, field 10 is bytes sent.
    • One line means nothing. Two hundred of the same line from one IP inside an hour means everything.
    • POST /wp-login.php returning 200 is a rejected attempt. Returning 302 means somebody got through.
    • Count before you block, and prefer hardening the endpoint over banning the address — botnets rotate IPs faster than you can write firewall rules.
    • Read the rotated files too. Attacks straddle midnight, and the .gz from yesterday holds the half you are missing.

    One line, read left to right

    Apache and nginx both default to the combined log format. Fields are separated by spaces, and the quoted parts can contain spaces of their own, which is why the request is wrapped in quotes as a single unit.

    203.0.113.45 - - [01/Nov/2026:04:12:33 +0800] "POST /wp-login.php HTTP/1.1" 200 4312 "-" "python-requests/2.31.0"

    Read it as a sentence. At 04:12 in the morning, an address that sent no referrer and identified itself as a Python script posted to the login form, and the server answered with a 200 and 4,312 bytes. Nothing about that line belongs to a person.

    Field In the line above Why it matters
    1 203.0.113.45 Who. Group by this first.
    4–5 01/Nov/2026:04:12:33 When. Real customers sleep.
    6–8 POST /wp-login.php What they asked for. Method plus path is the whole story.
    9 200 Result. 200 means rejected, 302 means logged in.
    10 4312 Bytes sent. Hundreds of identical counts means nothing changed.
    12 python-requests/2.31.0 Client. Browsers do not introduce themselves as Python.

    The IP above is from the documentation range, not a live log.

    Where the file actually lives

    This is the step most people give up at.

    cPanel: /home/<user>/access-logs/<domain.com>. It is a symlink to the real domlogs file, and it is readable by your account, which is all you need. The error log sits in ~/logs/. If you are in File Manager rather than SSH, turn on hidden files, because the directory is easy to miss next to the public_html tree.

    Plesk: /var/www/vhosts/system/<domain>/logs/. Inside it you get access_log, access_ssl_log, proxy_access_log and the matching error logs. On Plesk, nginx sits in front of Apache and sees the real client IP, so proxy_access_log is the file you want when access_log shows a loopback address.

    Plain nginx: /var/log/nginx/access.log, with access.log.1 and gzipped archives beside it. Rotated files end in .gz, so read them with zcat instead of cat — a normal attack runs for hours and will be split across two files.

    The four patterns worth naming

    Not every 404 is an attack and not every bot is hostile. These four are the ones I act on.

    1. The login drumbeat. Hundreds of POST /wp-login.php lines from one address, all 200, all roughly the same byte count. The 200 is the point: WordPress re-renders the form with an error, so a rejected attempt and a served form share a status. Identical byte counts are the giveaway.

    2. The 302 that follows. A POST /wp-login.php answered with 302 is a successful authentication — WordPress redirects to /wp-admin/ on success and only returns 200 on failure. If the timestamp is 3 a.m. and the IP is not yours, assume the password is already known to someone else. Rotate it before you do anything else, then run the five-minute infection check.

    3. XML-RPC as an amplifier. POST /xmlrpc.php with system.multicall lets a client try hundreds of passwords in a single HTTP request. It barely registers in request counts and hammers the CPU, which is also how small accounts end up suspended for resource abuse. If you do not publish from a desktop client or the WordPress app, turning XML-RPC off removes the whole class of attack.

    4. Reconnaissance before the attempt. GET /?author=1 through ?author=9 enumerates usernames. So does /wp-json/wp/v2/users. A scattering of 404s against plugin folders you never installed is somebody running a vulnerability scanner against your site. None of it breaks anything by itself; all of it is a preview of what comes next.

    Counting, not squinting

    You cannot eyeball ten thousand lines. Three commands answer almost every question I have. Run them from the directory that holds the log.

    # top talkers, all traffic
    awk '{print $1}' access_log | sort | uniq -c | sort -rn | head -20
    
    # who is hitting the login form
    grep 'POST /wp-login.php' access_log | awk '{print $1}' | sort | uniq -c | sort -rn | head
    
    # bursts: requests per IP per minute
    awk '{print $1, substr($4,2,17)}' access_log | sort | uniq -c | sort -rn | head -20
    
    # did anything actually succeed?
    awk '$9 == 302 && /POST \/wp-login\.php/ {print $1, $4}' access_log

    The third command separates a bot from a person. A visitor loads a page, an image, a script, then leaves — a handful of requests in a minute, all different paths. An attacker sends the same path over and over. More than a few dozen requests to one endpoint in one minute is not a customer.

    Save the output before you change anything. Logs rotate on a schedule you did not set, and the evidence you need for a host support ticket disappears with them.

    Blocking it without breaking your own site

    Blocking an IP is the weakest option and the one most people reach for first. A credential-stuffing run comes from hundreds of addresses; you can ban them one at a time until you run out of patience. Do the durable things first.

    • Rate limit the endpoint at the edge. One rule in Cloudflare — more than a handful of POSTs to your login path per minute from one IP, block for an hour — stops the drumbeat before it reaches PHP.
    • Two-factor authentication on every administrator. It converts a successful password guess into a failed login, which is the only fix that survives a leaked password.
    • fail2ban on a VPS. A jail watching the Apache or nginx access log for repeated 200s on wp-login.php will ban automatically and expire the ban on its own.
    • .htaccess as a last resort. If you must deny an address by hand, wrap the rule so you do not wipe out normal access, and whitelist yourself first.
    <RequireAll>
        Require all granted
        Require not ip 203.0.113.45
    </RequireAll>

    On cPanel, the IP Deny Manager does the same thing with a form. Either way, add your own address and your uptime monitor to the allow list before you add anything to the deny list, or you will lock yourself out of the admin you were trying to protect. If the log already shows a 302, the ban is housekeeping — change the password, check for new administrator accounts, and treat the site as compromised until you have looked.

    Ten minutes, the next time you get an alert

    1. Open the access log, not the dashboard.
    2. Run the top-talkers command and look for one address far ahead of the rest.
    3. Grep the login path and count per IP.
    4. Check for any 302 on a login POST. If it exists, rotate the password now.
    5. Check POST /xmlrpc.php traffic and disable XML-RPC if you do not use it.
    6. Look for ?author= sweeps and /wp-json/wp/v2/users requests.
    7. Read yesterday’s .gz log for the same address.
    8. Only then add a block, and only after you have hardened the endpoint.

    Do one thing today: run the top-talkers command on your busiest site and write the normal number down. You cannot recognise abnormal traffic until you have seen a quiet day. For the logs that matter when something breaks rather than attacks, this is the map I use.