Compare commits

...

2 Commits

Author SHA1 Message Date
45422753a3 further fixes 2026-06-13 15:00:40 -05:00
dfd5e744a4 cleanup 2026-06-13 14:36:27 -05:00
13 changed files with 317 additions and 180 deletions

6
.env.example Normal file
View File

@@ -0,0 +1,6 @@
# Amazon SES SMTP credentials (NOT IAM access keys — use dedicated SMTP credentials)
SES_SMTP_USER=
SES_SMTP_PASS=
# Verified sender email in Amazon SES (must be a verified identity)
SES_FROM_EMAIL=support@bayarea-cc.com

View File

@@ -0,0 +1,92 @@
import { NextResponse } from "next/server";
import nodemailer from "nodemailer";
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export async function POST(request: Request) {
let body: Record<string, unknown>;
try {
body = await request.json();
} catch {
return NextResponse.json({ error: "Invalid request body." }, { status: 400 });
}
const name = String(body.name ?? "").trim();
const email = String(body.email ?? "").trim();
const mailboxes = String(body.mailboxes ?? "").trim();
const provider = String(body.provider ?? "").trim();
const message = String(body.message ?? "").trim();
const errors: Record<string, string> = {};
if (!name) {
errors.name = "Please enter your name.";
}
if (!email) {
errors.email = "Please enter your business email.";
} else if (!EMAIL_REGEX.test(email)) {
errors.email = "Email address needs to include an @ symbol.";
}
if (Object.keys(errors).length > 0) {
return NextResponse.json({ error: "Validation failed.", fields: errors }, { status: 400 });
}
const sesUser = process.env.SES_SMTP_USER;
const sesPass = process.env.SES_SMTP_PASS;
const fromEmail = process.env.SES_FROM_EMAIL;
if (!sesUser || !sesPass || !fromEmail) {
return NextResponse.json({ error: "Server configuration error." }, { status: 500 });
}
const transporter = nodemailer.createTransport({
host: "email-smtp.us-east-2.amazonaws.com",
port: 587,
secure: false,
auth: {
user: sesUser,
pass: sesPass,
},
});
const textBody = [
`Name: ${name}`,
`Business Email: ${email}`,
`Mailboxes: ${mailboxes || "(not provided)"}`,
`Current Provider: ${provider || "(not selected)"}`,
`Message: ${message || "(none)"}`,
].join("\n");
const htmlBody = [
"<table style='font-family:sans-serif;border-collapse:collapse;'>",
`<tr><td style='padding:6px 12px 6px 0;font-weight:700;white-space:nowrap;'>Name</td><td style='padding:6px 0;'>${escapeHtml(name)}</td></tr>`,
`<tr><td style='padding:6px 12px 6px 0;font-weight:700;white-space:nowrap;'>Business Email</td><td style='padding:6px 0;'>${escapeHtml(email)}</td></tr>`,
`<tr><td style='padding:6px 12px 6px 0;font-weight:700;white-space:nowrap;'>Mailboxes</td><td style='padding:6px 0;'>${escapeHtml(mailboxes || "(not provided)")}</td></tr>`,
`<tr><td style='padding:6px 12px 6px 0;font-weight:700;white-space:nowrap;'>Current Provider</td><td style='padding:6px 0;'>${escapeHtml(provider || "(not selected)")}</td></tr>`,
`<tr><td style='padding:6px 12px 6px 0;font-weight:700;white-space:nowrap;'>Message</td><td style='padding:6px 0;'>${escapeHtml(message || "(none)")}</td></tr>`,
"</table>",
].join("\n");
try {
await transporter.sendMail({
from: fromEmail,
to: "support@bayarea-cc.com",
replyTo: email,
subject: `New email assessment request — ${name}`,
text: textBody,
html: htmlBody,
});
} catch {
return NextResponse.json({ error: "Unable to send your request. Please try again or call us." }, { status: 500 });
}
return NextResponse.json({ ok: true });
}
function escapeHtml(text: string): string {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}

View File

@@ -542,6 +542,14 @@ h3 {
line-height: 1.38; line-height: 1.38;
} }
.hero-local-trust {
max-width: 32rem;
margin: var(--space-2) 0 0;
color: var(--blue);
font-size: var(--step--1);
font-weight: 700;
}
.trust-list, .trust-list,
.check-stack, .check-stack,
.local-strip ul { .local-strip ul {
@@ -1470,6 +1478,7 @@ h3 {
display: grid; display: grid;
gap: var(--space-3); gap: var(--space-3);
max-width: 56rem; max-width: 56rem;
margin-inline: auto;
margin-bottom: var(--space-6); margin-bottom: var(--space-6);
} }
@@ -1489,8 +1498,16 @@ h3 {
.pain-grid { .pain-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: var(--space-4); gap: var(--space-4);
max-width: 68rem;
margin-inline: auto;
}
@media (min-width: 760px) {
.pain-grid {
grid-template-columns: repeat(3, 1fr);
}
} }
.pain-grid article { .pain-grid article {
@@ -2621,7 +2638,7 @@ h3 {
.pricing-builder { .pricing-builder {
display: grid; display: grid;
gap: var(--space-4); gap: var(--space-4);
max-width: var(--page-max); max-width: 68rem;
margin-inline: auto; margin-inline: auto;
padding: 0; padding: 0;
} }
@@ -2789,6 +2806,72 @@ h3 {
color: var(--green); color: var(--green);
} }
.offer-banner {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-4);
border: 1px solid oklch(67% 0.16 147 / 0.62);
border-radius: var(--radius-md);
background: oklch(16% 0.06 147 / 0.18);
color: var(--green);
font-size: var(--step--1);
font-weight: 800;
line-height: 1.4;
}
:root[data-theme="light"] .offer-banner {
background: oklch(90% 0.06 147 / 0.32);
}
.offer-banner-icon {
flex: 0 0 auto;
color: var(--green);
font-size: 1.1rem;
}
.price-subtext {
margin: 0;
color: var(--text-muted);
font-size: var(--step--1);
}
.pricing-included-panel {
display: grid;
gap: var(--space-4);
align-content: start;
padding: clamp(1.25rem, 2vw, 2rem);
border: 1px solid var(--line-soft);
border-radius: var(--radius-md);
background: oklch(12% 0.034 245 / 0.62);
}
:root[data-theme="light"] .pricing-included-panel {
background: oklch(99% 0.006 245 / 0.86);
}
.included-feature-list {
display: grid;
gap: var(--space-3);
list-style: none;
padding: 0;
margin: 0;
}
.included-feature-list li {
display: flex;
align-items: center;
gap: var(--space-3);
color: var(--text-muted);
}
.included-check {
flex: 0 0 auto;
color: var(--green);
font-weight: 800;
font-size: 0.95rem;
}
.plan-summary, .plan-summary,
.pricing-note { .pricing-note {
margin: 0; margin: 0;
@@ -3264,6 +3347,13 @@ textarea::placeholder {
font-size: var(--step--1); font-size: var(--step--1);
} }
.form-trust-line {
margin: var(--space-2) 0 0;
color: var(--text-dim);
font-size: var(--step--1);
text-align: center;
}
.form-status { .form-status {
min-height: 1.5rem; min-height: 1.5rem;
margin: 0; margin: 0;

View File

@@ -4,72 +4,21 @@ import { FormEvent, MouseEvent, useEffect, useState } from "react";
import SiteHeader from "../components/SiteHeader"; import SiteHeader from "../components/SiteHeader";
import HeroSection from "../components/HeroSection"; import HeroSection from "../components/HeroSection";
import ProblemSection from "../components/ProblemSection"; import ProblemSection from "../components/ProblemSection";
import ProcessSection from "../components/ProcessSection"; import PricingSection from "../components/PricingSection";
import DeliverabilitySection from "../components/DeliverabilitySection";
import ContinuitySection, { type ContinuityFeature, type FeatureKey } from "../components/ContinuitySection";
import MigrationProcessSection from "../components/MigrationProcessSection";
import PricingSection, { type Plan } from "../components/PricingSection";
import FaqSection from "../components/FaqSection"; import FaqSection from "../components/FaqSection";
import AssessmentSection from "../components/AssessmentSection"; import AssessmentSection from "../components/AssessmentSection";
import SiteFooter from "../components/SiteFooter"; import SiteFooter from "../components/SiteFooter";
type Theme = "dark" | "light"; type Theme = "dark" | "light";
const pricingSummaries: Record<Plan, string> = {
hosting:
"Core business email hosting with 25 GB mailboxes, custom domain email, and AWS-backed infrastructure.",
managed:
"Managed setup adds rollout planning, DNS validation, migration coordination, and device handoff checks during the assessment.",
};
const continuityFeatures: Record<FeatureKey, ContinuityFeature> = {
buffering: {
title: "Inbound buffering",
copy: "Incoming mail can be buffered before mailbox delivery during maintenance or provider-side disruption.",
proof: [
"Supports planned maintenance windows",
"Keeps delivery flow observable",
"Feeds mailbox delivery after processing",
],
},
sending: {
title: "Outbound sending",
copy: "Amazon SES gives outbound email an authenticated sending path with reputation tooling and clearer operational visibility.",
proof: [
"Separates outbound sending from old shared hosting mail",
"Uses domain authentication as part of setup",
"Makes sending behavior easier to troubleshoot",
],
},
standby: {
title: "Standby failover",
copy: "A standby environment is part of the continuity plan when primary systems need intervention or provider-side work.",
proof: [
"Keeps the fallback path visible",
"Reduces guesswork during incidents",
"Pairs with status checks and local support",
],
},
local: {
title: "Local management",
copy: "Domain records, migration, mailbox changes, device setup, and troubleshooting stay with a local Corpus Christi team.",
proof: [
"One support path for DNS and devices",
"Migration scope is reviewed before work starts",
"Mailbox changes stay tied to the business context",
],
},
};
export default function Page() { export default function Page() {
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const [theme, setTheme] = useState<Theme>("dark"); const [theme, setTheme] = useState<Theme>("dark");
const [activeFeature, setActiveFeature] = useState<FeatureKey>("buffering"); const [mailboxes, setMailboxes] = useState(10);
const [activePlan, setActivePlan] = useState<Plan>("hosting");
const [mailboxes, setMailboxes] = useState(25);
const [formErrors, setFormErrors] = useState({ name: "", email: "" }); const [formErrors, setFormErrors] = useState({ name: "", email: "" });
const [formStatus, setFormStatus] = useState(""); const [formStatus, setFormStatus] = useState("");
const activeFeatureDetails = continuityFeatures[activeFeature]; const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => { useEffect(() => {
const storedTheme = window.localStorage.getItem("bes-theme"); const storedTheme = window.localStorage.getItem("bes-theme");
@@ -164,7 +113,7 @@ export default function Page() {
} }
}; };
const handleAssessmentSubmit = (event: FormEvent<HTMLFormElement>) => { const handleAssessmentSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
const form = event.currentTarget; const form = event.currentTarget;
const formData = new FormData(form); const formData = new FormData(form);
@@ -191,8 +140,40 @@ export default function Page() {
return; return;
} }
setIsSubmitting(true);
try {
const response = await fetch("/api/assessment", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name,
email,
mailboxes: formData.get("mailboxes") ?? "",
provider: formData.get("provider") ?? "",
message: formData.get("message") ?? "",
}),
});
if (response.ok) {
setFormStatus("Thanks. We'll review your mailbox count and current provider before we reply."); setFormStatus("Thanks. We'll review your mailbox count and current provider before we reply.");
form.reset(); form.reset();
} else {
const data = await response.json().catch(() => null);
if (data?.fields) {
setFormErrors({
name: data.fields.name ?? "",
email: data.fields.email ?? "",
});
} else {
setFormStatus(data?.error ?? "Something went wrong. Please try again or call us.");
}
}
} catch {
setFormStatus("Network error. Please check your connection and try again.");
} finally {
setIsSubmitting(false);
}
}; };
return ( return (
@@ -211,25 +192,15 @@ export default function Page() {
<main id="main"> <main id="main">
<HeroSection /> <HeroSection />
<ProblemSection /> <ProblemSection />
<ProcessSection />
<DeliverabilitySection />
<ContinuitySection
activeFeature={activeFeature}
activeFeatureDetails={activeFeatureDetails}
onFeatureChange={setActiveFeature}
/>
<MigrationProcessSection />
<PricingSection <PricingSection
activePlan={activePlan}
mailboxes={mailboxes} mailboxes={mailboxes}
pricingSummaries={pricingSummaries}
onPlanChange={setActivePlan}
onMailboxesChange={setMailboxes} onMailboxesChange={setMailboxes}
/> />
<FaqSection /> <FaqSection />
<AssessmentSection <AssessmentSection
formErrors={formErrors} formErrors={formErrors}
formStatus={formStatus} formStatus={formStatus}
isSubmitting={isSubmitting}
onAssessmentSubmit={handleAssessmentSubmit} onAssessmentSubmit={handleAssessmentSubmit}
/> />
</main> </main>

View File

@@ -3,10 +3,11 @@ import type { FormEventHandler } from "react";
type AssessmentSectionProps = { type AssessmentSectionProps = {
formErrors: { name: string; email: string }; formErrors: { name: string; email: string };
formStatus: string; formStatus: string;
isSubmitting: boolean;
onAssessmentSubmit: FormEventHandler<HTMLFormElement>; onAssessmentSubmit: FormEventHandler<HTMLFormElement>;
}; };
export default function AssessmentSection({ formErrors, formStatus, onAssessmentSubmit }: AssessmentSectionProps) { export default function AssessmentSection({ formErrors, formStatus, isSubmitting, onAssessmentSubmit }: AssessmentSectionProps) {
return ( return (
<section className="content-section assessment-section" id="assessment" aria-labelledby="assessment-title"> <section className="content-section assessment-section" id="assessment" aria-labelledby="assessment-title">
<div className="assessment-bg" aria-hidden="true"></div> <div className="assessment-bg" aria-hidden="true"></div>
@@ -56,17 +57,23 @@ export default function AssessmentSection({ formErrors, formStatus, onAssessment
<label htmlFor="message">What should we review?</label> <label htmlFor="message">What should we review?</label>
<textarea id="message" name="message" rows={4} placeholder="Domain, mailbox count, migration timing, device setup..."></textarea> <textarea id="message" name="message" rows={4} placeholder="Domain, mailbox count, migration timing, device setup..."></textarea>
</div> </div>
<button className="button button-primary button-large" type="submit"> <button
Request email assessment className="button button-primary button-large"
<span aria-hidden="true"></span> type="submit"
disabled={isSubmitting}
aria-busy={isSubmitting}
>
{isSubmitting ? "Sending…" : "Request email assessment"}
<span aria-hidden="true">{isSubmitting ? "" : "→"}</span>
</button> </button>
<p className="form-trust-line">We reply within one business day. We don&rsquo;t share your details.</p>
<p className="form-status" role="status" aria-live="polite">{formStatus}</p> <p className="form-status" role="status" aria-live="polite">{formStatus}</p>
</form> </form>
</div> </div>
<div className="assessment-copy"> <div className="assessment-copy">
<p className="eyebrow">Start here</p> <p className="eyebrow">Start here</p>
<h2 id="assessment-title">Lets review your email setup.</h2> <h2 id="assessment-title">Let&rsquo;s review your email setup.</h2>
<p> <p>
Tell us about your domain, mailbox count, current provider, and support needs. We will reply with clear next steps for hosting, DNS, migration, and device setup. Tell us about your domain, mailbox count, current provider, and support needs. We will reply with clear next steps for hosting, DNS, migration, and device setup.
</p> </p>

View File

@@ -26,6 +26,10 @@ export default function FaqSection() {
<summary>Is this Microsoft 365 or Google Workspace?</summary> <summary>Is this Microsoft 365 or Google Workspace?</summary>
<p>No. It is a managed business email hosting service for teams that want professional email, DNS correctness, migration help, and local support without buying a full office suite by default.</p> <p>No. It is a managed business email hosting service for teams that want professional email, DNS correctness, migration help, and local support without buying a full office suite by default.</p>
</details> </details>
<details>
<summary>What does switching cost?</summary>
<p>Setup and migration are included at no extra charge for teams that switch by June 30, 2026. After that, the ongoing cost is $5 per mailbox per month. We confirm the final scope in your assessment before any work begins.</p>
</details>
</div> </div>
</section> </section>
); );

View File

@@ -8,6 +8,9 @@ export default function HeroSection() {
<p className="hero-lede"> <p className="hero-lede">
Infrastructure and support from a local team that understands how business gets done. Infrastructure and support from a local team that understands how business gets done.
</p> </p>
<p className="hero-local-trust">
Local team in downtown Corpus Christi same neighborhood as you.
</p>
<ul className="trust-list" aria-label="Core trust points"> <ul className="trust-list" aria-label="Core trust points">
<li> <li>

View File

@@ -1,50 +1,18 @@
export type Plan = "hosting" | "managed";
type PricingSectionProps = { type PricingSectionProps = {
activePlan: Plan;
mailboxes: number; mailboxes: number;
pricingSummaries: Record<Plan, string>;
onPlanChange: (plan: Plan) => void;
onMailboxesChange: (mailboxes: number) => void; onMailboxesChange: (mailboxes: number) => void;
}; };
export default function PricingSection({ activePlan, mailboxes, pricingSummaries, onPlanChange, onMailboxesChange }: PricingSectionProps) { export default function PricingSection({ mailboxes, onMailboxesChange }: PricingSectionProps) {
return ( return (
<section className="content-section pricing-detail" id="pricing-detail" aria-labelledby="pricing-title"> <section className="content-section pricing-detail" id="pricing-detail" aria-labelledby="pricing-title">
<div className="section-heading"> <div className="section-heading">
<p className="eyebrow">Simple mailbox pricing</p> <p className="eyebrow">Simple mailbox pricing</p>
<h2 id="pricing-title">$5 per inbox per month, scoped before you switch.</h2> <h2 id="pricing-title">$5 per inbox per month, scoped before you switch.</h2>
<p>
Pick a mailbox count and plan focus. The base mailbox price is transparent; setup, migration, support scope, minimums, taxes, and add-ons are confirmed during the assessment.
</p>
</div> </div>
<div className="pricing-builder" data-plan={activePlan} data-mailboxes={mailboxes}> <div className="pricing-builder" data-mailboxes={mailboxes}>
<div className="pricing-control-panel"> <div className="pricing-control-panel">
<div>
<span className="panel-label">Plan focus</span>
<div className="pricing-segmented" data-active={activePlan} role="group" aria-label="Plan focus">
<button
className={activePlan === "hosting" ? "is-active" : ""}
type="button"
data-plan="hosting"
aria-pressed={activePlan === "hosting"}
onClick={() => onPlanChange("hosting")}
>
Hosting
</button>
<button
className={activePlan === "managed" ? "is-active" : ""}
type="button"
data-plan="managed"
aria-pressed={activePlan === "managed"}
onClick={() => onPlanChange("managed")}
>
Managed setup
</button>
</div>
</div>
<div> <div>
<span className="panel-label">Mailbox count</span> <span className="panel-label">Mailbox count</span>
<div className="mailbox-options" role="group" aria-label="Estimated mailbox count"> <div className="mailbox-options" role="group" aria-label="Estimated mailbox count">
@@ -66,66 +34,47 @@ export default function PricingSection({ activePlan, mailboxes, pricingSummaries
<div className="price-estimate"> <div className="price-estimate">
<span className="panel-label">Estimated base hosting</span> <span className="panel-label">Estimated base hosting</span>
<p><span className="plan-total">${mailboxes * 5}</span><span>/ month</span></p> <p><span className="plan-total">${mailboxes * 5}</span><span>/ month</span></p>
<small>Based on <strong className="selected-mailboxes">{mailboxes}</strong> inboxes at $5 each.</small> <small>For teams of 10+ mailboxes.</small>
</div> </div>
<p className="plan-summary"> <div className="offer-banner">
{pricingSummaries[activePlan]} <span className="offer-banner-icon" aria-hidden="true">&#9733;</span>
</p> Free migration &amp; setup for teams that switch by June&nbsp;30,&nbsp;2026.
</div>
<p className="price-subtext">$5 per mailbox / month. Free migration if you switch by June 30, 2026. Final scope confirmed in your assessment.</p>
<a className="button button-primary" href="#assessment">Get a mailbox count quote</a> <a className="button button-primary" href="#assessment">Get a mailbox count quote</a>
</div> </div>
<div className="pricing-comparison-panel"> <div className="pricing-included-panel">
<div className="comparison-head"> <span className="panel-label">What&rsquo;s included</span>
<div> <ul className="included-feature-list">
<span className="panel-label">What changes by scope</span> <li>
<h3>Hosting first, managed help when the migration needs it.</h3> <span className="included-check" aria-hidden="true">&#10003;</span>
</div> 25 GB mailbox
<span className="status-pill">Operational plan</span> </li>
</div> <li>
<span className="included-check" aria-hidden="true">&#10003;</span>
<div className="plan-feature-grid" role="table" aria-label="Plan feature comparison"> Custom domain email
<div className="plan-feature-row table-head" role="row"> </li>
<span role="columnheader">Feature</span> <li>
<span role="columnheader">Hosting</span> <span className="included-check" aria-hidden="true">&#10003;</span>
<span role="columnheader">Managed setup</span> SPF, DKIM, DMARC configured
</div> </li>
<div className="plan-feature-row" role="row"> <li>
<span role="cell">25 GB mailbox</span> <span className="included-check" aria-hidden="true">&#10003;</span>
<span role="cell">Included</span> Migration from current provider
<span role="cell">Included</span> </li>
</div> <li>
<div className="plan-feature-row" role="row"> <span className="included-check" aria-hidden="true">&#10003;</span>
<span role="cell">Custom domain email</span> Outlook, iPhone, iPad setup
<span role="cell">Included</span> </li>
<span role="cell">Included</span> <li>
</div> <span className="included-check" aria-hidden="true">&#10003;</span>
<div className="plan-feature-row" role="row"> Local Corpus Christi support
<span role="cell">SPF, DKIM, DMARC check</span> </li>
<span role="cell">Configured</span> </ul>
<span role="cell">Configured + validated</span>
</div>
<div className="plan-feature-row" role="row">
<span role="cell">Migration from current provider</span>
<span role="cell">Scoped</span>
<span role="cell">Planned with handoff</span>
</div>
<div className="plan-feature-row" role="row">
<span role="cell">Outlook, iPhone, iPad setup</span>
<span role="cell">Guided</span>
<span role="cell">Checked before handoff</span>
</div>
<div className="plan-feature-row" role="row">
<span role="cell">Local support</span>
<span role="cell">Available</span>
<span role="cell">Priority during rollout</span>
</div>
</div>
<p className="pricing-note">
No inbox placement guarantees, no zero-downtime promise. The assessment confirms DNS access, provider constraints, migration timing, and device needs before work starts.
</p>
</div> </div>
</div> </div>
</section> </section>

View File

@@ -1,6 +1,6 @@
export default function ProblemSection() { export default function ProblemSection() {
return ( return (
<section className="content-section problem-section" aria-labelledby="problem-title"> <section className="content-section problem-section" id="why" aria-labelledby="problem-title">
<div className="section-heading"> <div className="section-heading">
<p className="eyebrow">Why it matters</p> <p className="eyebrow">Why it matters</p>
<h2 id="problem-title">Email problems rarely look technical at first.</h2> <h2 id="problem-title">Email problems rarely look technical at first.</h2>
@@ -10,17 +10,13 @@ export default function ProblemSection() {
</div> </div>
<div className="pain-grid"> <div className="pain-grid">
<article> <article>
<strong>Free addresses cost trust</strong> <strong>Unprofessional email hurts trust</strong>
<p>Gmail and Yahoo are fine for personal use. A business domain signals that your company is established.</p> <p>Free Gmail or Yahoo addresses and unreliable shared-hosting mailboxes both signal to customers that your business is not serious about communication.</p>
</article> </article>
<article> <article>
<strong>DNS mistakes send mail to spam</strong> <strong>DNS mistakes send mail to spam</strong>
<p>SPF, DKIM, and DMARC help receiving servers recognize legitimate mail from your domain.</p> <p>SPF, DKIM, and DMARC help receiving servers recognize legitimate mail from your domain.</p>
</article> </article>
<article>
<strong>Shared hosting mail breaks quietly</strong>
<p>Old hosting mailboxes often hide weak deliverability, limited storage, and unclear support paths.</p>
</article>
<article> <article>
<strong>Support queues slow the fix</strong> <strong>Support queues slow the fix</strong>
<p>When email breaks, a local team can assess the domain, provider, device, and DNS together.</p> <p>When email breaks, a local team can assess the domain, provider, device, and DNS together.</p>

View File

@@ -26,11 +26,10 @@ export default function SiteFooter() {
<nav className="footer-links" aria-label="Footer navigation"> <nav className="footer-links" aria-label="Footer navigation">
<h2>Explore</h2> <h2>Explore</h2>
<ul> <ul>
<li><a href="#services">Services</a></li> <li><a href="#why">Why email matters</a></li>
<li><a href="#infrastructure">Infrastructure</a></li>
<li><a href="#pricing-detail">Pricing</a></li> <li><a href="#pricing-detail">Pricing</a></li>
<li><a href="#migration-detail">Migration</a></li>
<li><a href="#faq">FAQ</a></li> <li><a href="#faq">FAQ</a></li>
<li><a href="#assessment">Get assessment</a></li>
</ul> </ul>
</nav> </nav>
@@ -47,7 +46,6 @@ export default function SiteFooter() {
<div className="footer-bottom"> <div className="footer-bottom">
<p>Bay Area Email Services, part of Bay Area IT. All rights reserved.</p> <p>Bay Area Email Services, part of Bay Area IT. All rights reserved.</p>
<div> <div>
<a href="#assessment">Support</a>
<a href="#top" className="back-to-top"> <a href="#top" className="back-to-top">
Back to top Back to top
<svg viewBox="0 0 24 24" aria-hidden="true"> <svg viewBox="0 0 24 24" aria-hidden="true">

View File

@@ -43,12 +43,10 @@ export default function SiteHeader({ menuOpen, theme, onMenuToggle, onThemeToggl
</button> </button>
<nav id="nav-menu" className={`nav-menu${menuOpen ? " is-open" : ""}`} onClick={onNavClick}> <nav id="nav-menu" className={`nav-menu${menuOpen ? " is-open" : ""}`} onClick={onNavClick}>
<a href="#services">Services</a> <a href="#why">Why email matters</a>
<a href="#infrastructure">Infrastructure</a> <a href="#pricing-detail">Pricing</a>
<a href="#pricing">Pricing</a> <a href="#faq">FAQ</a>
<a href="#migration">Migration</a> <a href="#assessment">Get assessment</a>
<a href="#faq">Resources</a>
<a href="#assessment">Support</a>
</nav> </nav>
<div className="header-actions"> <div className="header-actions">

21
package-lock.json generated
View File

@@ -9,11 +9,13 @@
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"next": "^15.3.3", "next": "^15.3.3",
"nodemailer": "^6.9.16",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.15.24", "@types/node": "^22.15.24",
"@types/nodemailer": "^6.4.17",
"@types/react": "^19.0.12", "@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
"typescript": "^5.8.3" "typescript": "^5.8.3"
@@ -648,6 +650,16 @@
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@types/nodemailer": {
"version": "6.4.24",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.24.tgz",
"integrity": "sha512-Ww4u0rT9wQNXh4JiQaIwx3QWdcOFXzOjQA2zc+jtFYNmQiT4mIUqcDin51bDFdkzKubFnQCZNK7FIHlPKQ/q9w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "19.2.15", "version": "19.2.15",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz",
@@ -781,6 +793,15 @@
} }
} }
}, },
"node_modules/nodemailer": {
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz",
"integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",

View File

@@ -10,11 +10,13 @@
}, },
"dependencies": { "dependencies": {
"next": "^15.3.3", "next": "^15.3.3",
"nodemailer": "^6.9.16",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.15.24", "@types/node": "^22.15.24",
"@types/nodemailer": "^6.4.17",
"@types/react": "^19.0.12", "@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
"typescript": "^5.8.3" "typescript": "^5.8.3"