1. Building the Integration Plugin
Following Week 20's plugin structure, the integration lives in its own plugin —
keeping "talk to the Laravel API" separate from any theme, exactly the same reason
Week 19 moved the CPT out of functions.php:
<?php
/**
* Plugin Name: API Bridge
* Description: Connects this site to the Laravel jobs/events/portfolio API.
* Version: 1.0.0
*/
if (!defined('ABSPATH')) exit;
define('AB_API_BASE_URL', 'https://api.yourproject.com/api/v1');
require_once __DIR__ . '/includes/api-client.php';
require_once __DIR__ . '/includes/shortcodes.php';
require_once __DIR__ . '/includes/settings.php';
The API key itself belongs in an admin setting (via Week 21's Settings API), never
hardcoded — the same "credentials don't belong in committed code" principle from
Week 9's .env discussion, applied here through WordPress's own
configuration mechanism.
2. Calling the API from WordPress
wp_remote_get()/wp_remote_post() are WordPress's built-in
HTTP client — the WordPress-native equivalent of Laravel's Http::
facade or a raw curl call:
<?php
if (!defined('ABSPATH')) exit;
function ab_fetch_jobs(): array {
$apiKey = get_option('ab_api_key');
$response = wp_remote_get(AB_API_BASE_URL . '/jobs', [
'headers' => ['Authorization' => "Bearer {$apiKey}"],
'timeout' => 5, // never let a slow external call hang the page indefinitely
]);
if (is_wp_error($response)) {
error_log('API Bridge: ' . $response->get_error_message());
return []; // fail gracefully -- an empty list, not a fatal error on the page
}
$statusCode = wp_remote_retrieve_response_code($response);
if ($statusCode !== 200) {
error_log("API Bridge: API returned status {$statusCode}");
return [];
}
$body = wp_remote_retrieve_body($response);
return json_decode($body, true) ?? [];
}
Every failure path returns an empty array rather than crashing the page — this is the "what does the WordPress site show if the API is down" question from Week 30's architecture section, answered concretely. A slow or failed external API call should degrade the page, never break it entirely.
<?php
function ab_jobs_shortcode(): string {
$jobs = ab_fetch_jobs();
if (empty($jobs)) {
return '<p>No open positions right now. Check back soon!</p>';
}
ob_start();
foreach ($jobs as $job) : ?>
<article class="job-listing">
<h3><?= esc_html($job['title']) ?></h3>
<p><?= esc_html($job['location']) ?></p>
<a href="<?= esc_url($job['apply_url']) ?>">Apply</a>
</article>
<?php endforeach;
return ob_get_clean();
}
add_shortcode('open_jobs', 'ab_jobs_shortcode');
Every value from the API response — data from an external system, no different
from user input in terms of trust — still gets escaped with esc_html()/
esc_url() exactly per Week 24's rule. "It came from my own API" isn't
an exemption from the sanitize/escape discipline.
3. Caching the Response
Calling the Laravel API on every single WordPress page load is both slow and
unnecessary — WordPress's Transients API is the right tool,
conceptually identical to Week 26's Cache::remember():
<?php
function ab_fetch_jobs(): array {
$cached = get_transient('ab_jobs_cache');
if ($cached !== false) {
return $cached;
}
// ... the wp_remote_get() call from above ...
$jobs = json_decode($body, true) ?? [];
set_transient('ab_jobs_cache', $jobs, HOUR_IN_SECONDS); // TTL-based, per Week 26
return $jobs;
}
This is the exact cache-aside pattern from Week 26, expressed through WordPress's
own storage mechanism (transients ultimately live in wp_options, or in
Redis if an object cache is configured, per Week 24) instead of Laravel's
Cache facade — same idea, same trade-off between staleness and API
load, different API surface.
4. Security & Performance Pass
Apply Weeks 25-26's cross-stack checklist to the complete, integrated system specifically:
✓ API key stored in WordPress options, never in committed plugin code
✓ Every Laravel write endpoint requires Sanctum auth + a Policy check (Week 30)
✓ Every value from the API response is escaped before being printed (esc_html/esc_url)
✓ The WordPress→API call has a timeout & fails gracefully, not fatally
✓ API responses are cached (transients), not fetched on every page load
✓ composer audit run against the Laravel app; wp plugin list --update=available checked
✓ Laravel's session cookies configured with secure/http_only/same_site (Week 25)
✓ OPcache configured correctly for the Laravel deployment (Week 26)
Running through this list deliberately, item by item, against your actual project — not assuming it's fine because each piece was correct in isolation during earlier weeks — is the point of a real review. Integration is exactly where gaps between two individually-correct systems tend to surface.
5. Shipping It
Deploy both halves following Week 28's playbook: the Laravel API to a VPS (or
Dockerized), the WordPress site with the api-bridge plugin to its own
hosting (the same server, or a separate one — either is a legitimate architecture,
and the API key/HTTP call approach works identically either way):
✓ Laravel API deployed via Week 28's Nginx + PHP-FPM (or Docker) setup
✓ Laravel migrations run against the production database
✓ Queue worker running under Supervisor (Week 16, 28)
✓ WordPress site deployed with the theme/plugin(s) from Weeks 17-24
✓ api-bridge plugin activated, with a real production API key configured
✓ CI (Week 27) green on the Laravel repository before this deploy
✓ A real end-to-end test: visit the live WordPress page and confirm it shows real,
live data from the deployed Laravel API -- not local/mock data
That last line is the actual finish line for this capstone — not "the code is written," but "a real visitor, on the live site, sees real data flow from the Laravel API you built through the WordPress integration you built." Confirm it directly, the same way Week 28's exercise insisted on watching a real deploy actually take effect.
6. This Week's Milestone
Build the integration, review the whole system, and ship it
Finish the capstone: WordPress talking to your live Laravel API, reviewed and deployed.
Requirements:
- Build the
api-bridgeWordPress plugin with an admin settings page for the API key/base URL, and at least one shortcode or template integration displaying real data from your Laravel API. - Implement transient caching for the API call, with graceful failure handling (timeout +
is_wp_errorcheck) confirmed by deliberately pointing the plugin at a wrong URL and verifying the page still renders without fataling. - Work through the full-system security/performance checklist against your actual project, fixing anything that doesn't hold.
- Deploy both halves to real hosting, following Week 28's approach.
- Confirm the finish-line test: a real request to the live WordPress site displays real, live data fetched from your deployed Laravel API.
Deliberately breaking the API connection (wrong key, wrong URL, stopping the Laravel server temporarily) and confirming the WordPress site degrades gracefully rather than fataling is worth doing on the live deployment too, not just locally — production failure modes are exactly where "it worked when I tested it once" tends to fall apart.
7. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why does ab_fetch_jobs() return an empty array on failure instead of letting the error propagate and break the page?
Why does ab_fetch_jobs() return an empty array on failure instead of letting the error propagate and break the page?
An external dependency — a separate Laravel API, over the network — can fail or slow down for reasons entirely outside the WordPress site's control. A careers page (or any page depending on this data) should degrade gracefully when that happens, showing an empty state rather than a fatal PHP error that takes down the entire page for every visitor just because one external call failed.
Q2
Why does ab_jobs_shortcode() still call esc_html() on data that came from your own Laravel API, which you built and trust?
Why does ab_jobs_shortcode() still call esc_html() on data that came from your own Laravel API, which you built and trust?
The escaping rule from Week 1, 8, 19 and 24 is about the output context, not the trustworthiness of the source — a job title that ultimately originated from a company's own form submission into your API is still, transitively, user-supplied content by the time it's printed on the WordPress page. Treating "it came from my own API" as an exemption reintroduces exactly the XSS risk this course has flagged repeatedly since Week 1.
Q3
What problem does caching the API response with a WordPress transient solve, beyond just "making the page load faster"?
What problem does caching the API response with a WordPress transient solve, beyond just "making the page load faster"?
Without caching, every single WordPress page view would trigger a fresh HTTP request to the Laravel API — a heavily-trafficked WordPress page could hammer the API with load proportional to WordPress site traffic, not to how often the underlying data actually changes. Caching decouples the two, the same reasoning behind Week 26's cache-aside pattern generally: expensive work amortized across many reads, refreshed on a sensible schedule rather than on every single request.
Q4
What does this lesson mean by the capstone's actual "finish line," beyond the code being written and deployed?
What does this lesson mean by the capstone's actual "finish line," beyond the code being written and deployed?
A direct, verified observation — visiting the live, deployed WordPress site and confirming it genuinely displays real data fetched from the deployed Laravel API, not local data, not mock data, not something that merely "should work" based on the code looking correct. This mirrors Week 28's insistence on watching a real deploy actually take effect: verifying the end-to-end system with your own eyes is different from, and more reliable than, trusting that each individually-correct piece will necessarily compose correctly.