diff --git a/.env.example b/.env.example
index 56c45c1..8682e4e 100644
--- a/.env.example
+++ b/.env.example
@@ -1,9 +1,9 @@
# Public site URL used for canonical metadata, sitemap, and robots.
-NEXT_PUBLIC_SITE_URL=https://anusbutt.com
+NEXT_PUBLIC_SITE_URL=https://www.anasbutt.site
# Resend API key. Keep this server-only and configure it in Vercel/local .env.local.
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxxxxxxx
# Verified sender and destination addresses for the fixed portfolio contact workflow.
-CONTACT_FROM_EMAIL=portfolio@anusbutt.com
+CONTACT_FROM_EMAIL=portfolio@anasbutt.site
CONTACT_TO_EMAIL=your-email@example.com
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index f8584b9..13b3b5d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,7 +25,6 @@ Thumbs.db
# IDE
.vscode/
.idea/
-.vercel
# Local Spec-Driven Development and agent workspace
.agents/
.claude/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 59d98be..3d8d3d6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,6 +1,6 @@
# Contributing
-1. Read the feature specification and plan under specs/portfolio-architecture-refactor/ before making architectural changes.
+1. Read README.md and the relevant source files before making a change.
2. Keep portfolio content in src/content and avoid duplicating identity values in components.
3. Run npm run check and npm run test:e2e before opening a pull request.
4. Preserve accessible keyboard navigation, reduced-motion behavior, responsive layout, and the existing visual identity.
diff --git a/README.md b/README.md
index 3da624b..72838fe 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
Personal portfolio for Anus Butt, an AI engineer and full-stack engineer building auditable agent systems, developer tools, and production web applications.
-Live site: https://anusbutt.com
+Live site: https://www.anasbutt.site
## Architecture
@@ -62,7 +62,7 @@ npm run check
## Deployment
-Deploy the repository to Vercel with the four environment variables configured in the project settings. Pushes to the main branch can use the included GitHub Actions checks as the merge gate. The contact limiter is intentionally an in-memory, per-instance guard suitable for low-volume portfolio traffic; a managed limiter would be the next step if abuse volume grows.
+Deploy the repository to Vercel with the four environment variables configured in the project settings. Pushes to the default branch can use the included GitHub Actions checks as the merge gate. The contact limiter is intentionally an in-memory, per-instance guard suitable for low-volume portfolio traffic; a managed limiter would be the next step if abuse volume grows.
## Contributing and security
diff --git a/next-env.d.ts b/next-env.d.ts
index a419cbe..ce4e94a 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,7 +1,7 @@
///
///
-import "./.next/dev/types/routes.d.ts";
-import "./.next/dev/types/root-params.d.ts";
+import "./.next/types/routes.d.ts";
+import "./.next/types/root-params.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/src/app/page.tsx b/src/app/page.tsx
index c7a199a..0c0a43a 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,4 +1,5 @@
import { profile } from "@/content/profile";
+import { siteUrl } from "@/content/site";
import Navbar from "@/components/layout/Navbar";
import Hero from "@/components/sections/hero/Hero";
import About from "@/components/sections/about/About";
@@ -14,6 +15,7 @@ const personJsonLd = {
jobTitle: profile.title,
address: { "@type": "PostalAddress", addressCountry: "Pakistan" },
sameAs: profile.sameAs,
+ url: siteUrl.toString(),
};
export default function Home() {
@@ -21,7 +23,9 @@ export default function Home() {
diff --git a/src/components/sections/contact/ContactForm.tsx b/src/components/sections/contact/ContactForm.tsx
index c4c1ce4..c7a7132 100644
--- a/src/components/sections/contact/ContactForm.tsx
+++ b/src/components/sections/contact/ContactForm.tsx
@@ -2,6 +2,7 @@
import { useState, FormEvent } from "react";
import { motion } from "framer-motion";
+import { CONTACT_FIELD_LIMITS, isValidContactEmail } from "@/shared/contact";
interface FormErrors {
name?: string;
@@ -13,6 +14,7 @@ export default function ContactForm() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [message, setMessage] = useState("");
+ const [website, setWebsite] = useState("");
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [status, setStatus] = useState<"idle" | "success" | "error">("idle");
@@ -20,15 +22,20 @@ export default function ContactForm() {
function validate(): FormErrors {
const errs: FormErrors = {};
- if (!name.trim()) errs.name = "Name is required";
- else if (name.length > 100) errs.name = "Name must be 100 characters or less";
+ const trimmedName = name.trim();
+ const trimmedEmail = email.trim();
+ const trimmedMessage = message.trim();
- if (!email.trim()) errs.email = "Email is required";
- else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
- errs.email = "Please enter a valid email";
+ if (!trimmedName) errs.name = "Name is required";
+ else if (trimmedName.length > CONTACT_FIELD_LIMITS.name)
+ errs.name = "Name must be 100 characters or less";
- if (!message.trim()) errs.message = "Message is required";
- else if (message.length > 2000) errs.message = "Message must be 2000 characters or less";
+ if (!trimmedEmail) errs.email = "Email is required";
+ else if (!isValidContactEmail(trimmedEmail)) errs.email = "Please enter a valid email";
+
+ if (!trimmedMessage) errs.message = "Message is required";
+ else if (trimmedMessage.length > CONTACT_FIELD_LIMITS.message)
+ errs.message = "Message must be 2000 characters or less";
return errs;
}
@@ -50,7 +57,12 @@ export default function ContactForm() {
const res = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ name: name.trim(), email: email.trim(), message: message.trim() }),
+ body: JSON.stringify({
+ name: name.trim(),
+ email: email.trim(),
+ message: message.trim(),
+ website,
+ }),
});
const data = await res.json();
@@ -61,6 +73,7 @@ export default function ContactForm() {
setName("");
setEmail("");
setMessage("");
+ setWebsite("");
} else {
setStatus("error");
setStatusMessage(data.message || "Failed to send message. Please try again.");
@@ -167,6 +180,19 @@ export default function ContactForm() {
)}
+