Skip to content
Deal212 Audit

Findings

Categorized findings

Every issue observed on the Deal212 public website, grouped by focus area. Each card shows what was observed, why it matters, a fix or reference, and a remediation checklist. Use the severity filter to focus a sweep.

Critical & Functional

5

CR-01

SPA catch-all returns HTTP 200 for unknown routes, /robots.txt, and /sitemap.xml

Critical

Verified on live site

What was observed

Requests to nonexistent paths, /robots.txt, and /sitemap.xml all return the single-page app's homepage document with HTTP 200 and Content-Type text/html, instead of a 404 or the expected text/XML files. The catch-all route serves index.html for everything, including paths that should resolve to static files.

Why it matters

Search engines and crawlers cannot read the crawl rules or the true page inventory, and social platforms cannot preview pages. Unknown routes silently return valid-looking content, which can index duplicates and error pages, inflate crawl budget, and hide real defects. Neither bots nor users get any signal that a page does not exist.

Fix or reference

Cloudflare Pages - _redirects
# Serve the real static files directly; let only known SPA routes and assets
# fall through to the app shell. Everything else gets a real 404.
/robots.txt    200
/sitemap.xml   200

# Force a true 404 for anything that is not a real route or asset (SPA fallback).
Cloudflare Worker (routing)
const SPA_PATHS = new Set(["/", "/check", "/architecture"]);

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    const { pathname } = url;

    // Let real static assets through untouched.
    if (/^\/assets\/|^\/(robots\.txt|sitemap\.xml)$/.test(pathname)) {
      return fetch(request);
    }

    // Unknown paths must not get the shell: return a true 404.
    if (!SPA_PATHS.has(pathname)) {
      return new Response("Not Found", { status: 404 });
    }

    return fetch(request); // serve the app shell for known routes
  },
};
robots.txt
User-agent: *
Allow: /

Sitemap: https://deal212.sites.tab.bot/sitemap.xml
sitemap.xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://deal212.sites.tab.bot/</loc>
    <lastmod>2026-10-22</lastmod>
  </url>
  <url>
    <loc>https://deal212.sites.tab.bot/check</loc>
    <lastmod>2026-10-22</lastmod>
  </url>
</urlset>

Remediation checklist

0 of 3 complete

HI-01

POST /api/lead has no rate limiting, CSRF protection, or server-side validation

High

Verified on live site

What was observed

The lead-intake endpoint accepts POST /api/lead. There is no evidence of per-IP, per-window rate limiting, no visible CSRF origin check or token, and no server-side length or type validation before processing. The submission origin is not verified.

Why it matters

An unthrottled, cross-site-callable intake endpoint is an open invitation to spam and abuse. It can be replayed at scale and triggered from attacker-controlled pages, and malformed or oversized payloads flow straight into the handler and whatever it stores.

Fix or reference

Rate limiter (in-memory)
// Per-IP, sliding-window limiter. Use an edge KV or Redis in production.
const WINDOW_MS = 60_000;
const MAX = 5;
const hits = new Map();

function denied(ip) {
  const now = Date.now();
  const bucket = (hits.get(ip) ?? []).filter((t) => now - t < WINDOW_MS);
  if (bucket.length >= MAX) {
    hits.set(ip, bucket);
    return true;
  }
  bucket.push(now);
  hits.set(ip, bucket);
  return false;
}

export default async (request) => {
  const ip = request.headers.get("CF-Connecting-IP") ?? "unknown";
  if (denied(ip)) {
    return new Response("Too many requests", { status: 429 });
  }
  // ... continue processing the lead
};
zod validation schema
import { z } from "zod";

const LeadSchema = z.object({
  name: z.string().trim().min(1).max(120),
  email: z.string().trim().email().max(200),
  phone: z.string().trim().max(40).optional().or(z.literal("")),
  address: z.string().trim().max(200).optional().or(z.literal("")),
  // honeypot: must stay empty for a real user
  website: z.string().trim().max(0),
});

// Origin check: reject cross-site, state-changing requests.
const originOk = request.headers.get("Origin") !==
  "https://deal212.sites.tab.bot";

const parsed = LeadSchema.safeParse(await request.json());
if (!parsed.success || !originOk) {
  return new Response("Bad request", { status: 400 });
}

Remediation checklist

0 of 4 complete

HI-02

Honeypot field must be hidden from assistive technology

High

Needs verification

What was observed

The contact form uses a hidden honeypot field to deter bots, but the exact markup could not be confirmed from the rendered page, which is what the recommendation relies on.

Why it matters

A honeypot that is only display:none can still be announced by a screen reader or reached by keyboard, turning it into a confusing, inaccessible control. To be effective and accessible, it must be hidden from assistive technology so it is only ever reached by automated bots.

Fix or reference

Accessible honeypot field
<input
  type="text"
  name="website"
  tabindex="-1"
  autocomplete="off"
  aria-hidden="true"
  class="hidden"          <!-- or position:absolute; left:-9999px etc. -->
/>

Remediation checklist

0 of 3 complete

MD-01

The literal text '&amp;' is rendered instead of '&' on /check

Medium

Verified on live site

What was observed

The Property Check page renders the literal text '&amp;' where an '&' should appear, indicating a string was HTML-escaped twice before rendering.

Why it matters

An escaped ampersand is visible to users as raw markup and signals a double-escaping bug in the render path, which can quietly corrupt other text in the same templates.

Fix or reference

React fix - render a text node
// Wrong: the string was escaped twice and is rendered as `&amp;`.
// const title = "Dept &amp; Finance";

// Right: a plain text node renders the raw character once.
const title = "Dept & Finance";

// In JSX, the raw source is escaped exactly once.
<p>{title}</p>

Remediation checklist

0 of 2 complete

MD-02

Stray 'Document ×' fragment appears at the end of /check

Medium

Verified on live site

What was observed

The text 'Document ×' appears at the end of the /check page, a leftover from an unclosed or orphaned modal template.

Why it matters

The residue renders an orphan close glyph and unfinished headline to every visitor, telling users and audit tools that the page is shipping incomplete template markup.

Fix or reference

Remove the residue
<!-- Remove this stray fragment entirely. Do not render hidden modals into the DOM. -->
<!-- <div class="modal">Document ×</div> -->

<!-- If a modal is needed, mount it only when open, with real content and a close handler. -->

Remediation checklist

0 of 2 complete

UI, UX & Accessibility

4

MD-03

Navigation differs between the homepage and /check

Medium

Verified on live site

What was observed

The homepage nav lists About, Services, Contact, while the /check page nav lists Home, Contact, Call. The two headers disagree in both items and order.

Why it matters

Inconsistent navigation disorients users, breaks the current-page affordance, and makes the two pages feel like unrelated sites.

Fix or reference

One shared header
// src/components/Header.tsx - a single, shared set of items
const LINKS = [
  { href: "/", label: "Home" },
  { href: "/check", label: "Property Check" },
  { href: "/services", label: "Services" },
  { href: "/contact", label: "Contact" },
];

// Render <a aria-current="page"> for the active route in every page's header.

Remediation checklist

0 of 2 complete

MD-04

Mobile menu behavior needs verification

Medium

Needs verification

What was observed

A visible menu toggle is present, but the interactive behavior could not be fully confirmed from the live site. Confirm accessible wiring and that the layout does not overflow.

Why it matters

The mobile menu is the primary navigation for most traffic; an inaccessible or broken toggle blocks that path entirely and can cause horizontal page overflow.

Fix or reference

Accessible disclosure
<button
  id="nav-toggle"
  aria-expanded="false"
  aria-controls="primary-nav">
  Menu
</button>

<nav id="primary-nav" aria-label="Primary" hidden>
  <a href="/">Home</a>
  <a href="/check">Property Check</a>
</nav>

<!-- Close on Escape; return focus to #nav-toggle;
     ensure no child wider than the viewport (no horizontal scroll). -->

Remediation checklist

0 of 3 complete

MD-08

Verify heading hierarchy and explicit form labels

Medium

Needs verification

What was observed

A skip link is present, which is good. Heading outline order and explicit labels for every form field were not fully confirmed on the live site.

Why it matters

A broken heading outline or missing labels degrades screen-reader navigation and makes forms hard to complete with a keyboard or voice input.

Fix or reference

Labeled field
<label for="address">Property address</label>
<input id="address" name="address" type="text" autocomplete="street-address" />

<!-- Keep exactly one h1 per page, then sequence h2, h3 in order. -->

Remediation checklist

0 of 3 complete

PL-01

Awkward button copy: 'Click to Call' and 'Click to Email'

Polish

Verified on live site

What was observed

Contact buttons are labeled 'Click to Call' and 'Click to Email'. The verbose phrasing adds noise where a plain action verb is clearer.

Why it matters

Redundant 'Click to' phrasing scans poorly and differs from OS-native action language. Shorter, direct labels are clearer and more accessible for icon-plus-text controls.

Fix or reference

Clearer labels
<a href="tel:+15550123" class="btn">Call</a>
<a href="mailto:..." class="btn">Email</a>
<a href="sms:+15550123" class="btn">Text</a>

<!-- Keep descriptive aria-labels only where a button is icon-only. -->

Remediation checklist

0 of 1 complete

Content & Compliance

3

HI-03

Scope mismatch: 'nationwide' claims vs a NYC-only property check

High

Verified on live site

What was observed

The homepage says off-market real estate is sourced 'nationwide' and the network spans 'major markets', while the Property Check tool covers NYC addresses only.

Why it matters

Contradictory scope claims overstate capability, erode trust, and disappoint users outside New York who expect coverage that does not exist. Precise scope wording also reduces legal and compliance risk.

Fix or reference

Qualified copy
Before:
"Off-market real estate sourced nationwide, with a network across major markets."

After:
"Off-market intel across the markets we work in today. The Property Check covers
NYC addresses only, built on New York City public records."

Remediation checklist

0 of 3 complete

HI-04

Property Check results lack required disclaimers

High

Verified on live site

What was observed

The Property Check shows estimated value, comparable sales, liens, violations, and litigation from public records, with no disclaimer adjacent to the results.

Why it matters

Without a disclaimer, users can mistake stale or incomplete public-record data for an appraisal, a title opinion, or investment advice, which is legally and reputationally risky.

Fix or reference

Results disclaimer
Disclaimer
Information comes from public records that may be incomplete, delayed, or inaccurate.
Estimates are not appraisals. This report is not legal, title, tax, or investment
advice. Verify important details with the originating agency.

Remediation checklist

0 of 3 complete

MD-05

Source attribution count is inconsistent

Medium

Verified on live site

What was observed

The homepage says 'drawn live from 18 city sources'. The /check page lists NYC Department of Finance, NYC Open Data datasets (PLUTO, ACRIS, HPD, DOB, OATH, DOT, LPC, NYPD, DOE, 311), NYC Planning GeoSearch, and FEMA flood maps. The count does not reconcile with the listed sources.

Why it matters

An inconsistent count undercuts the credibility of the 'live sourced data' claim and invites scrutiny from anyone who compares pages.

Fix or reference

Attributed source list
Property data comes from public sources:
- NYC Department of Finance
- NYC Open Data: PLUTO, ACRIS, HPD, DOB, OATH, DOT, LPC, NYPD, DOE, 311
- NYC Planning GeoSearch (geocoding)
- FEMA flood maps

Reconcile the claim "18 city sources" against this list and publish one count.

Remediation checklist

0 of 3 complete

SEO & Performance

5

HI-05

Non-unique titles and descriptions across pages

High

Verified on live site

What was observed

The homepage and several other pages share the generic title 'Deal212 | Direct Deal Network'; /check uses a different title. No page has a unique, descriptive title and meta description.

Why it matters

Duplicate or generic titles and descriptions weaken search visibility and click-through and make pages hard to tell apart in results.

Fix or reference

Per-route head
<!-- Homepage -->
<title>Deal212 - NYC Off-Market Property Intel</title>
<meta name="description" content="Live NYC property reports: value, comparables, liens, violations.">

<!-- Property Check -->
<title>Property Check - Deal212</title>
<meta name="description" content="Enter an NYC address for estimated value, comps, liens, and violations.">

Remediation checklist

0 of 2 complete

HI-06

Missing robots.txt, sitemap.xml, and social/canonical meta

High

Verified on live site

What was observed

robots.txt and sitemap.xml return homepage HTML (covered in CR-01). Open Graph, Twitter card, and canonical tags are not consistently set per page.

Why it matters

Without these, crawlers cannot correctly index or discover pages, and social platforms render weak or broken link previews. Canonical tags prevent duplicate-content dilution.

Fix or reference

Canonical + social meta
<link rel="canonical" href="https://deal212.sites.tab.bot/check">
<meta property="og:title" content="Property Check - Deal212">
<meta property="og:description" content="Enter an NYC address for value, comps, liens, violations.">
<meta property="og:type" content="website">
<meta name="twitter:card" content="summary_large_image">
<meta property="og:image" content="https://deal212.sites.tab.bot/opengraph.jpg">

Remediation checklist

0 of 2 complete

MD-06

CDN caching is not tuned for assets or the lead endpoint

Medium

Verified on live site

What was observed

Caching policy is not tuned: no evidence of immutable, long-lived caching for hashed asset URLs, no clear short/no-cache rule for HTML, and no guarantee that POST /api/lead responses are never cached.

Why it matters

Wrong caching can serve stale HTML, waste bandwidth, or - worst case - cache lead-intake responses that contain submitted personal data.

Fix or reference

Cache-Control guidance
# Hashed assets: immutable, far-future
/assets/*.{js,css,woff2}    Cache-Control: public, max-age=31536000, immutable

# HTML: short or no-cache so deploys propagate fast
/*.html                     Cache-Control: public, max-age=0, must-revalidate

# Never cache lead intake
POST /api/lead               Cache-Control: no-store

# Never cache responses containing personal data
/api/lead          no-store, no-cache, must-revalidate

Remediation checklist

0 of 3 complete

MD-07

Asset optimization is incomplete

Medium

Verified on live site

What was observed

Images appear to be served in legacy formats without explicit dimensions, and bundles and fonts are not fully optimized, which slows first paint on mobile.

Why it matters

Oversized images, layout-shifting without width/height attributes, and blocking resources make the critical path heavier than it needs to be on the devices that matter most.

Fix or reference

Responsive image
<picture>
  <source type="image/avif" srcset="/hero.avif" />
  <source type="image/webp" srcset="/hero.webp" />
  <img
    src="/hero.jpg"
    alt="Property in New York City"
    width="1200"
    height="800"
    loading="lazy"
    decoding="async"
  />
</picture>

<!-- @font-face { font-display: swap; } -->
<!-- route-level code splitting; purge unused CSS and JS -->

Remediation checklist

0 of 3 complete

LW-01

No Organization or WebSite JSON-LD structured data

Low

Needs verification

What was observed

No Organization or WebSite JSON-LD schema was found on the homepage. This caps eligibility for rich search results and entity understanding.

Why it matters

Structured data helps search engines understand the site name and organization, and enables eligible rich results.

Fix or reference

JSON-LD
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "Deal212",
  "url": "https://deal212.sites.tab.bot/"
}
</script>

Remediation checklist

0 of 2 complete