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 Semrush Laravel Package

jeffersongoncalves/laravel-semrush

A Laravel wrapper for the SEMrush API that lets you pull SEO, keyword, domain, and competitor data directly into your PHP applications through a clean, configurable interface.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up credentials:

composer require <vendor>/laravel-semrush
php artisan vendor:publish --tag="semrush-config"   # if the package ships a config file

Set the key in .env:

SEMRUSH_API_KEY=your-key
SEMRUSH_DATABASE=us

Then make one call to confirm the round trip:

use Vendor\Semrush\Semrush; // check the actual namespace/FQCN in the package's src/

$rows = app(Semrush::class)->domainOverview('example.com');
dump($rows[0]);

Look first at three places:

  1. The client class itself — it is the entire public surface. The report methods are thin wrappers over get(); read get() once and you understand every method.
  2. The exception classes (SemrushException, RequestException) — the split between them is the single most important behavioural detail of this package (see Gotchas).
  3. The published config — specifically how SEMRUSH_API_KEY and services.semrush.key are resolved, because that determines whether multi-tenant or per-environment keys are viable.

First realistic use case: a scheduled domain overview for your own site plus 3–5 competitors, cached daily, feeding a dashboard. domainOverview() + domainCompetitors() gets you there in under an hour.

Implementation Patterns

Resolve once, use everywhere. Bind the client through the container or a facade and inject it. Because every report method is just get() with a preset type, there is no reason to instantiate multiple clients in a request.

Treat report methods as presets, get() as the real API.

$semrush->domainOrganic('example.com', ['display_limit' => 100, 'display_sort' => 'tr_desc']);
$semrush->get('phrase_this', ['phrase' => 'laravel', 'database' => 'us']);

Anything the wrapper does not expose — export_columns, display_filter, display_offset, unusual report types, alternate databases per call — goes through the second argument. This is where most real work happens; the named methods are conveniences.

Paginate explicitly. Semrush caps result sets and charges per returned line. The reliable loop is offset-based: request display_limit rows at display_offset = n, and stop when you get back fewer rows than you asked for.

$limit = 1000;
for ($offset = 0; ; $offset += $limit) {
    $rows = $semrush->domainOrganic($domain, [
        'display_limit'  => $limit,
        'display_offset' => $offset,
        'export_columns' => 'Ph,Po,Nq,Cp,Ur',
    ]);

    if (count($rows) < $limit) {
        break;
    }
}

Keep the page size sane (500–1000) so a single failed request does not cost you much re-work.

Cache aggressively; the API is metered. Every row costs units. Wrap calls in Cache::remember() keyed on report type + domain + params hash, with a TTL that matches your business need (daily for dashboards, weekly for competitor snapshots). Never call Semrush from a page render.

Push bulk pulls to the queue. A job per domain, with $tries and a backoff, is the correct shape. Rate limits and long result sets both punish synchronous loops.

Normalise rows at the boundary. Rows are plain associative arrays keyed by Semrush column codes (Ph, Nq, Cp, Ur, Dn, Rk, Or), not friendly names. Map them into DTOs or typed arrays once, in one place, so the rest of the app never sees Nq.

final class OrganicRow
{
    public static function fromSemrush(array $row): self { /* map Ph, Po, Nq, ... */ }
}

Handle the two exception paths distinctly. RequestException means transport/auth/HTTP problems — retry or alert. SemrushException means the API answered "ERROR" in a 200 body — often "nothing found", which is a data outcome, not an outage.

try {
    $rows = $semrush->keywordOverview('laravel developer');
} catch (SemrushException $e) {
    // often "NOTHING FOUND" — log at info, return []
} catch (RequestException $e) {
    // transport/auth — retry or alert
}

Testing. Because this sits on Laravel's HTTP client (verify — if it uses Guzzle directly, adjust), Http::fake() with a real captured CSV body is the highest-value test fixture you can build. Capture one response per report type once, commit them, and never hit the live API in CI.

Gotchas and Tips

HTTP 200 does not mean success. This package is explicit that ERROR bodies arrive with a 200 status. ERROR 50 :: NOTHING FOUND is extremely common and is not a bug — it means the domain/keyword has no data in that database. Decide early whether your app treats that as an empty result or a failure, and encode that decision in one helper so it is consistent.

config:cache will bite you. If SEMRUSH_API_KEY stops resolving after php artisan config:cache, the classic Laravel cause applies: env() called outside a config file returns null once config is cached. The services.semrush.key fallback exists partly for this — prefer putting the key in config/services.php and reading it from config, not from env() at runtime.

Two config sources, one precedence order. SEMRUSH_API_KEY wins over services.semrush.key. If you set both (easy when copying snippets), you will edit the one that is being ignored. Pick one location per project and delete the other.

Multi-tenant keys need verification. If you are tempted to config()->set('services.semrush.key', $tenantKey) mid-request, test it: whether it takes effect depends on whether the client reads config at resolution time or captures it in the constructor. If it captures, bind a fresh instance with the tenant's key instead. Do not assume.

Column codes are report-specific and undocumented in the package. domainOverview and keywordOverview return entirely different columns. Always pass export_columns explicitly rather than relying on the default set — it makes your code self-documenting and reduces payload size and unit cost.

Cost control is your responsibility. Three levers: display_limit (fewer rows = fewer units), export_columns (fewer columns = fewer units), and caching. A careless get() in a loop over 500 domains can burn a month of units in an afternoon.

Do not log URLs. The key travels as a request parameter. Laravel's HTTP client logging, Telescope's request log, and exception messages containing the full URL are all places your key can leak. Scrub it.

get() is untyped by nature. It returns associative arrays with no schema guarantee. Validate that the keys you depend on exist before using them; an unexpected response shape should fail loudly at your boundary, not three layers deep.

Database codes matter more than you expect. us and uk return materially different volumes for the same domain. Make the database an explicit, visible parameter in your own service layer rather than relying on the env default, or you will eventually ship a report that silently mixes markets.

Extension point: if you need a report type repeatedly, write your own thin wrapper method around get() in an application-level subclass or a small dedicated class, rather than scattering raw get('some_type', [...]) calls. That keeps the params and column mappings in one testable place.

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.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast