Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/(marketing)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Link from "next/link";
import { HeroAuditForm } from "@/components/hero-audit-form";
import { ANON_DAILY_SCANS, SCAN_CREDITS, SIGNUP_CREDITS } from "@/lib/credits";
import { OrganizationJsonLd, SoftwareApplicationJsonLd, FaqJsonLd } from "@/components/json-ld";
import { PerformancePreview } from "@/components/report/performance-preview";
import { UptimePreview } from "@/components/report/uptime-preview";
Expand Down Expand Up @@ -33,7 +34,7 @@ const faqs = [
},
{
q: "Is the free tier really free?",
a: "Yes — anonymous visitors get 10 audits per day per IP with no signup, and signing up unlocks 20 free credits (1 AI-model scan). Paid scans cost 20 credits (~$1) with volume discounts down to $0.50/scan at the 100-scan pack. No subscription, credits never expire.",
a: `Yes — anonymous visitors get ${ANON_DAILY_SCANS} audits per day per IP with no signup, and signing up unlocks ${SIGNUP_CREDITS} free credits (${SIGNUP_CREDITS / SCAN_CREDITS} AI-model scan). Paid scans cost ${SCAN_CREDITS} credits (~$1) with volume discounts down to $0.50/scan at the 100-scan pack. No subscription, credits never expire.`,
},
];

Expand Down
13 changes: 8 additions & 5 deletions app/(marketing)/pricing/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import Link from "next/link";
import {
ANON_DAILY_SCANS,
CREDIT_PACKS,
CREDIT_RACK_CENTS,
ENGINES,
LEAD_RUN_CREDITS,
OUTREACH_CREDITS,
SCAN_CREDITS,
SIGNUP_CREDITS,
discountPct,
dollars,
perCreditCents,
perCreditLabel,
} from "@/lib/credits";

export const metadata = {
Expand All @@ -30,7 +32,7 @@ export default function PricingPage() {
<h1 className="text-center text-4xl font-extrabold">Pay per scan</h1>
<p className="mx-auto mt-2 max-w-2xl text-center text-[var(--color-muted)]">
Rule-based scans are free up to the daily limit. AI-model scans cost{" "}
<strong>20 credits (~$1) each</strong>. Buy credits with crypto via CoinPay;
<strong>{SCAN_CREDITS} credits (~$1) each</strong>. Buy credits with crypto via CoinPay;
no subscription, no expiry.
</p>
<div className="mx-auto mt-8 grid max-w-6xl gap-4 md:grid-cols-2 lg:grid-cols-3">
Expand Down Expand Up @@ -131,7 +133,7 @@ export default function PricingPage() {
<div className="mt-1 text-sm text-[var(--color-muted)]">
{p.credits} credit{p.credits === 1 ? "" : "s"} ·{" "}
<span className="font-mono text-xs">
{dollars(perCreditCents(p))}/credit
{perCreditLabel(p)}/credit
</span>
</div>
<Link
Expand All @@ -146,8 +148,9 @@ export default function PricingPage() {
</div>

<p className="mt-8 text-center text-xs text-[var(--color-muted)]">
Sign-ups include 20 free credits (1 AI-model scan). Anonymous visitors get 10
free rule-based scans per day per IP.
Sign-ups include {SIGNUP_CREDITS} free credits ({SIGNUP_CREDITS / SCAN_CREDITS}{" "}
AI-model scan). Anonymous visitors get {ANON_DAILY_SCANS} free rule-based scans
per day per IP.
</p>
</main>
);
Expand Down
107 changes: 107 additions & 0 deletions app/(marketing)/tools/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import Link from "next/link";
import { ANON_DAILY_SCANS, ENGINES, SIGNUP_CREDITS, type Engine } from "@/lib/credits";

// Every free scanner, in one place.
//
// Six of the engines cost nothing, but only two of them — the rule-based AEO
// audit and the Slop Score — can run without an account, and those two are the
// only ones the homepage selector offers. So the other four were real, free,
// and undiscoverable: /pricing listed them among the paid engines, and nothing
// linked anywhere.
//
// The split is stated rather than hidden. Putting all six in the anonymous
// selector would be the obvious "fix" and the wrong one — four of them would
// fail on submit, because `ANON_ENGINES` in app/actions/runAudit.ts genuinely
// only permits two. Telling somebody a tool needs an account is a better
// experience than letting them find out by having it not work.

export const metadata = {
title: "Free website scanners",
description:
"Every free CrawlProof scanner: AEO audit, Slop Score, DNS analyzer, link checker, specification.website checklist and the Vu1nz security scan.",
alternates: { canonical: "/tools" },
};

/** Runs without an account — kept in sync with ANON_ENGINES in app/actions/runAudit.ts. */
const ANONYMOUS: Engine[] = ["rule", "slop"];

/** Where each free engine can be started, when it has its own entry point. */
const START_AT: Partial<Record<Engine, string>> = {
rule: "/",
slop: "/slop",
};

export default function ToolsPage() {
const free = (Object.keys(ENGINES) as Engine[]).filter(
(k) => ENGINES[k].cost === 0 && ENGINES[k].available,
);
const anonymous = free.filter((k) => ANONYMOUS.includes(k));
const needsAccount = free.filter((k) => !ANONYMOUS.includes(k));

return (
<main className="mx-auto max-w-5xl px-4 sm:px-6 py-16">
<h1 className="text-center text-4xl font-extrabold">Free website scanners</h1>
<p className="mx-auto mt-3 max-w-2xl text-center text-[var(--color-muted)]">
{free.length} scanners that cost nothing to run. {anonymous.length} need no
account at all — anonymous visitors get {ANON_DAILY_SCANS} scans per day per
IP. The rest are free once you sign up, which also includes{" "}
{SIGNUP_CREDITS} credits for the AI-model audits.
</p>

<section className="mt-12">
<h2 className="text-2xl font-bold">No account needed</h2>
<div className="mt-4 grid gap-4 sm:grid-cols-2">
{anonymous.map((k) => (
<ToolCard key={k} engine={k} href={START_AT[k]} cta="Run it free" />
))}
</div>
</section>

<section className="mt-12">
<h2 className="text-2xl font-bold">Free with an account</h2>
<p className="mt-1 text-sm text-[var(--color-muted)]">
Still zero credits to run — signing in is what makes them available.
</p>
<div className="mt-4 grid gap-4 sm:grid-cols-2">
{needsAccount.map((k) => (
<ToolCard key={k} engine={k} href="/signup" cta="Sign up free" />
))}
</div>
</section>

<p className="mt-12 text-center text-sm text-[var(--color-muted)]">
Looking for the AI-model audits?{" "}
<Link href="/pricing" className="underline">
See pricing
</Link>
.
</p>
</main>
);
}

function ToolCard({
engine,
href,
cta,
}: {
engine: Engine;
href?: string;
cta: string;
}) {
const meta = ENGINES[engine];
return (
<div className="card flex flex-col p-5">
<div className="text-xs uppercase tracking-wider text-[var(--color-muted)]">
Free · 0 credits
</div>
<h3 className="mt-1 text-lg font-bold">{meta.label}</h3>
<p className="mt-2 flex-1 text-sm text-[var(--color-muted)]">{meta.blurb}</p>
{href && (
<Link href={href} className="btn mt-4 w-full">
{cta}
</Link>
)}
</div>
);
}
3 changes: 2 additions & 1 deletion app/llms.txt/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SCAN_CREDITS, SIGNUP_CREDITS } from "@/lib/credits";
import { env } from "@/lib/env";

const body = `# CrawlProof
Expand All @@ -6,7 +7,7 @@ const body = `# CrawlProof

## Product
- Free single-URL AEO audit, 10 per day per IP, no signup required.
- Signed-in users get 20 free credits (1 AI-model scan); each AI-model scan costs 20 credits (~$1, volume discounts down to $0.50/scan at the 100-scan pack). No subscription — credits never expire.
- Signed-in users get ${SIGNUP_CREDITS} free credits (${SIGNUP_CREDITS / SCAN_CREDITS} AI-model scan); each AI-model scan costs ${SCAN_CREDITS} credits (~$1, volume discounts down to $0.50/scan at the 100-scan pack). No subscription — credits never expire.
- Saved projects, scheduled re-audits, multi-engine LLM scans (Claude Sonnet 4.6, OpenAI GPT-5 Mini, Gemini 2.5 Pro, Perplexity Sonar Pro, Qwen Plus, Kimi v2.6, DeepSeek V4, Z.AI GLM-5.2), consolidated PDF reports, and diff view.
- Identifies as CrawlProofBot/1.0 (+https://crawlproof.com/bot).

Expand Down
3 changes: 3 additions & 0 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
{ url: `${base}/pricing`, changeFrequency: "monthly", priority: 0.9 },
{ url: `${base}/hire`, changeFrequency: "monthly", priority: 0.9 },
{ url: `${base}/slop`, changeFrequency: "monthly", priority: 0.9 },
// Hub for the six free scanners — the four that need an account had no
// inbound link anywhere before this page existed.
{ url: `${base}/tools`, changeFrequency: "monthly", priority: 0.9 },
{ url: `${base}/get-guide`, changeFrequency: "monthly", priority: 0.9 },
{ url: `${base}/about`, changeFrequency: "monthly", priority: 0.7 },
{ url: `${base}/press`, changeFrequency: "monthly", priority: 0.6 },
Expand Down
12 changes: 11 additions & 1 deletion components/hero-audit-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { startAuditFromForm } from "@/app/actions/runAudit";

// Datafa.st injects `window.datafast` once its script.js loads. Calls
Expand Down Expand Up @@ -134,7 +135,16 @@ export function HeroAuditForm({ defaultScan = "rule" }: { defaultScan?: HeroScan
</Field>
<p className="text-xs leading-relaxed text-[var(--color-muted)]">
No email, no signup, no card. The report opens on-page in seconds — you
can email yourself a PDF copy from the report once it&apos;s ready.
can email yourself a PDF copy from the report once it&apos;s ready.{" "}
{/* Only two of the six free scanners can run anonymously, so the other
four were invisible from here — real, free, and reachable by nobody.
A link rather than four more radio buttons: the rest genuinely
require an account, and offering them in this selector would mean
submitting a form that cannot succeed. */}
<Link href="/tools" className="underline">
All free tools
</Link>
.
</p>
<div className="border-t border-[var(--color-border)] pt-3">
<label className="flex items-start gap-2 text-xs text-[var(--color-muted)]">
Expand Down
4 changes: 2 additions & 2 deletions components/json-ld.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { env } from "@/lib/env";
import { CREDIT_PACKS } from "@/lib/credits";
import { ANON_DAILY_SCANS, CREDIT_PACKS, SIGNUP_CREDITS } from "@/lib/credits";

function Tag({ data }: { data: unknown }) {
return (
Expand Down Expand Up @@ -51,7 +51,7 @@ export function SoftwareApplicationJsonLd() {
price: "0",
priceCurrency: "USD",
description:
"10 anonymous audits/day per IP, plus 20 free credits on signup.",
`${ANON_DAILY_SCANS} anonymous audits/day per IP, plus ${SIGNUP_CREDITS} free credits on signup.`,
},
// Real catalog — pay-per-scan credit packs, no subscription tier.
...CREDIT_PACKS.map((p) => ({
Expand Down
104 changes: 66 additions & 38 deletions components/report/email-report-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import { requestReportPdf } from "@/app/actions/reportPdf";

// Post-report PDF capture. Shown at the BOTTOM of /r/<token>, after the
// findings — value-first, so we ask for an email only once the visitor has
// already seen the report. Email is required (it's where the PDF goes);
// phone and monthly sales stay optional.
// already seen the report. Email is the only thing asked for up front; the
// commercial questions (monthly sales, phone) appear only behind an explicit
// "estimate what this costs me" action, and remain optional even then.
export function EmailReportForm({
token,
complete,
Expand All @@ -19,6 +20,9 @@ export function EmailReportForm({
const [phone, setPhone] = useState("");
const [monthlySales, setMonthlySales] = useState("");
const [marketingOptIn, setMarketingOptIn] = useState(false);
// Revealed only when the visitor asks for the impact estimate — see the
// note on the form body.
const [wantsEstimate, setWantsEstimate] = useState(false);
const [error, setError] = useState<string | null>(null);
const [done, setDone] = useState<null | "emailed" | "queued">(null);

Expand Down Expand Up @@ -70,46 +74,70 @@ export function EmailReportForm({
: "Drop your email and we'll send the PDF the moment this scan finishes."}
</p>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<Field label="Email">
<input
type="email"
placeholder="you@example.com"
value={email}
onChange={(e) => {
setEmail(e.target.value);
if (error) setError(null);
}}
className="input"
autoComplete="email"
/>
</Field>
<Field label="Phone" helper="Optional.">
<input
type="tel"
placeholder="+1 555 123 4567"
value={phone}
onChange={(e) => setPhone(e.target.value)}
className="input"
autoComplete="tel"
/>
</Field>
</div>
<Field
label="Monthly website sales"
helper="Optional. Used only to estimate the possible revenue impact of crawlability issues."
>
{/* Email alone, and nothing else, until the visitor asks for more.

The form previously showed three inputs for what is a one-field
request: a phone number and "monthly website sales" sat next to the
email, both marked optional. Optional or not, being asked your
revenue in order to receive a PDF reads as a sales qualification,
and it is the wrong trade at the moment somebody is doing us the
favour of handing over an address. The commercial questions are
worth asking — but behind something the visitor chose, for a thing
they get in return. */}
<Field label="Email">
<input
type="number"
inputMode="decimal"
min="0"
step="any"
placeholder="USD"
value={monthlySales}
onChange={(e) => setMonthlySales(e.target.value)}
type="email"
placeholder="you@example.com"
value={email}
onChange={(e) => {
setEmail(e.target.value);
if (error) setError(null);
}}
className="input"
autoComplete="email"
/>
</Field>

{!wantsEstimate ? (
<button
type="button"
onClick={() => setWantsEstimate(true)}
className="self-start text-sm underline text-[var(--color-muted)]"
>
Also estimate what these issues might cost me
</button>
) : (
<div className="flex flex-col gap-3 border-l-2 border-[var(--color-border)] pl-3">
<p className="text-xs leading-relaxed text-[var(--color-muted)]">
We&apos;ll add a revenue-impact estimate to the report. Both fields are
optional — the PDF sends either way.
</p>
<Field label="Monthly website sales">
<input
type="number"
inputMode="decimal"
min="0"
step="any"
placeholder="USD"
value={monthlySales}
onChange={(e) => setMonthlySales(e.target.value)}
className="input"
autoFocus
/>
</Field>
<Field label="Phone" helper="Only if you'd like us to walk through it.">
<input
type="tel"
placeholder="+1 555 123 4567"
value={phone}
onChange={(e) => setPhone(e.target.value)}
className="input"
autoComplete="tel"
/>
</Field>
</div>
)}

<button type="submit" className="btn btn-primary" disabled={pending}>
{pending ? "Sending…" : "Email me the PDF"}
</button>
Expand Down
5 changes: 4 additions & 1 deletion components/report/performance-charts.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use client";

import { SCAN_CREDITS, SIGNUP_CREDITS } from "@/lib/credits";

import dynamic from "next/dynamic";
import Link from "next/link";

Expand Down Expand Up @@ -84,7 +86,8 @@ export function PerformanceCharts({ overlayTitle, overlayBody }: PerformanceChar
Sign up free
</Link>
<p className="mt-2 text-xs text-[var(--color-muted)]">
3 free AI credits on signup · free rule-based scans up to the daily limit.
{SIGNUP_CREDITS} free credits on signup ({SIGNUP_CREDITS / SCAN_CREDITS} AI-model
scan) · free rule-based scans up to the daily limit.
</p>
</div>
</div>
Expand Down
Loading
Loading