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.
Adds sites.root_domain (indexed string, backfilled for existing rows)
computed via a naive "last two labels" heuristic (App\Support\RootDomain)
— good enough for example.com/somesite.es, but wrong for two-part
public suffixes like .co.uk. Doing that correctly needs the Mozilla
Public Suffix List; flagged as a known simplification rather than
built now.
Deliberately not a hard FK: like Backlink.target_domain, a subdomain
can get indexed before its root domain does (or vice versa), so
Site::subdomains()/rootSite() match on the domain string rather than
requiring the relationship to exist at write time.
Site detail page: a root domain shows a paginated "Subdomains"
section (same guest-preview cap as Pages/Backlinks); a subdomain
shows "Subdomain of {root}" linking back, when the root is itself
indexed.
Verified against real data: 21k.ro correctly lists its two known
subdomains (buzau.21k.ro, timisoara.21k.ro), each of which correctly
links back to 21k.ro. Added SubdomainsTest (root lists subdomains,
subdomain links to root, empty state) — full suite (31 tests) passes.
Hand-rolled login/register/logout on Laravel's built-in Auth facade
rather than Breeze/Jetstream, which scaffold Tailwind v3 config and
would fight our existing Tailwind v4 CSS-first setup. Login/Register
are Livewire components; logout is a plain POST route (auth-gated,
CSRF-protected).
Routing: "/" is now a public landing page describing the project;
the sites list moved from "/" to "/sites" to make room for it.
Data gating: App\Support\GuestPreview::paginate() forces page 1 and a
5-row page size for unauthenticated visitors on Sites, Discover, and
a site's Pages/Backlinks tables, while total() still reflects the
real match count — authenticated users get the existing full
pagination unchanged. A shared guest-preview-notice component shows
"N of M results, sign in to see all" and only renders when something
is actually being hidden.
Testing: pointed the test suite at a dedicated MySQL database
(huginn_testing) instead of the default in-memory SQLite, since the
existing fulltext index migration isn't SQLite-compatible — this app
is MySQL-specific by now regardless. Added AuthFlowTest (register,
login, wrong-password rejection, logout) and GuestPreviewTest (guest
capped to a preview, authenticated sees everything), all passing.
Also fixed a real bug the tests caught: request()->session() isn't
reliably attached during Livewire's in-process test action calls;
switched Login/Register to the session() helper, which resolves the
session store directly and works in both contexts.
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.
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.
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.
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.