Compare commits
6 Commits
45422753a3
...
knuti
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ce04ba694 | |||
| 143ca13601 | |||
| f36cb9ce51 | |||
| fa33ccbe72 | |||
| d6f6c6480d | |||
| 546d6fbba3 |
22
.dockerignore
Normal file
@@ -0,0 +1,22 @@
|
||||
node_modules/
|
||||
.next/
|
||||
.git/
|
||||
.gitignore
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
.env
|
||||
.env.*
|
||||
npm-debug.log*
|
||||
.env.example
|
||||
docker-compose.yml
|
||||
README.md
|
||||
PLAN.md
|
||||
PRODUCT.md
|
||||
PROJECT_BRIEF.md
|
||||
SHAPE_BRIEF.md
|
||||
DESIGN.md
|
||||
research/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.vscode/
|
||||
.idea/
|
||||
31
Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
||||
# ---- deps ----
|
||||
FROM node:22-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
# ---- builder ----
|
||||
FROM node:22-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
RUN npm run build
|
||||
|
||||
# ---- runner ----
|
||||
FROM node:22-alpine AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV PORT=3000
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs && \
|
||||
adduser --system --uid 1001 nextjs
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
CMD ["node", "server.js"]
|
||||
@@ -492,9 +492,17 @@ svg {
|
||||
align-content: start;
|
||||
gap: var(--space-3);
|
||||
padding-block: clamp(0.4rem, 2vw, 1rem);
|
||||
max-width: 56rem;
|
||||
margin-inline: auto;
|
||||
text-align: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.hero-copy h1 {
|
||||
max-width: none;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0;
|
||||
color: oklch(80% 0.1 246);
|
||||
@@ -535,19 +543,23 @@ h3 {
|
||||
}
|
||||
|
||||
.hero-lede {
|
||||
max-width: 32rem;
|
||||
max-width: 38rem;
|
||||
margin-inline: auto;
|
||||
margin-bottom: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: clamp(1rem, 0.45vw + 0.93rem, 1.15rem);
|
||||
line-height: 1.38;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-local-trust {
|
||||
max-width: 32rem;
|
||||
margin: var(--space-2) 0 0;
|
||||
max-width: 38rem;
|
||||
margin-inline: auto;
|
||||
margin-top: var(--space-2);
|
||||
color: var(--blue);
|
||||
font-size: var(--step--1);
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.trust-list,
|
||||
@@ -561,6 +573,7 @@ h3 {
|
||||
.trust-list {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
justify-items: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@@ -621,6 +634,7 @@ h3 {
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
|
||||
@@ -2973,6 +2987,36 @@ details p {
|
||||
padding: 0 var(--space-4) var(--space-4);
|
||||
}
|
||||
|
||||
.faq-answer-body {
|
||||
padding: 0 var(--space-4) var(--space-4);
|
||||
}
|
||||
|
||||
.faq-answer-body p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.faq-diagram-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 42rem;
|
||||
height: auto;
|
||||
margin-top: var(--space-4);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.faq-diagram-light {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .faq-diagram-dark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .faq-diagram-light {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.assessment-section {
|
||||
position: relative;
|
||||
display: block;
|
||||
@@ -3508,6 +3552,13 @@ textarea::placeholder {
|
||||
}
|
||||
|
||||
@media (min-width: 760px) {
|
||||
.trust-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.site-header {
|
||||
grid-template-columns: auto 1fr auto;
|
||||
}
|
||||
@@ -3593,8 +3644,12 @@ textarea::placeholder {
|
||||
}
|
||||
|
||||
.hero-grid {
|
||||
grid-template-columns: minmax(18rem, 0.55fr) minmax(43rem, 1.65fr);
|
||||
gap: clamp(2.5rem, 4vw, 5rem);
|
||||
gap: clamp(2.5rem, 4vw, 4rem);
|
||||
}
|
||||
|
||||
.architecture-panel {
|
||||
max-width: 52rem;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.module-grid {
|
||||
@@ -3840,10 +3895,6 @@ textarea::placeholder {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.hero-lede {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.hero-copy {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,17 @@ export const metadata: Metadata = {
|
||||
title: "Business Email Hosting Corpus Christi | Bay Area Email Services",
|
||||
description:
|
||||
"Professional domain email hosting for Corpus Christi businesses. 25 GB mailboxes, Outlook and iPhone setup, SPF/DKIM/DMARC, migration, and local support.",
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: "/favicon.svg", type: "image/svg+xml" },
|
||||
{ url: "/favicon.ico" },
|
||||
{ url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" },
|
||||
{ url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" },
|
||||
{ url: "/favicon-48x48.png", sizes: "48x48", type: "image/png" },
|
||||
],
|
||||
shortcut: "/favicon.ico",
|
||||
apple: { url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" },
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
16
app/manifest.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export default function manifest(): MetadataRoute.Manifest {
|
||||
return {
|
||||
name: "Bay Area Email Services",
|
||||
short_name: "Bay Area Email",
|
||||
start_url: "/",
|
||||
display: "standalone",
|
||||
background_color: "#05070d",
|
||||
theme_color: "#000080",
|
||||
icons: [
|
||||
{ src: "/icon-192.png", sizes: "192x192", type: "image/png" },
|
||||
{ src: "/icon-512.png", sizes: "512x512", type: "image/png" },
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -18,6 +18,28 @@ export default function FaqSection() {
|
||||
<summary>Can you migrate our existing email?</summary>
|
||||
<p>Yes. The migration plan depends on your current provider, mailbox count, domain access, and preferred migration window.</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>How does mail stay online if a server goes down?</summary>
|
||||
<div className="faq-answer-body">
|
||||
<p>Incoming mail is temporarily held in Amazon AWS if the primary server needs maintenance or encounters an issue — your messages aren’t lost. A standby backup environment is ready to take over, so your team can keep sending and receiving. Once things are back to normal, buffered mail is delivered to your inboxes automatically.</p>
|
||||
<img
|
||||
className="faq-diagram-image faq-diagram-dark"
|
||||
src="/assets/mail-flow-panel.png"
|
||||
alt="Diagram of Bay Area Email mail flow: inbound buffering, mailbox delivery, outbound via Amazon SES, and standby failover."
|
||||
loading="lazy"
|
||||
/>
|
||||
<img
|
||||
className="faq-diagram-image faq-diagram-light"
|
||||
src="/assets/mail-flow-panel-light.png"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
loading="lazy"
|
||||
/>
|
||||
<p className="sr-only">
|
||||
Inbound messages are collected from the internet, remote mail servers, and other providers, buffered and processed through Amazon S3, delivered to mailboxes, sent outbound through Amazon SES, and supported by standby infrastructure.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Does this work with Outlook, iPhone, and iPad?</summary>
|
||||
<p>Yes. Outlook, iPhone, iPad, web, and desktop access are part of the setup conversation.</p>
|
||||
@@ -28,7 +50,7 @@ export default function FaqSection() {
|
||||
</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>
|
||||
<p>Setup and migration are included at no extra charge for teams that switch by July 31, 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>
|
||||
</section>
|
||||
|
||||
@@ -35,24 +35,6 @@ export default function HeroSection() {
|
||||
<a className="call-inline" href="tel:+13617658400">Call (361) 765-8400</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section id="infrastructure" className="architecture-panel" aria-labelledby="architecture-title">
|
||||
<h2 id="architecture-title" className="sr-only">Mail flow designed for continuity</h2>
|
||||
<img
|
||||
className="architecture-image architecture-image-dark"
|
||||
src="/assets/mail-flow-panel.png"
|
||||
alt="Mail flow diagram showing inbound email, Amazon S3 buffering, mailbox delivery, Amazon SES outbound sending, standby infrastructure, and operational system status."
|
||||
/>
|
||||
<img
|
||||
className="architecture-image architecture-image-light"
|
||||
src="/assets/mail-flow-panel-light.png"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<p className="sr-only">
|
||||
Inbound messages are collected from the internet, remote mail servers, and other providers, buffered and processed through Amazon S3, delivered to mailboxes, sent outbound through Amazon SES, and supported by standby infrastructure.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="module-grid" aria-label="Service highlights">
|
||||
|
||||
@@ -39,10 +39,10 @@ export default function PricingSection({ mailboxes, onMailboxesChange }: Pricing
|
||||
|
||||
<div className="offer-banner">
|
||||
<span className="offer-banner-icon" aria-hidden="true">★</span>
|
||||
Free migration & setup for teams that switch by June 30, 2026.
|
||||
Free migration & setup for teams that switch by July 31, 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>
|
||||
<p className="price-subtext">$5 per mailbox / month. Free migration if you switch by July 31, 2026. Final scope confirmed in your assessment.</p>
|
||||
|
||||
<a className="button button-primary" href="#assessment">Get a mailbox count quote</a>
|
||||
</div>
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function SiteFooter() {
|
||||
</div>
|
||||
|
||||
<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 <a href="https://bayarea-cc.com" target="_blank" rel="noopener noreferrer">Bay Area IT</a>. All rights reserved.</p>
|
||||
<div>
|
||||
<a href="#top" className="back-to-top">
|
||||
Back to top
|
||||
|
||||
28
docker-compose.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
# docker compose up -d --build
|
||||
#
|
||||
# The host runs Caddy as a reverse proxy in its own compose project.
|
||||
# Caddy forwards requests for email-cc.com to this container by name:
|
||||
# reverse_proxy email-cc-web:3000
|
||||
# For that to resolve, Caddy and this container must share a Docker network.
|
||||
# The network "email-cc" is created by the Caddy compose project and joined
|
||||
# here as external.
|
||||
#
|
||||
# Prerequisites: a .env file in the project directory with:
|
||||
# SES_SMTP_USER=
|
||||
# SES_SMTP_PASS= (SMTP credentials, not IAM access keys)
|
||||
# SES_FROM_EMAIL= (must be an SES-verified identity)
|
||||
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
container_name: email-cc-web
|
||||
env_file:
|
||||
- .env
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- email-cc
|
||||
|
||||
networks:
|
||||
email-cc:
|
||||
external: true
|
||||
name: email-cc
|
||||
@@ -1,4 +1,7 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
const nextConfig = {
|
||||
output: "standalone",
|
||||
outputFileTracingRoot: process.cwd(),
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
BIN
public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 425 B |
BIN
public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 757 B |
BIN
public/favicon-48x48.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 426 B |
13
public/favicon.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#000080"/>
|
||||
<stop offset="1" stop-color="#0000FF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="0" y="0" width="512" height="512" rx="112" fill="url(#bg)"/>
|
||||
<rect x="104" y="150" width="304" height="212" rx="28" fill="#FFFFFF"/>
|
||||
<path d="M132 182 L256 286 L380 182"
|
||||
fill="none" stroke="#0000FF" stroke-width="30"
|
||||
stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 629 B |
BIN
public/icon-192.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
public/icon-512.png
Normal file
|
After Width: | Height: | Size: 19 KiB |