Skip to content

Bonbon Dino — Website Checklist

These are best practices and references, not hard requirements. They represent patterns that worked well across past projects and serve as a starting point for any new client site. Every project is different — adapt, skip, or replace any pattern based on what the client actually needs. The goal is to avoid re-solving the same problems from scratch, not to enforce a rigid template.

The patterns below describe how things typically should behave — and the Tailwind/Next.js implementation patterns that make them work reliably.


Companion files

These siblings live next to this file. Read whichever applies to the project you're building:

  • admin-panel-checklist.md — single-admin JWT auth, password reset, settings page, dashboard layout
  • qa-board-checklist.md — public form, modal-by-query-param, sequential numbering, URL-as-state
  • email-checklist.md — Resend lazy init, fire-and-forget notifications, password reset tokens
  • db-and-deploy-checklist.md — Turso/LibSQL + Drizzle + Cloudflare Workers (OpenNext) deploy
  • debugging-lessons.md — symptom → root cause → fix log from past projects
  • RETROSPECTIVE.md — session-end ritual: review what was learned, propose appends

Deployment default (as of 2026-06): Cloudflare for both dev and prod. The "two-track Vercel demo + Cloudflare prod" model in §17 below is historical; see db-and-deploy-checklist.md for the current Cloudflare-only flow.


1. Sticky Header That Stays Visible on Scroll

What it does: The header sticks to the top of the viewport as the user scrolls down. It does not disappear.

Why: Users need persistent access to navigation, phone number, and the CTA button at all times — especially on long service pages.

How to implement (Tailwind):

sticky top-0 z-40 bg-white/95 backdrop-blur

Refinement: Add a subtle blur + semi-transparent background so content scrolling behind the header feels polished, not jarring.


2. Scroll-to-Section with Sticky Header Offset

What it does: When a user clicks a nav link that targets an anchor (#section-id), the page scrolls to that section and the content is NOT hidden behind the sticky header.

Why: Without the offset, the sticky header covers the top of the destination section. It looks broken.

How to implement: Add scroll-mt-[header-height] to every anchor target section. If the header is ~96px tall, use scroll-mt-24.

Example:

// Nav link
<Link href="/services#divorce">Divorce</Link>

// Target section on the page
<section id="divorce" className="scroll-mt-24">

Also needed: Set scroll-behavior: smooth in globals.css so the scroll animates instead of jumping.


3. Dropdown Navigation (Desktop)

What it does: Hovering a top-level nav item reveals a dropdown panel of sub-links. Works with keyboard focus too.

Why: Professional services sites often have grouped pages. Dropdowns keep the top nav clean.

How to implement (pure CSS, no JS state):

<div className="relative group">
  <Link href="/services">Services</Link>
  <div className="absolute hidden group-hover:block group-focus-within:block top-full left-0 bg-white shadow-md">
    <Link href="/services/mediation">Mediation</Link>
    <Link href="/services/workshops">Workshops</Link>
  </div>
</div>

Why pure CSS: No useState, no click handlers, no hydration issues. Works on first render.


4. Mobile Slide-Out Drawer Navigation

What it does: On mobile, the desktop nav is hidden. A hamburger button opens a slide-out drawer with all nav links.

Why: The two-row desktop header does not fit on a 390px screen. Mobile needs its own nav.

Pattern: - Desktop nav: hidden lg:flex - Hamburger button: block lg:hidden - Drawer: separate MobileMenu component, opened via boolean state - Always include the brand logo inside the mobile menu (easy to forget)


5. Two-Row Header Layout

What it does: Header has two visual tiers: 1. Top thin bar: phone number + email address 2. Main row: logo left, nav links right, CTA button far right

Why: Contact info at the very top is immediately scannable. The CTA button is always one click away.

Pattern:

[top bar: phone | email]
[logo    nav-link  nav-link  nav-link  [CTA Button]]

Sizing guidance: Main nav row h-20 to h-24 works well. Below h-16 feels cramped with a logo + text.


What it does: Clicking the site logo (in the header or mobile drawer) always scrolls the page to the very top — even when already on the homepage.

Why: In Next.js App Router, Link preserves scroll position on same-page navigation. Without explicit handling, clicking the logo on the homepage does nothing. Users expect the logo to act as a "home" button that takes them back to the top.

How to implement:

The logo must be a client component (or use useEffect) because window.scrollTo is browser-only:

// components/logo-link.tsx
"use client";
import Image from "next/image";
import Link from "next/link";

export function LogoLink() {
  return (
    <Link
      href="/"
      scroll={true}
      onClick={() => window.scrollTo(0, 0)}
      className="flex items-center gap-3"
    >
      <Image src="/logo.jpg" alt="..." width={92} height={92} className="rounded" priority />
      <span className="font-black text-xl hidden sm:block" style={{ color: "#A52550" }}>
        Site Name
      </span>
    </Link>
  );
}

scroll={true} on Link handles cross-page top navigation. window.scrollTo(0, 0) in onClick handles the same-page case that Link won't navigate for.

Mobile drawer: The same pattern applies inside the mobile menu drawer — both the logo and "Home" nav link need scroll={true} and onClick={() => window.scrollTo(0, 0)} in addition to closing the drawer.


7. Hero Section with Side Image (2-Column)

What it does: Homepage hero has text on the left and an image on the right, side by side.

Why: Gives the hero visual weight and a human face. Better than a full-bleed background image with text overlay on mobile — the overlay approach kills readability on small screens.

Pattern:

lg:grid-cols-2 items-center

  • Image: hidden lg:block (image hidden on mobile — text fills the full width)
  • Image container: rounded-xl overflow-hidden shadow-lg aspect-[16/9]
  • Use next/image with fill + object-cover + priority for hero images

Mobile: The hero shows text only. Do not shrink the image to fit mobile — hide it.


8. Alternating Content Blocks

What it does: On a list/detail page (services, features, team members), each block alternates — image left, text right, then image right, text left.

Why: A solid visual rhythm keeps a long page from feeling monotonous.

Pattern:

{items.map((item, i) => (
  <div className={`flex gap-8 ${i % 2 === 0 ? 'flex-row' : 'flex-row-reverse'}`}>
    <div className="w-1/2"><img /></div>
    <div className="w-1/2"><p>{item.description}</p></div>
  </div>
))}

On mobile: always stack vertically (flex-col) regardless of alternation.


9. Page Hero (Interior Pages)

What it does: Every interior page has a consistent banner at the top with a title and optional subtitle. Optionally includes a photo on the right.

Why: Gives users a clear sense of where they are and unifies the visual language across all pages.

Pattern (shared component): - Full width background (brand off-white or light tone) - Title: text-3xl sm:text-4xl font-display - Optional image: md:grid-cols-[1fr_380px], image hidden on mobile - Padding: py-8 sm:py-14


What it does: A card or button on one page links directly to a specific section on another page.

Why: Reduces clicks for the most common user journey — user lands exactly where they need to be, not at the top of the page.

How to implement:

// Source page card
<Link href="/services#divorce-mediation">Learn more</Link>

// Destination page section
<section id="divorce-mediation" className="scroll-mt-24">

This pattern depends on #2 (scroll-mt offset) to work correctly.


11. Section Spacing / Vertical Rhythm

What it does: Consistent vertical padding between sections creates readable breathing room.

Why: Inconsistent spacing makes pages feel unprofessional.

Guidelines: - Full-width sections (hero, CTA strip): py-16 sm:py-24 - Interior content sections: py-10 sm:py-16 - Cards and blocks inside a section: gap-8 to gap-12 - Heading-to-content gap: mt-4 inside a block, mb-8 before a grid


12. CTA Strip (Persistent Call-to-Action)

What it does: A full-width accent-colored section near the bottom of every page with a single message and button.

Why: Users who read to the bottom of a page are warm leads. Give them a conversion point right there.

Pattern: - Full-width, solid brand accent color - Single headline + single button ("Schedule a Consultation", "Get a Quote", etc.) - Appears on every interior page - On homepage: appears between the last section and the footer


13. Contact Form Best Practices

What it does: A form that validates, protects against bots, and emails the client on submit.

Checklist for every site: 1. Server action ('use server') — no client-side API key exposure 2. Bot protection (Cloudflare Turnstile) — free, invisible to real users 3. Email delivery (Resend) — reliable, generous free tier 4. Three required env vars: RESEND_API_KEY, TURNSTILE_SECRET_KEY, CONTACT_EMAIL 5. Show a success/error state after submit — no silent failures 6. Always use prod Turnstile keys before go-live — dev keys only work on localhost


14. Mobile-First Responsive Sizing Rules

Tailwind is mobile-first. The pattern is always: base = mobile, sm/lg = override upward.

text-2xl sm:text-3xl lg:text-4xl    ← scale up for larger screens
py-6 sm:py-10 lg:py-14              ← more padding on desktop
hidden lg:block                      ← desktop-only element
block lg:hidden                      ← mobile-only element

Common component heights: - Header main row: h-20 to h-24 - Hero section minimum: min-h-[480px] (prevents collapse on short content) - Hero on desktop: min-h-[560px] to min-h-[640px] - Page hero: let content drive height with py-8 sm:py-14


15. Image Handling

Always use next/image — auto-optimizes, lazy loads, prevents layout shift.

For hero images with a fixed container:

<div className="relative w-full aspect-[16/9]">
  <Image src="..." fill className="object-cover" priority />
</div>

Priority flag: Set priority on any image above the fold (hero, page hero). Everything else lazy-loads by default.

File organization: All images flat in public/assets/ — no subdirectories. Consistent naming: {page}-photo.jpg, {service-slug}.png.


16. SEO Scaffolding (Every Project)

Minimum setup for every client site:

  1. app/sitemap.ts — auto-generates sitemap for all routes
  2. app/robots.ts — tells search crawlers what to index
  3. app/opengraph-image.tsx — Next.js built-in OG image (see §16b)
  4. metadata export on every page with unique title and description
  5. JSON-LD schema markup on every page (see §16c)

16b. OG Image (Open Graph)

What it does: A branded image that appears when the site is shared on Slack, Twitter, iMessage, LinkedIn, etc.

Why: Without an OG image, social shares show no preview or a broken blank. Every site needs one.

How to implement (Next.js App Router):

// app/opengraph-image.tsx
import { ImageResponse } from 'next/og'

export const runtime = 'edge'
export const alt = 'Site Name'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'

export default function OGImage() {
  return new ImageResponse(
    (
      <div
        style={{
          background: '#2a8c7e',  // brand teal
          width: 1200,
          height: 630,
          display: 'flex',
          flexDirection: 'column',
          justifyContent: 'flex-end',
          padding: '48px',
        }}
      >
        <span style={{ color: 'white', fontSize: 72, fontWeight: 700 }}>
          Site Name
        </span>
        <span style={{ color: 'white', fontSize: 36 }}>
          Tagline text here
        </span>
        <span style={{ color: 'white', fontSize: 24 }}>
          City, State
        </span>
      </div>
    ),
    { ...size }
  )
}

Important: Register the route in lib/metadata.ts by updating OG image URLs from /images/og-image.png/opengraph-image in both openGraph.images and twitter.images.

Design: Keep it simple — solid brand color background, site name large, one-line tagline, location small at bottom. No external fonts (use system sans-serif).


16c. JSON-LD Schema Markup (Google SEO)

What it does: Structured data in <script type="application/ld+json"> tags that helps Google understand each page's content and enables rich results (star ratings, FAQ expanders, breadcrumb trails in search).

Why: Rich results improve click-through rate. Schema is the main on-page SEO lever beyond basic metadata.

Core pattern — @graph for multi-schema pages:

// app/about/page.tsx
import { generatePageMetadata, generateBreadcrumbSchema, LOCAL_BUSINESS_JSONLD, SITE_URL, SITE_NAME } from '@/lib/metadata'

const PAGE_JSONLD = {
  '@context': 'https://schema.org',
  '@graph': [
    { /* primary schema type */ },
    generateBreadcrumbSchema('Page Title', '/path'),
  ],
}

export default function Page() {
  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(PAGE_JSONLD) }}
      />
      {/* existing JSX */}
    </>
  )
}

Page-type schema reference:

Page Schema Type(s)
Homepage LocalBusiness + WebSite (@graph)
About Person + BreadcrumbList (@graph)
Services (list) ItemList of Service + BreadcrumbList (@graph)
Individual service Service + BreadcrumbList (@graph)
For Attorneys Service (with OfferCatalog) + BreadcrumbList (@graph)
Fees/Pricing Service with Offer/PriceSpecification + BreadcrumbList (@graph)
Events/Workshops Course + BreadcrumbList (@graph)
Contact ContactPage + LocalBusiness + BreadcrumbList (@graph)

Shared constants in lib/metadata.ts:

export const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://www.yoursite.com'
export const SITE_NAME = 'Your Site Name'

export const LOCAL_BUSINESS_JSONLD = {
  '@context': 'https://schema.org',
  '@type': ['LocalBusiness', 'ProfessionalService'],
  name: SITE_NAME,
  description: '...',
  url: SITE_URL,
  address: { '@type': 'PostalAddress', addressLocality: 'City', addressRegion: 'ST', addressCountry: 'US' },
  areaServed: ['City, ST'],
  serviceType: ['Service A', 'Service B'],
  priceRange: '$$',
  telephone: '+1XXXXXXXXXX',  // confirmed public number only
  email: 'contact@yoursite.com',
}

export function generateBreadcrumbSchema(label: string, path: string) {
  return {
    '@type': 'BreadcrumbList',
    'itemListElement': [
      { '@type': 'ListItem', position: 1, name: 'Home', item: SITE_URL },
      { '@type': 'ListItem', position: 2, name: label, item: `${SITE_URL}${path}` },
    ],
  }
}

Rules: - Only use telephone and email if confirmed and publicly displayed — do not add placeholders - @context goes only at the top of @graph — never inside individual nodes - generateBreadcrumbSchema does NOT include @context in its return value - Use SITE_URL/SITE_NAME constants everywhere — never hardcode the domain or business name - Validate at Google Rich Results Test after domain is live

SEO skills (read, don't install): Skills repo at ~/Documents/skill_repos/marketingskills/skills/: - seo-audit/ — technical + on-page audit - schema/ — JSON-LD implementation - site-architecture/ — page hierarchy + internal linking

To use: Read /Users/ja58968/Documents/skill_repos/marketingskills/skills/<skill>/SKILL.md then follow it. No npx skills add needed.


17. Deployment: Two-Track Model

Track 1 — Demo (Vercel): Use immediately when client wants to review the site before production accounts are set up. No Cloudflare account needed. ~2 minutes.

Track 2 — Production (Cloudflare Pages): Where the real site lives. Requires client to own all accounts.

Never skip Track 1. It unblocks client review right away and gives you a feedback loop before the full production deployment.


18. Client Account Ownership (Agency Rule)

  • Client creates all service accounts (Cloudflare, Resend, domain registrar) with their own business email
  • Bonbon Dino is added as collaborator, never the account owner
  • GitHub repo can stay under bonbon-dino org for non-technical clients
  • Walk the client through all accounts in a handoff meeting — do not just send a doc

What it does: A small, unobtrusive credit line in the footer reads "website by bonbondino.com" and links to the agency site.

Why: Free passive marketing on every site Bonbon Dino ships. Clients expect it; it's standard agency practice.

How to implement:

<p className="text-sm text-gray-400 mt-2">
  website by{" "}
  <a
    href="https://bonbondino.com"
    target="_blank"
    rel="noopener noreferrer"
    className="hover:text-gray-600 transition-colors"
  >
    bonbondino.com
  </a>
</p>

Placement: Bottom of Footer.tsx, visually separated from the client's own footer content. Keep it subtle — small, low-contrast text so it doesn't compete with the client's brand.


20. Admin Panel Auth Security

Rule: use a session cookie with no maxAge. This makes the browser delete the cookie automatically when the window is fully closed. The client never needs to remember to log out.

Why: A persistent cookie (with maxAge) survives browser closes — if the client forgets to sign out on a shared or public computer, the admin remains accessible. A session cookie eliminates that risk with no extra UX cost.

How to implement (Next.js login route):

// app/api/auth/login/route.ts
response.cookies.set(SESSION_COOKIE, token, {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production',
  sameSite: 'lax',
  path: '/',
  // No maxAge — session cookie, cleared on browser close
})

Server-side expiry: Set a reasonable JWT expiration (e.g. setExpirationTime('8h')) as a fallback for browsers left open. The JWT check on every request handles this — no maxAge on the cookie needed.

CSP + dev mode: React/Turbopack requires unsafe-eval in development for source maps and HMR. Add it conditionally so production CSP stays strict:

const isDev = process.env.NODE_ENV !== 'production'
const csp = `script-src 'self' 'unsafe-inline'${isDev ? " 'unsafe-eval'" : ""} ...`

Sign-out redirect: After clearing the session cookie, redirect to / (the public homepage) — not back to the admin login page.


Quick Checklist — "Is This Page Done?"

  • [ ] Sticky header visible on scroll
  • [ ] All nav anchor links land below the header (scroll-mt applied to targets)
  • [ ] Smooth scroll enabled globally (scroll-behavior: smooth in globals.css)
  • [ ] Logo link scrolls to top on same-page click (client component with window.scrollTo(0, 0))
  • [ ] Mobile menu has brand logo
  • [ ] Hero image hidden on mobile (text-only hero on small screens)
  • [ ] CTA button visible in header at all times
  • [ ] CTA strip at bottom of every page
  • [ ] Page hero with title on every interior page
  • [ ] next/image used for all images, priority on above-fold images
  • [ ] metadata export with unique title and description on every page
  • [ ] app/opengraph-image.tsx exists and OG URL registered in lib/metadata.ts
  • [ ] JSON-LD @graph schema on every page (at minimum: homepage LocalBusiness + inner pages with BreadcrumbList)
  • [ ] Schema validated in Google Rich Results Test before production go-live
  • [ ] Contact form: bot protection + success/error state + prod Turnstile key
  • [ ] Deep-links from homepage cards scroll to correct sections (not top of page)
  • [ ] Footer includes "website by bonbondino.com" credit link
  • [ ] Admin session cookie has no maxAge (auto-logout on browser close)
  • [ ] Admin sign-out redirects to / not the login page
  • [ ] Dev CSP includes unsafe-eval; production CSP does not