Public marketing website for Techno Hub Technology (PVT) LTD., a Sri Lankan IT products & solutions company based in Anuradhapura.
Built with Next.js 15 (App Router) + TypeScript + Tailwind CSS, deployable to Vercel with no extra configuration.
npm install
npm run devThen open http://localhost:3000.
The contact form works immediately in development — with no API key configured
the /api/contact route validates the submission, logs it to the server
console, and returns success. See Wiring up email to turn on
real delivery.
| Script | What it does |
|---|---|
npm run dev |
Start the dev server |
npm run build |
Production build |
npm start |
Serve the production build |
npm run typecheck |
tsc --noEmit |
The project ships without a linter. To add the standard Next.js setup:
npm i -D eslint eslint-config-next @eslint/eslintrcthen create eslint.config.mjs:
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
});
export default [
...compat.extends("next/core-web-vitals", "next/typescript"),
{ ignores: [".next/**", "node_modules/**", "next-env.d.ts"] },
];and add "lint": "eslint ." to scripts. (Use the ESLint CLI rather than
next lint, which is deprecated in Next.js 15 and removed in 16.)
The contact form posts JSON to app/api/contact/route.ts. That route validates
the payload with the shared Zod schema in lib/contact-schema.ts, then sends the
message with Resend.
Copy the template and fill it in:
cp .env.example .env.local| Variable | Required | Purpose |
|---|---|---|
RESEND_API_KEY |
for mail | Resend API key. Without it the route logs instead of sending. |
CONTACT_FROM_EMAIL |
no | Sender address. Must be on a domain verified in Resend. Defaults to onboarding@resend.dev, which works for testing. |
CONTACT_TO_EMAIL |
no | Where enquiries are delivered. Defaults to the company email. |
NEXT_PUBLIC_SITE_URL |
no | Absolute site URL for canonical + Open Graph tags. Defaults to https://technohub.lk. |
- Sign up at https://resend.com and open API Keys → Create API Key.
- Paste it into
.env.localasRESEND_API_KEY. - To send from your own domain, add it under Domains and complete the DNS
records, then set
CONTACT_FROM_EMAILto an address on that domain. - Restart the dev server.
Submissions now arrive at CONTACT_TO_EMAIL, with the sender's address set as
replyTo so replying from your inbox goes straight back to the customer.
Add the same variables under Project → Settings → Environment Variables, then redeploy. Nothing else is required — the project builds and deploys as-is.
If you would rather send through Gmail or your hosting provider's SMTP server,
replace the Resend block in app/api/contact/route.ts with:
import nodemailer from "nodemailer"; // npm i nodemailer
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST, // e.g. smtp.gmail.com
port: Number(process.env.SMTP_PORT ?? 587),
secure: false,
auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
});
await transporter.sendMail({
from: process.env.CONTACT_FROM_EMAIL,
to: process.env.CONTACT_TO_EMAIL,
replyTo: email,
subject: `[Website] ${subject}`,
text: `${name} (${email}, ${phone})\n\n${message}`,
});For Gmail you must use an App Password, not the account password.
app/
layout.tsx Root layout: fonts, metadata, navbar, footer
page.tsx Home — single long scrolling page
products/page.tsx Products route
contact/page.tsx Contact route
api/contact/route.ts Contact form handler (Resend)
not-found.tsx 404
robots.ts / sitemap.ts SEO routes
icon.svg Favicon
components/
ui/ Button, Container, Logo, Icon, Reveal, SectionLabel,
Spark, PageHeader
sections/ Navbar, Hero, WhyChooseUs, Solutions, About,
FeaturedProducts, StatsBar, CTABanner, Footer
products/ ProductCatalog (filter chips + grid)
contact/ ContactForm (React Hook Form + Zod)
lib/
company.ts Single source of truth for all company content
products.ts Static product & category catalogue
contact-schema.ts Zod schema shared by the form and the API route
cn.ts Classname helper
public/
hero.svg, products/* Placeholder imagery (see below)
Almost all copy lives in lib/company.ts (name, tagline, email, address,
phone numbers, hours, mission, vision, why-choose-us list) and
lib/products.ts (categories and product ranges). Change it there and it
updates everywhere on the site.
Privacy note: the owner's personal phone number and personal email address are deliberately not stored anywhere in this repository. Only the business email and the three business phone numbers are published.
Every file in public/products/ and public/hero.svg is a generated
dark-gradient placeholder panel — correct in tone and aspect ratio, but not a
real product photo. Each usage is marked with a
// TODO: replace with a real product photo comment.
To swap them in:
- Drop real photos into
public/products/(JPG, PNG or WebP). - Update the
imagefield for the relevant entries inlib/products.ts. - Once no SVGs remain, delete
dangerouslyAllowSVG,contentDispositionTypeandcontentSecurityPolicyfromnext.config.mjs— they exist only so the image optimizer will serve the placeholder SVGs.
Recommended aspect ratios: 1:1 or 4:5 for product cards, 4:3 for the hero.
| Token | Value | Used for |
|---|---|---|
ink-950 |
#0A0A0A |
Page background |
ink-900 |
#0D0D0D |
Alternating section bands |
ink-800 |
#151515 |
Cards and panels |
brand-600 |
#E31E24 |
Buttons, icons, accents |
brand-500 |
#FF1E2D |
Headline highlights, hover states |
font-display |
Archivo (600–900) | Headlines, uppercase labels |
font-sans |
Inter | Body copy |
Helper classes display-xl / display-lg / display-md (in app/globals.css)
provide the fluid, tight-tracking editorial headline scale. .panel is the
standard dark card treatment.
Measured contrast ratios against the backgrounds they are actually used on:
| Combination | Ratio | WCAG |
|---|---|---|
White on brand-600 (buttons) |
4.69:1 | AA normal text |
White on ink-950 |
19.8:1 | AAA |
brand-500 on ink-950 |
5.16:1 | AA normal text |
gray-400 on ink-950 |
7.80:1 | AAA |
gray-400 on ink-800 |
7.19:1 | AA normal text |
brand-400 on ink-800 |
5.52:1 | AA normal text |
gray-500 (4.02:1) is deliberately not used for any informational text — it
remains only as the form-input placeholder colour and one decorative arrow icon.
- Visible focus rings on every interactive element, a skip-to-content link,
semantic landmarks,
alttext on all images, andaria-liveannouncements on the contact form result and the product filter count. - All animation is wrapped in
prefers-reduced-motionguards.
- Push the repository to GitHub.
- Import it at https://vercel.com/new — the framework is detected automatically, no build settings to change.
- Add the environment variables from Wiring up email.
- Deploy.