Adds the auth Breeze scaffolds that were missing before: password
reset and email verification, plus a profile page (update name/email,
change password, delete account). User now implements MustVerifyEmail
so verification is actually enforced, not just scaffolded.
This does mean accepting Tailwind v3 (postcss/tailwind.config.js)
over the v4 CSS-first setup the app started with, and Livewire drops
from ^4.3 to ^3.6.4 — both forced by Breeze/Volt's current
compatibility requirements. Restored the Instrument Sans font Breeze's
installer dropped so styling stays close to what we had.
Reconciliation, since Breeze's installer assumes a blank-slate app:
- Deleted the hand-rolled Login/Register Livewire components/views/
test — superseded by Breeze's Volt-based auth pages
(resources/views/livewire/pages/auth/*), which also cover
forgot-password, reset-password, verify-email, and confirm-password.
- Deleted Breeze's own default welcome/dashboard views and its
separate app layout + nav component — kept our existing
components/layouts/app.blade.php as the one real app layout, used
Breeze's minimal layouts/guest.blade.php only for the auth pages.
- Rewrote routes/web.php merging our routes (home, sites, discover)
with routes/auth.php; replaced every hardcoded route('dashboard')
reference (5 Blade files plus, easy to miss, a plain PHP controller
VerifyEmailController) with sites.index.
- Logout: Breeze's Actions\Logout::__invoke() returns void, so wired
directly as a route it silently no-ops instead of redirecting;
fixed with a thin closure that calls it then redirects.
- Fixed Breeze's own generated tests (Auth/AuthenticationTest,
RegistrationTest, EmailVerificationTest, PasswordConfirmationTest)
where they assumed the deleted dashboard route/nav component.
Verified: full suite (28 tests, including Breeze's Registration/
Authentication/PasswordReset/PasswordConfirmation/PasswordUpdate/
EmailVerification/Profile tests plus our own GuestPreviewTest) passes
against the huginn_testing MySQL database. Also smoke-tested for real:
all key pages return 200, /profile redirects guests to /login, guest
preview capping still works, and a real password-reset email was
generated and logged with correct branding and a working link.
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:
index.commoncrawl.org— the query API. Good for targeted lookups: a specific URL, host, domain, or an entire TLD, paginated.data.commoncrawl.org— the raw index files. The same data as static, downloadable shard files (cdx-*.gz), listed per collection incc-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(orcrawlindex:fetch --all-pageswithout 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-metadatawith 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.