Commit Graph
11 Commits
Author SHA1 Message Date
m105 4c0cc5a7f5 Add language filter to Discover
The CDX languages field (ISO 639 codes, e.g. "spa,eng") has been
captured on every page since the first ingestion command, but never
surfaced anywhere. Filters via FIND_IN_SET for an exact code match
rather than a LIKE substring match, since the column holds a
comma-separated list.

Verified against real data: lang=spa returns exactly the expected 47
sites (25+22 across two pages), a bogus code renders the empty state,
and lang=spa combined with tld=es correctly narrows to 20.
2026-07-22 23:11:30 +03:00
m105 59e152fd29 Show rel (nofollow/sponsored/ugc) badges on the backlinks table
The rel attribute has been captured on every backlink since that
table was added, but never displayed — a dofollow vs. nofollow
backlink is a real difference for SEO/competitive analysis, not just
a link count. Links with none of those tokens show as "dofollow".

Verified against real data (existing noopener-only link renders as
dofollow) plus a temporary synthetic nofollow+sponsored row to
confirm that branch too, removed after confirming.
2026-07-22 22:56:15 +03:00
m105 27f91c7fea Add platform filter to Discover, alongside keyword and TLD
Filters sites by server/powered_by/generator (e.g. "Shopify",
"WordPress", "nginx"), combinable with the existing keyword/TLD
filters. Results table now also shows each site's detected platform.

Verified: platform=Wix correctly isolates the one known Wix site,
tld=es+platform=nginx narrows to the 12 real nginx-hosted .es sites,
and the no-match case renders the empty state correctly.
2026-07-22 22:18:49 +03:00
m105 c7299fe928 Add tech-stack fingerprinting (server, powered-by, generator)
WarcRecordFetcher was splitting out and discarding the HTTP response
headers to get at the HTML body — now it keeps and parses them, so
Server/X-Powered-By headers get captured alongside the <meta
name="generator"> tag already sitting in the parsed DOM. No extra
requests: same WARC segment already fetched for --with-metadata.

Site copies these from its homepage capture, same as
title/description/keywords. New badges on the site detail page show
the detected platform (e.g. "Wix.com Website Builder") and server
signature at a glance.

Verified against real .es data: correctly detected Wix
("00padel.es"), nginx/Apache/Cloudflare server headers, and PHP
X-Powered-By headers across the batch.
2026-07-22 22:14:40 +03:00
m105 6e6be7d7c6 Add backlinks section to the site detail page
Shows inbound links (who links to this site, via Site::inboundLinks())
with the referring site, source page, and anchor text — paginated
separately from the Pages table via a distinct page-name query param
so both can paginate independently. Also surfaces a simple outbound
link count and a "N backlinks from M referring domains" summary.

Verified against real data: crawled podcastyradio.es (previously only
known as a backlink target from 00podcast.es) and confirmed its detail
page correctly shows that inbound link with the right anchor text.
2026-07-22 21:36:19 +03:00
m105 3d0b9a6523 Extract and store outbound links (backlinks) during content fetch
Adds a backlinks table: one row per <a href> on a captured page that
points at a different domain (same-site nav links are skipped).
Piggybacks on the existing --with-metadata WARC fetch/DOM-parse in
WarcRecordFetcher rather than a separate command/job, since extracting
links needs the same HTML already being downloaded for
title/description/content — no extra requests.

WarcRecordFetcher gains href resolution (absolute, protocol-relative,
root-relative, query-only, and plain relative hrefs) against the
page's own URL. CommonCrawlIndexService::saveBacklinks() replaces a
page's link set wholesale on each (re)fetch, so reruns stay
idempotent. Site gets outboundLinks()/inboundLinks() relations, the
latter keyed by domain string since a link's target site may not be
indexed yet.

Verified against real .es data: 781 backlinks extracted across 205
distinct target domains (social platforms, Trustpilot, genuine
inter-site links with correct anchor text), and a rerun of the same
fetch left the count unchanged.
2026-07-22 21:29:11 +03:00
m105 cefb846a19 Add competitor-discovery search (/discover)
The actual "who sells honey with a .es domain" feature: search sites
by keyword — matched against page title/description/keywords/body
text via a new MySQL fulltext index on site_pages — combined with an
optional TLD filter on the domain. Results are sites ranked by number
of matching pages. Shows nothing until a filter is entered, so it
doesn't just duplicate the plain Sites browse page.

Verified against real .es data pulled in for the test (dominio ->
4 matching sites, correctly ranked and TLD-filtered).
2026-07-22 21:22:14 +03:00
m105 f15eb5aec1 Add first pass of the web interface: Sites index + detail, via Livewire
Home page is now a Livewire-powered, searchable/paginated Site list
(search by domain) instead of the stock Laravel welcome page. Clicking
a site goes to a plain controller-rendered detail page listing its
pages. Shared layout at resources/views/components/layouts/app.blade.php.
2026-07-22 21:16:17 +03:00
m105 4dd5a3c17d Replace default Laravel README with project documentation
Explains what Huginn does (Common Crawl-backed site index for
competitor discovery), the two ingestion paths (query API vs bulk CDX
shard download), the Site/SitePage data model, and how to set up and
run the app locally.
2026-07-20 16:07:14 +03:00
m105 4dc9b84eb8 Add bulk CDX shard ingestion command as an API-independent fallback
crawlindex:bulk-fetch walks Common Crawl's CDX shard files directly on
data.commoncrawl.org (via cc-index.paths.gz), instead of the
index.commoncrawl.org query API — usable for bulk backfill/refresh or
as a fallback when that API is down. Shares the group/save pipeline
with crawlindex:fetch via a new SavesCommonCrawlCaptures trait.

Also fixes a real bug this surfaced under load: site_pages.urlkey was
a varchar(255), which is too narrow for real-world captured URLs
(long query strings on spam/malformed pages overflow it). Widened to
text and switched the uniqueness constraint to a sha1 hash column,
since MySQL can't fully index a TEXT column.
2026-07-20 00:26:27 +03:00
m105 1c9fc5d423 Initial commit: Laravel scaffold + Common Crawl site indexing
Adds Site/SitePage models and the crawlindex:fetch command, which pulls
CDX index records from Common Crawl and optionally enriches them with
page title/description/keywords/body text extracted from the WARC
archive. CDX/WARC access and the site/page upsert logic live in
CommonCrawlIndexService and WarcRecordFetcher.
2026-07-20 00:04:45 +03:00