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

Wappalyzer Laravel Package

madeitbelgium/wappalyzer

madeitbelgium/wappalyzer is a Laravel/PHP wrapper for Wappalyzer that detects the technologies behind any website, including CMSs, frameworks, analytics, servers, and more. Use its simple API to identify tech stacks for audits, lead research, or competitive analysis.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package via Composer. It is a Laravel package, so the service provider and Wappalyzer facade are auto-discovered on Laravel 5.5+:

composer require madeitbelgium/wappalyzer

Publish the config file so you can tune HTTP behaviour (timeout, user-agent, rules path) without touching vendor code:

php artisan vendor:publish --provider="MadeItBelgium\Wappalyzer\WappalyzerServiceProvider"

This drops a config/wappalyzer.php into your app. On older Laravel versions register MadeItBelgium\Wappalyzer\WappalyzerServiceProvider in config/app.php and add the facade alias manually.

First call — hand it a URL and get back the detected technologies:

use MadeItBelgium\Wappalyzer\Facades\Wappalyzer;

$technologies = Wappalyzer::analyze('https://example.com');

foreach ($technologies as $tech) {
    // name, categories, and often version/confidence depending on release
    dump($tech);
}

It also works outside Laravel by instantiating the underlying class directly:

$wappalyzer = new \MadeItBelgium\Wappalyzer\Wappalyzer();
$result = $wappalyzer->analyze('https://example.com');

Where to look first:

  1. The README for the exact method signatures — the public API (analyze, setUrl, result shape) has shifted between releases, so match it to your pinned version.
  2. config/wappalyzer.php — timeouts and user-agent matter more than you think for detection accuracy.
  3. The bundled technology rules (the Wappalyzer technologies.json equivalent) — this file is the detector. Everything the package can recognise lives there, and it is the first thing you will want to override or refresh.
  4. tests/ — the fastest way to see the expected input/output contract without reading the ported JS logic.

Canonical first use case: domain enrichment. You have a list of company domains (leads, signups, vendors) and you want to tag each with their stack — Shopify vs. WooCommerce, WordPress vs. Next.js, Cloudflare, GA4, HubSpot — then use those tags for segmentation, routing, or competitive reporting. A single analyze() call in a tinker session against your own site is the right smoke test before wiring it into a pipeline.

Implementation Patterns

Wrap the facade in your own service. Don't sprinkle Wappalyzer::analyze() through controllers. Bind a TechnologyDetector interface to a class that wraps the package, so you can swap the engine (headless browser, third-party API) later and mock it in tests:

class WappalyzerDetector implements TechnologyDetector
{
    public function detect(string $url): Collection
    {
        try {
            return collect(Wappalyzer::analyze($url))
                ->map(fn ($t) => TechnologyData::fromArray($t));
        } catch (\Throwable $e) {
            report($e);
            return collect();
        }
    }
}

Always scan in a queued job, one URL per job. Each scan is an outbound HTTP request plus a regex pass over a large rules file. Doing that inline in a web request with more than a handful of URLs will blow your response time and possibly your memory limit:

class DetectTechnologies implements ShouldQueue
{
    public $tries = 3;
    public $backoff = [10, 60, 300];

    public function __construct(public int $companyId) {}

    public function handle(TechnologyDetector $detector): void
    {
        $company = Company::findOrFail($this->companyId);
        $techs = $detector->detect($company->normalized_domain);

        $company->technologies()->sync(
            $techs->mapWithKeys(fn ($t) => [$t->slug => [
                'version' => $t->version,
                'detected_at' => now(),
            ]])->all()
        );
    }
}

Normalise into relational tables, not a JSON blob. A technologies table (slug, name, categories) plus a company_technology pivot with version and detected_at lets you query "all leads on Shopify with Klaviyo" with plain Eloquent. Keep a raw JSON column too if you want the audit trail, but don't build reporting on it.

Cache aggressively by domain. Site stacks change on the order of months, not minutes. Cache the raw result for 7–30 days keyed on the registrable domain (strip www, lowercase, handle punycode). This turns a re-scan of your CRM into a cache read:

$techs = Cache::remember("wappalyzer:{$domain}", now()->addDays(14), fn () =>
    Wappalyzer::analyze("https://{$domain}")
);

Reuse already-fetched HTML when you can. If your crawler or link-preview service already pulled the page, you're paying for the request twice. Where the installed version allows passing a body/headers instead of a URL, do it; otherwise fetch once with Guzzle and hand the same response to both the scanner and your own parsing.

Batch with a command and a rate limiter. For backfills, use chunkById() and Laravel's RateLimiter or a simple usleep() so you don't hammer targets (or get your IP banned). Emit progress so a stuck job is obvious:

Company::whereNull('scanned_at')->chunkById(100, function ($companies) {
    foreach ($companies as $company) {
        RateLimiter::attempt("scan:{$company->id}", 1, fn () =>
            DetectTechnologies::dispatch($company->id)
        , 1);
    }
});

Emit a domain event on detection. A TechnologiesDetected event lets lead scoring, segmentation, and Slack notifications subscribe without coupling to the scanner. This is the extension point that keeps the package at arm's length.

Layer signals for a fuller profile. Technology detection is one column in a company profile. Pair it with DNS records, TLS certificate issuer, HTTP headers, and WHOIS — then a "is this a Shopify store on Cloudflare" check becomes a single query instead of several integrations.

Derive categories, not just names. Wappalyzer tags each technology with categories (ecommerce, CMS, analytics, CDN). Store those and you can build a "sales-ready ecommerce lead" segment without maintaining a hardcoded list of 400 product names.

Gotchas and Tips

  • GPL-3.0 licence. The package itself is GPL-3.0. Using it as a dependency inside a hosted SaaS is generally fine, but distributing a modified version — or shipping it inside a closed-source product you hand to customers — carries copyleft obligations. Get this reviewed before it lands in anything you redistribute, and definitely before you fork the detection rules.
  • No JavaScript execution. This is a passive HTML/header scanner. Anything injected client-side — React/Next hydration markers, GTM-loaded scripts, SPAs that render their stack at runtime — will be missed. If you need reliable SPA detection, put a headless browser (Dusk/Panther/Puppeteer microservice) upstream and feed the rendered HTML in.
  • The ruleset goes stale. Wappalyzer v7 moved to a closed commercial model, so the bundled rules age quickly. New frameworks simply won't be recognised. Plan for a configurable rules path and a periodic refresh you control, and treat "not detected" as "unknown", not "absent".
  • Failures are per-URL, not fatal. Timeouts, redirect loops, 403s for bot-looking user-agents, expired TLS, and DNS failures all happen in bulk scans. Wrap each call in try/catch, record the failure, and move on — one dead domain should never abort a batch. Set a realistic timeout (5–10s) and a browser-like user-agent in config.
  • SSRF risk. Never pass arbitrary user-supplied URLs straight to the scanner from inside your network. Validate with filter_var($url, FILTER_VALIDATE_URL), resolve the host, and reject private/link-local ranges and metadata endpoints.
  • Parse cost, not just network cost. The technology rules file is large; regex-matching it against every page is CPU-heavy. In long-running queue workers, be deliberate about whether you reuse an instance or let it be released — and cache the compiled rules if the package exposes that.
  • Treat results as signals, not facts. A single wp-content string tags "WordPress"; a script present in a CDN bundle gets attributed to every site using it. False positives are common and versions are best-effort and frequently absent — never build logic that requires a version string.
  • Facade alias collision. The alias Wappalyzer is generic. Import MadeItBelgium\Wappalyzer\Facades\Wappalyzer explicitly in every file rather than relying on the global alias, or you'll fight another package for the name.
  • Config drift on upgrade. Published config files don't update with the package. After composer update, diff your config/wappalyzer.php against the vendor default — new options (timeouts, rules path, user-agent) silently fall back to package defaults otherwise.
  • Pin the version. This is a small package (16 stars, last release April 2024) with a thin maintenance surface. Pin a minor version in composer.json, cover the wrapper with your own tests, and keep the TechnologyDetector abstraction so a replacement engine is a one-class swap rather than a refactor.
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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource