m105 b181dd7e1c Add sitemap.xml/robots.txt and a clearer guest message on site pages
Sitemap lists the static pages plus one URL per indexed site (644
URLs currently); not sharded — fine at this scale, but the sitemap
protocol caps a single file at 50,000 URLs, so this will need a
sitemap index if the site count ever gets there. robots.txt moved
from a static public file to a route so it can point at the real
sitemap URL in any environment (a static file would also silently
shadow the dynamic route in production, since webservers serve
existing public/ files before hitting Laravel's router at all).

Site detail page: guests now get one prominent banner up top ("you're
viewing limited data — register or sign in to see everything") in
addition to the existing per-section "N of M results" notices, so the
overall gating is obvious immediately rather than only on scroll.
Also gave site pages a real per-page meta description (using the
site's own description/title) instead of falling back to the generic
site-wide one — worth doing now that these pages are actually in the
sitemap and indexable.

Caught and fixed a real Blade bug along the way: an inline
:description="..." expression on <x-layouts.app> with escaped
double-quoted PHP strings corrupted the compiler's directive parsing
in a way that only surfaced as a 500 on a page rendering the guest
@guest block below it (mismatched @if/@endif at compile time) — not
on the expression itself. Fixed by computing the description in a
@php block first and passing a plain variable instead.

Verified over real HTTP: sitemap.xml validates as well-formed XML
(644 <url> entries), robots.txt correctly emits the live sitemap URL,
the guest banner and site-specific meta description both render
correctly. Full test suite (31 tests) passes.
2026-07-25 16:25:08 +03:00

Huginn

Huginn builds a local, searchable index of websites sourced from Common Crawl — a public archive of billions of web pages. It's the data layer for a competitor-discovery tool: point it at a product and a market (e.g. "who sells honey with a .es domain?") and it should be able to answer from the indexed data, without live-scraping the web yourself.

How it works

Common Crawl publishes a monthly URL index ("CDX") that maps every page it captured to metadata (status, mime type, timestamp) and a location inside one of its WARC archive files. Huginn pulls from that index two ways:

  1. index.commoncrawl.org — the query API. Good for targeted lookups: a specific URL, host, domain, or an entire TLD, paginated.
  2. data.commoncrawl.org — the raw index files. The same data as static, downloadable shard files (cdx-*.gz), listed per collection in cc-index.paths.gz. Good for bulk ingestion, and doesn't depend on the query API being up.

Either way, matching captures are grouped into Sites (one per domain) and SitePages (one per unique URL within that site), and optionally enriched with the page's title/description/keywords/body text — fetched straight out of the relevant WARC file via an HTTP range request, so only the bytes for that one page are downloaded.

Data model

  • Site — a domain (e.g. example.com) within a given Common Crawl collection, with its homepage's title/description/keywords.
  • SitePage — one captured URL belonging to a Site: its CDX metadata (status, mime, timestamp, WARC filename/offset/length) plus, if fetched, its title/description/keywords/body text.

Artisan commands

crawlindex:fetch — query the CDX API for a specific URL/host/domain, or browse a whole TLD, and save the results.

# A specific domain
php artisan crawlindex:fetch example.com

# Every page under example.com (prefix match)
php artisan crawlindex:fetch "example.com/*" --match-type=prefix

# Browse an entire TLD, walking every index page, enriching each page
php artisan crawlindex:fetch --tld=es --all-pages --with-metadata

Key options: --collection (defaults to the latest), --match-type (exact|prefix|host|domain), --page/--pages/--all-pages for pagination, --with-metadata to fetch page content (slower — one extra request per page).

crawlindex:bulk-fetch — walk Common Crawl's CDX shard files directly from data.commoncrawl.org, bypassing the query API entirely. Useful for bulk backfill/refresh of the database, or as a fallback when the query API is offline.

# One shard, filtered locally to .es domains
php artisan crawlindex:bulk-fetch --collection=CC-MAIN-2026-25 --tld=es --shard-count=1

# Every shard in the collection (large — see caveat below)
php artisan crawlindex:bulk-fetch --collection=CC-MAIN-2026-25 --all-shards

Key options: --collection, --tld (filters locally, not via the API), --shard-start/--shard-count/--all-shards to bound how many of a collection's ~300 CDX shard files get processed in one run.

Scale caveat: a monthly collection's CDX index is spread across ~300 shard files, each downloaded and decompressed in full. Running --all-shards (or crawlindex:fetch --all-pages without a --tld) over a whole collection means processing on the order of billions of records — pair broad runs with --tld (or a specific URL) to keep them bounded, and be especially careful combining --with-metadata with a broad run, since it adds one extra WARC request per page.

Setup

Requirements: PHP 8.3+, Composer, Node/npm, and a MySQL database.

composer install
npm install

cp .env.example .env
php artisan key:generate

Edit .env and point DB_* at your MySQL instance, then run migrations:

php artisan migrate

Start the app (server, queue worker, and Vite dev server together):

composer run dev

Then populate the database with one of the crawlindex:* commands above.

S
Description
No description provided
Readme
210 KiB
Languages
PHP 65.5%
Blade 34%
JavaScript 0.5%