Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Wordpress Api Laravel Package

rickwest/laravel-wordpress-api

A Laravel package for consuming the WordPress REST API. Retrieve posts, pages, categories, tags, media, and custom post types with an expressive query builder, plus filtering, pagination, authentication, and Laravel-friendly responses.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish

composer require rickwest/laravel-wordpress-api
php artisan vendor:publish --tag=wordpress-api-config

Package auto-discovery registers the service provider; the facade is RickWest\WordPressApi\Facades\WordPressApi.

Configure the endpoint first, before writing any code. This is the single step that trips most people up. The client expects the WordPress REST root, not the site root:

// config/wordpress-api.php
return [
    'url'     => env('WORDPRESS_API_URL', 'https://example.com/wp-json'),
    'timeout' => 10,
];
WORDPRESS_API_URL=https://example.com/wp-json

If the site uses plain permalinks or ?rest_route= style URLs, the client cannot build resource paths for you — fix permalinks to /%postname%/ or point the config at https://example.com/?rest_route=/ and accept that every call needs care.

Verify the API is actually reachable before blaming the package:

curl -s https://example.com/wp-json/wp/v2/posts?per_page=1 | head -c 400

Anything other than JSON means a security plugin, a WAF, or rest_enabled filters are blocking you — the package will surface this as a generic request exception, not a useful message.

First use case — a blog index. The facade exposes resource-scoped calls; posts are the natural starting point:

use RickWest\WordPressApi\Facades\WordPressApi;

$posts = WordPressApi::posts()->all();          // default page of 10, published only
$post  = WordPressApi::posts()->get($id);
$post  = WordPressApi::posts()->getBySlug('hello-world');

Where to look first, in order: config/wordpress-api.php (what knobs exist), the facade accessor and its underlying client class (how the HTTP call is composed — it wraps Laravel's Http client, so Http::fake() works in tests), then the per-resource classes (Posts, Pages, Categories, Tags, Media, Users, Comments) which define exactly which query parameters and endpoints you get for free versus have to pass manually.

Implementation Patterns

Wrap it in a repository, don't scatter facade calls. The value of this package is that it is thin — which means it has no caching, no retry, no DTO mapping. Put that in one place:

namespace App\Services;

use Illuminate\Support\Facades\Cache;
use RickWest\WordPressApi\Facades\WordPressApi;

class WordPressRepository
{
    public function posts(int $page = 1, int $perPage = 12): array
    {
        return Cache::remember("wp.posts.{$page}.{$perPage}", now()->addMinutes(15), function () use ($page, $perPage) {
            return WordPressApi::posts()->all([
                'page'     => $page,
                'per_page' => $perPage,
                '_embed'   => true,
            ]);
        });
    }

    public function post(string $slug): ?array
    {
        return Cache::remember("wp.post.{$slug}", now()->addHour(), function () use ($slug) {
            $result = WordPressApi::posts()->getBySlug($slug);

            return $result ?: null;
        });
    }
}

Whether you pass query parameters as the method argument or via a chained builder depends on the resource class — check the resource you're using. The underlying query string is plain WordPress REST semantics, so anything documented at /wp/v2/posts arguments is fair game.

Render the post body with care. content.rendered is HTML from WordPress, already filtered. Do not double-escape it in Blade:

{!! $post['content']['rendered'] !!}

Then strip or sanitize on ingest if the WP install has multiple authors you don't fully trust — the package does zero sanitization, correctly, because that's not its job.

Use _embed instead of N+1 featured images. This is the highest-leverage pattern in the whole package. Requesting _embed gives you _embedded['wp:featuredmedia'][0]['source_url'] and _embedded['wp:term'] inline, so a 12-post index is one HTTP call instead of thirteen:

$image = data_get($post, '_embedded.wp:featuredmedia.0.source_url');
$tags  = collect(data_get($post, '_embedded.wp:term.0', []))->pluck('name');

Use _fields to keep payloads small when you only need a listing:

WordPressApi::posts()->all(['_fields' => 'id,slug,title,excerpt,date', 'per_page' => 20]);

A default /wp/v2/posts response is 30–60 KB per post with _embed; trimming fields is the difference between a 40 ms and a 400 ms page.

Pagination lives in headers, and the package may not surface it. Read X-WP-Total and X-WP-TotalPages from the response — either via the package's response object if it exposes one, or by catching the underlying Illuminate\Http\Client\Response. Build a simple paginator rather than guessing:

$response = Http::get(config('wordpress-api.url').'/wp/v2/posts', ['per_page' => 12, 'page' => $page]);
$totalPages = (int) $response->header('X-WP-TotalPages');

Custom post types and taxonomies generally aren't first-class resources in the package. Drop to the path directly, remembering the /wp/v2 prefix is already applied for built-ins but not for custom routes:

WordPressApi::get('wp/v2/portfolio', ['per_page' => 6]);

The CPT must be registered with 'show_in_rest' => true and a rest_base on the WordPress side — no amount of Laravel config fixes a CPT that isn't REST-exposed.

Scheduled content sync. Because it's read-only, the package shines as an ingest layer: a scheduled command that pulls posts into Eloquent gives you full-text search, relationships, and Blade caching without touching WordPress per-request. Wrap the whole sync in DB::transaction() and upsert on slug or WP id so re-runs are idempotent.

Testing. The client sits on Laravel's HTTP client, so Http::fake() covers it:

Http::fake([
    '*/wp-json/wp/v2/posts*' => Http::response([['id' => 1, 'slug' => 'hello']], 200, [
        'X-WP-TotalPages' => 3,
    ]),
]);

Fake the header too — pagination bugs are the ones that slip through otherwise.

Gotchas and Tips

Read-only means read-only — no media uploads, no comment creation. If the feature request is "let editors publish from the Laravel admin," this is the wrong package; you need a full-featured WP client or a custom integration.

Auth is under-documented and rarely needed. Public posts, pages, categories, and tags need no credentials at all. If you need private/draft content, context=edit, or user data, you need Application Passwords (WP 5.6+) over HTTPS, passed as HTTP Basic auth. Plain HTTP will silently drop the Authorization header on many hosts — the request succeeds, returns public-only data, and you spend an hour wondering why. Application Passwords also require the site to not have wp_is_application_passwords_available filtered off.

getBySlug on a missing slug usually returns an empty result, not an exception. The WP API returns 404 with code: rest_post_invalid_id for ID lookups and an empty array for slug lookups. Always null-check and abort(404) yourself — otherwise your Blade view explodes on $post['title'].

Default per_page is 10, and the hard max is 100. Asking for 200 gets you a 400 rest_invalid_param, not a truncation. Paginate properly or accept the cap.

Errors are HTTP errors. A 404 for a bad slug, a 401 for bad auth, a 403 from a WAF, and a 200 containing an HTML maintenance page all fail differently. Log the effective URL and status on failure:

try {
    $posts = WordPressApi::posts()->all();
} catch (\Throwable $e) {
    Log::error('WP API failure', ['url' => config('wordpress-api.url'), 'error' => $e->getMessage()]);
    $posts = [];
}

Always have a $posts = [] fallback. A WordPress outage should degrade the marketing page, not 500 it.

Set a timeout and a retry. The package ships a configurable timeout (10s by default in most installs) — lower it to 3–5s for page-render calls so a hung WP doesn't hold PHP workers. Add retries at the HTTP layer, not by looping the package call, which will just retry the same failure.

Local and staging WordPress sites frequently have invalid or self-signed TLS. You'll get cURL error 60. Fix the certificate, not the code — disabling verification ('verify' => false) is a footgun that inevitably ships to production.

Caching is entirely your problem. There is no cache, no ETag handling, no conditional requests. Every all() call is a fresh network round-trip to WordPress. On a shared WordPress host, an uncached blog index under load will get you rate-limited or IP-banned. Cache aggressively and key on the full query parameter set.

WordPress.com-hosted sites behave differently from self-hosted: different rate limits, some endpoints gated by plan, and the REST root is https://public-api.wordpress.com/wp/v2/sites/{site} — you can point the config there, but expect _embed and context=edit to be unreliable.

Custom post types are invisible until show_in_rest is true, and even then the rest_base may not match the post type slug. Check https://example.com/wp-json/wp/v2/types to see exactly what's exposed before debugging Laravel.

Small package, 37 stars — read the source before you rely on it. It's a thin wrapper, which is a feature (you can audit it in ten minutes) and a risk (no community pressure to fix edge cases). Pin the version in composer.json, and if your usage is more than a handful of calls, consider binding your own thin client over Laravel's Http facade instead — you'd be reimplementing perhaps 100 lines and gain full control. Use this package when you want the facade ergonomics and are content to layer caching, error handling, and pagination on top yourself.

Extension point: since everything funnels through Laravel's HTTP client, Http::globalRequestMiddleware() / globalResponseMiddleware() or a Http::beforeSending hook is the clean place to add auth headers, correlation IDs, or response logging without forking the package.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect