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 layoutqa-board-checklist.md— public form, modal-by-query-param, sequential numbering, URL-as-stateemail-checklist.md— Resend lazy init, fire-and-forget notifications, password reset tokensdb-and-deploy-checklist.md— Turso/LibSQL + Drizzle + Cloudflare Workers (OpenNext) deploydebugging-lessons.md— symptom → root cause → fix log from past projectsRETROSPECTIVE.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.mdfor 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.
6. Logo Link Always Scrolls to Top¶
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/imagewithfill+object-cover+priorityfor 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
10. Deep-Link from One Page to a Section on Another Page¶
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:
app/sitemap.ts— auto-generates sitemap for all routesapp/robots.ts— tells search crawlers what to indexapp/opengraph-image.tsx— Next.js built-in OG image (see §16b)metadataexport on every page with uniquetitleanddescription- 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-dinoorg for non-technical clients - Walk the client through all accounts in a handoff meeting — do not just send a doc
19. Footer Agency Credit¶
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-mtapplied to targets) - [ ] Smooth scroll enabled globally (
scroll-behavior: smoothin 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/imageused for all images,priorityon above-fold images - [ ]
metadataexport with unique title and description on every page - [ ]
app/opengraph-image.tsxexists and OG URL registered inlib/metadata.ts - [ ] JSON-LD
@graphschema 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