Human Made ยท Interview Prep

WordPress Engineering Journal

Every lesson follows the same pattern: understand it โ†’ see it working โ†’ build it yourself.
Logged in as: Guest

1

PHP vs WordPress Functions

Foundations
๐Ÿ“– Understand it

PHP is the language. WordPress is a tool built on top of it.

PHP gives you the basics: print text, do maths, loop through a list. WordPress wraps those basics with functions that know about your site โ€” your posts, your users, your settings.

A simple way to think about it: PHP is like a kitchen. WordPress is the recipe book written for that kitchen. You still need the kitchen, but the recipe book saves you from starting from scratch every time.

  • echo โ€” pure PHP. Just prints text.
  • get_bloginfo('name') โ€” WordPress function. Fetches your site name from the database.
  • get_current_user_id() โ€” WordPress function. Looks up who is logged in right now.
At Human Made, you will write both every day. The job is knowing when to use which one.
โœ… See it working

This code is running right now. The output below is real โ€” pulled live from your site.

<?php
// Pure PHP โ€” just maths
$php_result = 10 + 5;

// WordPress โ€” asks the database
$site_name   = get_bloginfo( 'name' );
$wp_version  = get_bloginfo( 'version' );
$user_id     = get_current_user_id();

echo "PHP maths result: " . $php_result . "\n";
echo "Site name (WP):   " . $site_name . "\n";
echo "WP version (WP):  " . $wp_version . "\n";
echo "Current user ID:  " . $user_id;
?>
Live Output
PHP maths result: 15
Site name (WP): KistuLab
WP version (WP): 6.8.5
Current user ID: 0
๐Ÿ›  Your turn
Task: Open this file and edit the code above.
  1. Change $php_result = 10 + 5 to use multiplication instead. Reload and confirm the output changes.
  2. Add a new line: $tagline = get_bloginfo( 'description' ); then echo it. What does it print?
  3. Replace $user_id with wp_get_current_user()->user_email โ€” now it shows an email instead of a number. What's the difference?
๐Ÿ’ฌ Interview talking point

"WordPress functions are wrappers around PHP that handle things like database queries, caching, and security for you. Using get_bloginfo() instead of a raw SQL query means WordPress can cache the result and you don't risk injection bugs."

2

Variables

Foundations
๐Ÿ“– Understand it

A variable is just a named box that holds a value. In PHP every variable starts with a dollar sign $.

You use them to store information so you can reuse it without repeating yourself. Think of them like sticky labels on boxes โ€” the label is the variable name, the contents of the box is the value.

  • $name = "Busayo" โ€” a string (text)
  • $age = 25 โ€” an integer (number)
  • $is_logged_in = true โ€” a boolean (true/false)
  • $skills = ["PHP", "WordPress", "Git"] โ€” an array (list)
โœ… See it working
<?php
$role        = "WordPress Engineer";
$company     = "Human Made";
$skills      = ["PHP", "WordPress", "REST API", "Git"];
$has_job     = false;

// Joining strings together is called "concatenation"
echo $role . " at " . $company . "\n";

// Loop through an array
foreach ( $skills as $skill ) {
    echo "- " . $skill . "\n";
}

// Check a boolean
if ( ! $has_job ) {
    echo "\nKeep going โ€” interview prep in progress.";
}
?>
Live Output
WordPress Engineer at Human Made
- PHP
- WordPress
- REST API
- Git

Keep going โ€” interview prep in progress.
๐Ÿ›  Your turn
Task:
  1. Change $has_job = false to true. The last line should disappear when you reload.
  2. Add two more skills to the $skills array. Confirm they appear in the list.
  3. Create a new variable $years_experience = 3 and echo it into the first line so it says "WordPress Engineer at Human Made ยท 3 years".
3

Action Hooks

WordPress Core
๐Ÿ“– Understand it

WordPress runs through a sequence of steps every time a page loads โ€” like a checklist. At certain points in that checklist it shouts: "Anyone want to do something here?"

Those shouting points are action hooks. You use add_action() to say "yes, run my function at that point."

Real-world analogy: Imagine a school timetable. WordPress is the timetable, and wp_footer is the "end of day" slot. You add an action to put your stuff in that slot.

add_action( 'hook_name', 'your_function_name', priority, args );

// Example: runs when WordPress loads the <head>
add_action( 'wp_head', 'my_custom_tracking_code' );

function my_custom_tracking_code() {
    echo '<!-- tracking snippet here -->';
}

The priority (default: 10) decides order. Lower numbers run first.

โœ… See it working

This hook fired right now when WordPress built this page. We captured the timestamp to prove it.

<?php
// This would go in functions.php in real code.
// Here we simulate it by calling do_action() manually.

$log = [];

add_action( 'demo_page_start', function() use ( &$log ) {
    $log[] = "[Priority 10] First function ran at: " . date('H:i:s');
});

add_action( 'demo_page_start', function() use ( &$log ) {
    $log[] = "[Priority 20] Second function ran at: " . date('H:i:s');
}, 20 );

do_action( 'demo_page_start' );
?>
Live Output
[Priority 10] First function ran at: 04:20:01
[Priority 20] Second function ran at: 04:20:01
๐Ÿ›  Your turn
Task โ€” do this in functions.php:
  1. Open functions.php and add this at the bottom:
    add_action( 'wp_footer', 'zix_my_first_footer_note' );
    function zix_my_first_footer_note() {
        echo '<p style="text-align:center;color:#999;">Built by Busayo</p>';
    }
  2. Save and reload any page on your site. Scroll to the bottom โ€” your text should appear.
  3. Change the priority to 5 and add a second action at priority 15. Inspect the page source โ€” which one appears first?
๐Ÿ’ฌ Interview talking point

"Action hooks are how WordPress lets you run code at specific points in the page lifecycle without modifying core files. This keeps upgrades safe โ€” your plugin or theme code stays separate from WordPress itself."

4

Filter Hooks

WordPress Core
๐Ÿ“– Understand it

Actions say "do something at this point." Filters say "take this value, change it, and give it back."

A filter always receives a value, and you must always return a value from it. If you forget to return something, you'll break the page.

Analogy: A filter is like a coffee machine. Raw water goes in, filtered water comes out. You don't throw the water away โ€” you change it.

add_filter( 'the_title', 'my_title_modifier' );

function my_title_modifier( $title ) {
    return 'โญ ' . $title; // always return the value
}
โœ… See it working
<?php
// apply_filters() sends a value through any registered filter functions.
$raw_title = "WordPress Engineering at Human Made";

// This filter adds a prefix
add_filter( 'demo_title_filter', function( $title ) {
    return strtoupper( $title );
});

// This filter adds a suffix (runs after, priority 20)
add_filter( 'demo_title_filter', function( $title ) {
    return $title . ' โœ“';
}, 20 );

$final_title = apply_filters( 'demo_title_filter', $raw_title );
echo "Before: " . $raw_title . "\n";
echo "After:  " . $final_title;
?>
Live Output
Before: WordPress Engineering at Human Made
After:  WORDPRESS ENGINEERING AT HUMAN MADE โœ“
๐Ÿ›  Your turn
Task:
  1. Edit the first filter above to add "[HM] " at the start of the title instead of uppercasing it. Reload โ€” does the output change?
  2. Add a third filter at priority 30 that replaces all spaces with underscores using str_replace( ' ', '_', $title ).
  3. Try removing the return from one of the filters. What happens to the output? (Then put it back.)
5

User Meta

WordPress Data
๐Ÿ“– Understand it

WordPress stores extra information about each user in a table called wp_usermeta. Each row has a user ID, a key, and a value.

Think of it like a filing cabinet โ€” each user has their own drawer, and inside are labelled folders (keys) with documents inside (values).

  • get_user_meta( $user_id, 'key', true ) โ€” reads a value
  • update_user_meta( $user_id, 'key', 'value' ) โ€” writes a value (creates if it doesn't exist)
  • delete_user_meta( $user_id, 'key' ) โ€” removes it

Always prefix your keys to avoid clashing with other plugins โ€” e.g. zix_learning_goal not just learning_goal.

โœ… See it working

This reads your stored note from the database. If nothing is saved yet it shows a default message.

<?php
$uid        = get_current_user_id();
$saved_note = get_user_meta( $uid, 'zix_journal_note', true );
echo "User ID: " . $uid . "\n";
echo "My note: " . $saved_note;
?>
Live Output (your data from the database)
User ID: 0
My note: (nothing saved yet โ€” complete the task below to add your first note)
๐Ÿ›  Your turn
Task: This form saves a note to the database against your user account. Fill it in, hit save, then scroll up to the output box โ€” it should show what you wrote.

After saving, scroll back to the "See it working" section โ€” your note will appear there.

๐Ÿ’ฌ Interview talking point

"I always use sanitize_text_field() before saving user input and wp_nonce_field() on every form. The nonce verifies the form was submitted from my site, not forged from somewhere else."

6

Prefixing & Plugin Structure

Plugin Dev
๐Ÿ“– Understand it

WordPress runs inside one PHP environment. Every plugin and theme is loaded together. That means if two plugins both define a function called get_user_name(), PHP throws a fatal error.

The fix: always prefix your function names, class names, hooks, and meta keys with something unique โ€” usually a short project code.

  • โŒ function get_user_name() โ€” could clash
  • โœ… function zix_get_user_name() โ€” unique to your project
  • โœ… class Zix_User_Manager โ€” unique class name
  • โœ… zix_learning_goal โ€” unique meta key

A WordPress plugin at minimum needs one PHP file with a specific comment block at the top (the "plugin header").

โœ… See it working

Here is the minimum structure of a real plugin file:

<?php
/*
 * Plugin Name: My Custom Plugin
 * Description: Does something useful.
 * Version:     1.0.0
 * Author:      Busayo Felix
 */

// Always guard against direct file access
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

// Prefixed function โ€” won't clash with other plugins
function zix_hello_world() {
    return "Hello from my plugin!";
}

// Hooked in cleanly
add_action( 'wp_footer', 'zix_hello_world' );
?>
<?php
// Detecting name clashes โ€” PHP will tell you:
// Fatal error: Cannot redeclare my_function()

// Safe version with prefix check:
if ( ! function_exists( 'zix_safe_example' ) ) {
    function zix_safe_example() {
        return "Only defined once, safely.";
    }
}

echo zix_safe_example();
?>
Live Output
Only defined once, safely.
๐Ÿ›  Your turn
Task โ€” in functions.php:
  1. Add this function (note the prefix):
    function zix_show_login_status() {
        if ( is_user_logged_in() ) {
            $user = wp_get_current_user();
            echo '<p style="text-align:center">Logged in: ' . esc_html( $user->display_name ) . '</p>';
        }
    }
    add_action( 'wp_footer', 'zix_show_login_status' );
  2. Save, reload the site, check the footer. You should see your name.
  3. Now deliberately try to define the same function twice. Look at your error log or enable WP_DEBUG โ€” PHP will show you exactly what happens without prefixing.
๐Ÿ’ฌ Interview talking point

"I always prefix everything โ€” functions, classes, meta keys, hooks โ€” with a project-specific code. At Human Made scale, you have dozens of plugins loaded at once. A naming collision causes a fatal error on a live production site."

7

Block Themes & theme.json

Modern WordPress
๐Ÿ“– Understand it

Classic WordPress themes use PHP templates (header.php, single.php) to control layout. Block themes replace those with HTML files that contain block markup โ€” and almost all design decisions live in a single file: theme.json.

Why does this matter? Human Made builds enterprise WordPress platforms. Block themes mean editors can change layouts in the Site Editor without touching code. Your job is to control what they can and can't change through theme.json.

Key things theme.json controls:

  • Colors โ€” define your brand palette and lock it so editors only pick from it
  • Typography โ€” set font families and size scales
  • Spacing โ€” control padding/margin via a scale (2, 4, 8, 16, ...)
  • Layout โ€” set the max content width
โœ… See it working

This is a real theme.json example. Copy this pattern โ€” this is exactly what you'd write at Human Made.

{
    "$schema": "https://schemas.wp.org/trunk/theme.json",
    "version": 2,
    "settings": {
        "color": {
            "defaultPalette": false,
            "palette": [
                { "name": "Brand Dark",  "slug": "brand-dark",  "color": "#0f0c29" },
                { "name": "Brand Mid",   "slug": "brand-mid",   "color": "#302b63" },
                { "name": "White",       "slug": "white",       "color": "#ffffff" }
            ]
        },
        "typography": {
            "fontFamilies": [
                {
                    "name": "Inter",
                    "slug": "inter",
                    "fontFamily": "Inter, sans-serif"
                }
            ],
            "fontSizes": [
                { "name": "Small",  "slug": "small",  "size": "14px" },
                { "name": "Normal", "slug": "normal", "size": "16px" },
                { "name": "Large",  "slug": "large",  "size": "24px" }
            ]
        },
        "layout": {
            "contentSize": "960px",
            "wideSize": "1200px"
        }
    },
    "styles": {
        "color": {
            "background": "var(--wp--preset--color--white)",
            "text":       "var(--wp--preset--color--brand-dark)"
        },
        "typography": {
            "fontFamily": "var(--wp--preset--font-family--inter)",
            "fontSize":   "var(--wp--preset--font-size--normal)"
        }
    }
}
Note
No theme.json found in your child theme. This is normal for classic themes โ€” the task below will walk you through adding one.
๐Ÿ›  Your turn
Task:
  1. Go to wp-admin โ†’ Appearance โ†’ Editor. This is the Site Editor. Look around โ€” notice how you can edit the header, footer, and page layouts visually.
  2. If your theme has a theme.json, open it and add a new colour to the palette. Save and go back to the editor โ€” does your new colour appear in the colour picker?
  3. In theme.json, set "defaultPalette": false inside the color settings. This removes WordPress's built-in colours and forces editors to use only yours. Reload the editor to see the change.
๐Ÿ’ฌ Interview talking point

"In block themes, theme.json is where I enforce design constraints. Setting defaultPalette: false means editors can only pick from the approved brand colours โ€” they can't introduce random colours that break the design system. That's important at enterprise scale."

8

PHP Security: Sanitize, Escape & Nonces

Security
๐Ÿ“– Understand it

There are two moments where you must be careful with user input:

  • When saving (input comes in) โ€” Sanitize it. Strip out anything dangerous before you store it.
  • When displaying (output goes out) โ€” Escape it. Make sure any HTML-like characters are turned into harmless text.

And there's a third thing: Nonces (number used once). These are security tokens added to forms and AJAX requests to prove the request is genuine โ€” not forged by a malicious site.

  • sanitize_text_field() โ€” strips tags and extra whitespace from plain text input
  • esc_html() โ€” makes text safe to display in HTML (converts < to &lt;)
  • esc_url() โ€” cleans up URLs before displaying them
  • esc_attr() โ€” makes text safe for use inside HTML attributes
  • wp_kses_post() โ€” strips unsafe HTML but allows safe tags like <p>, <strong>
โœ… See it working

An attacker might try to put JavaScript inside a form field. Watch what the escape functions do to it:

<?php
$attacker_input  = '<script>alert("hacked!")</script>';

// Sanitize before saving โ€” strips the whole script tag
$sanitized = sanitize_text_field( $attacker_input );

// Escape before displaying โ€” converts < and > to HTML entities
// so the browser shows the text instead of running the script
$safe_to_show = esc_html( $attacker_input );

echo "After sanitize_text_field(): " . $sanitized    . "\n";
echo "After esc_html():            " . $safe_to_show;
?>
Live Output โ€” the script attack is stopped
After sanitize_text_field():
After esc_html():            <script>alert("hacked!")</script>
Notice the script tag is gone after sanitize_text_field(), and it becomes visible text (not runnable code) after esc_html(). Neither version can harm the browser.
๐Ÿ›  Your turn
Task:
  1. Change $attacker_input to '<img src=x onerror="alert(1)">' โ€” a different type of XSS attack. What does esc_html() do to it?
  2. Try esc_url( 'javascript:evil()' ) โ€” echo the result. WordPress strips the javascript: protocol.
  3. Add a form with <?php wp_nonce_field('my_action','my_nonce'); ?> inside it. View the page source โ€” you'll see a hidden nonce input WordPress generated.
๐Ÿ’ฌ Interview talking point

"I treat input and output as two separate security steps. Sanitize on the way in, escape on the way out. I never trust that something was sanitized when it was saved โ€” I always escape at the point of display. esc_html() for text, esc_attr() for attributes, esc_url() for URLs."

9

MySQL & $wpdb

Database
๐Ÿ“– Understand it

All your WordPress content โ€” posts, users, settings โ€” lives in a MySQL database. PHP talks to that database, and WordPress gives you a global variable called $wpdb to do it safely.

The most important rule: never put user input directly into a SQL query. If you do, an attacker can manipulate the query and read or delete your database. This is called SQL Injection.

The fix is $wpdb->prepare() โ€” it uses placeholders (%s for strings, %d for integers) and fills them in safely.

<?php
global $wpdb;

// โŒ NEVER do this โ€” SQL injection risk
$results = $wpdb->get_results( "SELECT * FROM wp_users WHERE user_login = '" . $login . "'" );

// โœ… ALWAYS do this โ€” $wpdb->prepare() makes it safe
$results = $wpdb->get_results(
    $wpdb->prepare( "SELECT * FROM %i WHERE user_login = %s", $wpdb->users, $login )
);
?>
โœ… See it working

This queries the database right now and shows you real meta data from your account.

<?php
global $wpdb;
$uid = get_current_user_id();

// Fetch all meta keys that start with 'zix_' for the current user
$meta_rows = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT meta_key, meta_value
         FROM {$wpdb->usermeta}
         WHERE user_id = %d
         AND meta_key LIKE %s
         LIMIT 5",
        $uid,
        'zix_%'
    )
);

foreach ( $meta_rows as $row ) {
    echo $row->meta_key . ': ' . $row->meta_value . "\n";
}
?>
Live Output (your zix_ meta from the database)
No zix_ meta found yet. Complete Lesson 5 to write your first row.
๐Ÿ›  Your turn
Task:
  1. Change the LIKE %s pattern from zix_% to wp_%. Reload โ€” you'll see WordPress's own internal meta keys for your user.
  2. Change LIMIT 5 to LIMIT 10. More rows appear.
  3. Try $wpdb->get_var() instead of get_results() โ€” this returns a single value. Use it to count how many meta rows your user has:
$count = $wpdb->get_var(
    $wpdb->prepare(
        "SELECT COUNT(*) FROM {$wpdb->usermeta} WHERE user_id = %d",
        $uid
    )
);
echo "Total meta rows: " . $count;
๐Ÿ’ฌ Interview talking point

"I always use $wpdb->prepare() for any query that includes dynamic values. The prepare method uses parameterized queries โ€” it separates the SQL structure from the data, which is the correct way to prevent SQL injection at the database level."

10

REST API

Integration
๐Ÿ“– Understand it

The WordPress REST API lets external apps (or your own JavaScript) talk to WordPress over HTTP. Instead of loading a full PHP page, you make a request to a URL like /wp-json/wp/v2/posts and get back clean JSON data.

Why it matters at Human Made: Enterprise platforms often need to push data between systems โ€” a React front-end, a mobile app, a third-party CMS. The REST API is how WordPress speaks to all of them.

You can also register your own endpoints:

add_action( 'rest_api_init', function() {
    register_rest_route( 'zix/v1', '/hello', [
        'methods'             => 'GET',
        'callback'            => 'zix_hello_endpoint',
        'permission_callback' => '__return_true', // public
    ]);
});

function zix_hello_endpoint( WP_REST_Request $request ) {
    return new WP_REST_Response([
        'message' => 'Hello from my custom endpoint!'
    ], 200);
}

Your endpoint would then be live at: /wp-json/zix/v1/hello

โœ… See it working

Let's call the built-in WordPress REST API right now using PHP's wp_remote_get(). This fetches the 3 most recent posts from your own site.

<?php
// Calling your own REST API from PHP
$api_url  = home_url( '/wp-json/wp/v2/posts?per_page=3&_fields=id,title,link' );
$response = wp_remote_get( $api_url );

if ( ! is_wp_error( $response ) ) {
    $posts = json_decode( wp_remote_retrieve_body( $response ), true );
    foreach ( $posts as $post ) {
        echo $post['id'] . ' โ€” ' . $post['title']['rendered'] . "\n";
    }
}
?>
Live Output โ€” posts from your REST API
2628 โ€” debug shortcode
2483 โ€” Ambassador Signup
๐Ÿ›  Your turn
Task:
  1. Change per_page=3 to per_page=5. Reload โ€” more posts appear.
  2. Change the endpoint from /wp/v2/posts to /wp/v2/users. What data does it return? (Note: the fields will be different.)
  3. Register your own endpoint โ€” add this to functions.php:
    add_action( 'rest_api_init', function() {
        register_rest_route( 'zix/v1', '/me', [
            'methods'             => 'GET',
            'callback'            => 'zix_me_endpoint',
            'permission_callback' => 'is_user_logged_in',
        ]);
    });
    function zix_me_endpoint() {
        $user = wp_get_current_user();
        return new WP_REST_Response([
            'name'  => $user->display_name,
            'email' => $user->user_email,
        ], 200);
    }
    Then visit https://kistulab.com/wp-json/zix/v1/me in your browser while logged in.
๐Ÿ’ฌ Interview talking point

"The REST API is how I decouple the WordPress back-end from the front-end. I register custom namespaced endpoints โ€” vendor/v1/ โ€” with proper permission callbacks so public routes return only safe data and authenticated routes are protected. I always use WP_REST_Response so WordPress handles the headers correctly."

11

Git: Branching & Pull Requests

Workflow
๐Ÿ“– Understand it

Git is version control software. It tracks every change you make to your code, lets you go back to any previous version, and lets teams work on the same project without breaking each other's work.

A branch is like a copy of the codebase you can work on freely. When you're done, you merge it back into the main branch through a Pull Request (PR) โ€” which gives teammates a chance to review your code before it goes live.

At Human Made, branching strategy matters. The typical flow is:

  1. main (or master) โ€” production code, only merged into from reviewed PRs
  2. develop โ€” integration branch where features come together
  3. feature/your-feature-name โ€” one branch per feature
  4. fix/bug-description โ€” one branch per bug fix
โœ… See it working

Here is the exact workflow used on a typical Human Made project:

# 1. Always start from the latest main
git checkout main
git pull origin main

# 2. Create your feature branch
git checkout -b feature/zix-rest-api-endpoint

# 3. Do your work, then stage and commit
git add .
git commit -m "feat: add /zix/v1/me REST endpoint"

# 4. Push your branch to GitHub/GitLab
git push origin feature/zix-rest-api-endpoint

# 5. Open a Pull Request on GitHub
# Request a review from a teammate

# 6. After review + approval, merge into main
# Then delete the feature branch โ€” it's done
# Good commit message format (Conventional Commits)
feat: add custom REST endpoint for user profile
fix: correct nonce check in form handler
docs: update README with endpoint list
refactor: extract user query to helper function
Check your project's current Git status โ€” run this in terminal
cd /Users/busayofelix/Desktop/zix-child && git log --oneline -5
๐Ÿ›  Your turn โ€” in terminal
Task:
  1. Open your terminal, navigate to your project:
    cd /Users/busayofelix/Desktop/zix-child
  2. Check what branch you're on and see recent history:
    git status
    git log --oneline -5
  3. Create a new branch for this learning journal:
    git checkout -b feature/learning-journal-updates
  4. Make a small change to any file, then stage and commit it with a proper commit message:
    git add .
    git commit -m "feat: update learning journal with 3-part lesson structure"
  5. Push the branch and go to GitHub to open a Pull Request (even if only you review it โ€” the practice is what matters):
    git push origin feature/learning-journal-updates
๐Ÿ’ฌ Interview talking point

"I follow a feature branch workflow โ€” one branch per task, always branched from the latest main. I write commit messages in the conventional commits format so the changelog is readable. I never push directly to main. Every change goes through a PR, even small ones, because code review is how the team catches bugs before they hit production."

12

npm & Composer

Tooling
๐Ÿ“– Understand it

Modern WordPress projects use two package managers:

  • npm โ€” manages JavaScript dependencies. Things like webpack, eslint, Sass compilers. Defined in package.json.
  • Composer โ€” manages PHP dependencies. Things like coding standards checkers, test libraries, and PHP helper packages. Defined in composer.json.

Think of them both like an App Store for code. Instead of copy-pasting someone else's library into your project, you list what you need, and the package manager downloads and manages it for you.

At Human Made, you'll also use npm scripts to run tasks: building CSS, linting code, running tests.

โœ… See it working

Here are the files you'd find in a real Human Made WordPress project:

// package.json โ€” JavaScript dependencies and scripts
{
    "name": "my-hm-theme",
    "scripts": {
        "build":  "webpack --mode production",
        "start":  "webpack --mode development --watch",
        "lint":   "eslint assets/js/**/*.js",
        "lint:css": "stylelint assets/css/**/*.scss"
    },
    "devDependencies": {
        "@wordpress/scripts": "^27.0.0",
        "eslint":             "^8.0.0"
    }
}
// composer.json โ€” PHP dependencies
{
    "name": "human-made/my-project",
    "require": {
        "php": ">=8.1"
    },
    "require-dev": {
        "squizlabs/php_codesniffer":       "^3.7",
        "wp-coding-standards/wpcs":        "^3.0",
        "phpunit/phpunit":                 "^9.0"
    },
    "scripts": {
        "lint":  "phpcs --standard=WordPress .",
        "test":  "phpunit"
    }
}
Your project files
package.json: โŒ Not found
composer.json: โŒ Not found
๐Ÿ›  Your turn โ€” in terminal
Task:
  1. In your terminal, go to your project and check what npm scripts are available:
    cd /Users/busayofelix/Desktop/zix-child
    cat package.json
  2. Run npm install if you haven't recently (installs all listed packages).
  3. If a build script exists, run npm run build. Watch what it compiles.
  4. Check Composer: cat composer.json โ€” are there any coding standard tools listed? These are what Human Made uses to enforce code quality automatically.
  5. If Composer is installed run: composer install then composer run lint โ€” it will check your PHP against WordPress coding standards.
๐Ÿ’ฌ Interview talking point

"I'm comfortable with both npm and Composer. On the PHP side I use PHPCS with the WordPress Coding Standards ruleset to catch style issues automatically. On the JS side I use @wordpress/scripts which gives you a webpack setup pre-configured for Gutenberg block development. Running these as CI checks means code quality problems are caught before review."

13

Accessibility (a11y)

Standards
๐Ÿ“– Understand it

Accessibility means your site works for everyone โ€” including people using screen readers, keyboard-only navigation, or who have low vision.

Governments and enterprise clients often require WCAG 2.1 AA compliance. Human Made builds for enterprise organisations, so accessibility is a real requirement โ€” not an optional extra.

The most common issues (and how to fix them):

  • โŒ <div onclick="..."> โ€” divs aren't keyboard-focusable
    โœ… Use <button> instead โ€” it gets focus and keyboard events for free
  • โŒ <img src="..."> โ€” screen readers skip it
    โœ… Add alt="descriptive text" always
  • โŒ Low colour contrast between text and background
    โœ… Use a contrast checker โ€” WCAG AA requires a ratio of at least 4.5:1 for normal text
  • โŒ Missing form labels โ€” screen readers don't know what a field is for
    โœ… Always connect labels with <label for="field-id">
โœ… See it working

Bad vs good โ€” look at the HTML source difference. Tab through these with your keyboard to feel the difference.

<!-- โŒ BAD โ€” not keyboard accessible, no semantics -->
<div onclick="doSomething()" style="cursor:pointer;">Click me</div>

<!-- โœ… GOOD โ€” button gets focus, Enter key works, screen reader says "button" -->
<button type="button" onclick="doSomething()">Click me</button>

<!-- โŒ BAD form โ€” screen reader doesn't know what this field is -->
<input type="text" placeholder="Enter email">

<!-- โœ… GOOD form โ€” label linked by 'for' and 'id' attributes -->
<label for="user-email">Email address</label>
<input type="email" id="user-email" name="email">

<!-- Skip link โ€” lets keyboard users jump past the nav -->
<a href="#main-content" class="skip-link">Skip to content</a>

Tab through these โ€” notice which ones get a visible focus ring:

โŒ Div (not keyboard accessible)
๐Ÿ›  Your turn
Task:
  1. Press Tab on this page and navigate using only the keyboard. Can you reach all the interactive elements? Notice anything broken?
  2. Open Chrome DevTools โ†’ Lighthouse โ†’ check the "Accessibility" category. Run an audit on this page. What score do you get? What issues does it flag?
  3. Right-click the โŒ Div element above โ†’ Inspect. Notice it has no role, no tabindex. Now inspect the โœ… Button โ€” see how the browser treats them differently in accessibility terms.
  4. Find any form on your site that is missing a <label> tag. Add one and verify it works by clicking the label text โ€” if the input focuses, it's correctly linked.
๐Ÿ’ฌ Interview talking point

"I follow WCAG 2.1 AA as a baseline. The practical things I check every time: semantic HTML (buttons for actions, links for navigation), visible focus states, colour contrast ratios, and labels on every form field. I use Lighthouse and keyboard-only testing before any PR is raised."

14

Debugging

Dev Workflow
๐Ÿ“– Understand it

Debugging is the process of finding out why code isn't doing what you expect. In WordPress, there are two main tools:

  • WP_DEBUG โ€” a constant you enable in wp-config.php that turns on PHP error reporting for WordPress
  • error_log() โ€” a PHP function that writes messages to a log file so you can trace what's happening in your code without breaking the page
  • Query Monitor plugin โ€” a browser toolbar that shows every database query, hook, PHP notice, and slow query on the page

Add these three lines to wp-config.php to turn on debug mode:

define( 'WP_DEBUG',         true  );  // Show PHP errors
define( 'WP_DEBUG_LOG',    true  );  // Write errors to /wp-content/debug.log
define( 'WP_DEBUG_DISPLAY', false );  // Don't show errors to site visitors
โœ… See it working

This code writes to your debug log right now (if WP_DEBUG_LOG is on) and shows you diagnostic output on screen.

<?php
// Writing to the debug log โ€” check /wp-content/debug.log after running this
error_log( '[MyPlugin] User ' . get_current_user_id() . ' loaded the page at ' . current_time('mysql') );

// Printing a variable's contents for inspection (use this during development only)
$my_data = [ 'post_id' => 42, 'status' => 'published' ];
error_log( '[MyPlugin] Data: ' . print_r( $my_data, true ) );

// wp_die() stops execution and shows a message โ€” useful during debugging
// wp_die( 'Stopping here to inspect' );
?>
Live Diagnostic โ€” your environment right now
timestamp: 2026-04-13 04:20:02
current_user_id: 0
site_url: https://kistulab.com
php_version: 7.4.33
wp_debug_on: YES
debug_log_on: YES

A line was written to your debug.log โ€” check: /wp-content/debug.log
๐Ÿ›  Your turn
Task:
  1. Open wp-config.php and confirm or add the three WP_DEBUG constants above. Save, then reload this page.
  2. Open /wp-content/debug.log in your editor. You should see the "[LJ Debug]" line that was written when this page loaded.
  3. Add your own error_log() call in any function in functions.php. Reload the site and watch the new line appear in the log.
  4. Install the Query Monitor plugin (free on wordpress.org). After activating it, reload this page โ€” look at the toolbar at the top of the screen. Click "Queries" โ€” how many database queries ran to build this page?
๐Ÿ’ฌ Interview talking point

"My debugging workflow: first I check WP_DEBUG_LOG โ€” errors are there without showing anything to users. Then I use Query Monitor to find slow queries or unexpected hook calls. I use error_log( print_r( $var, true ) ) to inspect variables during development, and I always remove those before committing. For deeper issues I step through code with Xdebug."