SEO overhaul: metadata, robots, sitemap, structured data
- Add metadataBase to root layout so OG/Twitter/canonical URLs resolve to absolute https URLs (fixes broken social previews) - New src/lib/seo.ts with SITE_URL + JSON-LD builders - New robots.ts (disallow api/admin/private app paths) and sitemap.ts (marketing pages + blog posts with real lastmod dates) - JSON-LD: Organization/WebSite/SoftwareApplication on home, Blog+Breadcrumb on blog list, BlogPosting+Breadcrumb on posts - Per-page canonical + Open Graph on all marketing pages; article OG + Twitter cards on blog posts; per-post dynamic OG image - noindex on (app) and admin layouts; richer PWA manifest - Fix CSP to allow plausible.io in script-src/connect-src (analytics was silently blocked) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
39b2787484
commit
dad0611350
18 changed files with 500 additions and 15 deletions
|
|
@ -23,9 +23,9 @@ const nextConfig: NextConfig = {
|
||||||
{ key: "Content-Security-Policy", value:
|
{ key: "Content-Security-Policy", value:
|
||||||
"default-src 'self'; " +
|
"default-src 'self'; " +
|
||||||
"img-src 'self' data: https://*.r2.cloudflarestorage.com https://*.r2.dev; " +
|
"img-src 'self' data: https://*.r2.cloudflarestorage.com https://*.r2.dev; " +
|
||||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://plausible.io; " +
|
||||||
"style-src 'self' 'unsafe-inline'; " +
|
"style-src 'self' 'unsafe-inline'; " +
|
||||||
"connect-src 'self' https://llm.manohargupta.com; " +
|
"connect-src 'self' https://llm.manohargupta.com https://plausible.io; " +
|
||||||
"font-src 'self' data:;"
|
"font-src 'self' data:;"
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,9 +1,15 @@
|
||||||
|
import type { Metadata } from "next";
|
||||||
import { ThemeProvider } from "@/app/ThemeProvider";
|
import { ThemeProvider } from "@/app/ThemeProvider";
|
||||||
import { FamilyProvider } from "@/app/FamilyProvider";
|
import { FamilyProvider } from "@/app/FamilyProvider";
|
||||||
import { PageTransition } from "@/components/PageTransition";
|
import { PageTransition } from "@/components/PageTransition";
|
||||||
import { BottomNav } from "@/components/BottomNav";
|
import { BottomNav } from "@/components/BottomNav";
|
||||||
import { InstallPrompt } from "@/components/InstallPrompt";
|
import { InstallPrompt } from "@/components/InstallPrompt";
|
||||||
|
|
||||||
|
// Private, authenticated app — never index these pages.
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
robots: { index: false, follow: false },
|
||||||
|
};
|
||||||
|
|
||||||
export default function AppLayout({
|
export default function AppLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,15 @@ import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "About",
|
title: "About Tia",
|
||||||
description: "The story behind Tia — built for Indian families by a parent who wanted something better.",
|
description: "The story behind Tia — built for Indian families by a parent who wanted something better.",
|
||||||
|
alternates: { canonical: "/about" },
|
||||||
|
openGraph: {
|
||||||
|
type: "website",
|
||||||
|
url: "/about",
|
||||||
|
title: "About Tia",
|
||||||
|
description: "The story behind Tia — built for Indian families by a parent who wanted something better.",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AboutPage() {
|
export default function AboutPage() {
|
||||||
|
|
|
||||||
82
src/app/(marketing)/blog/[slug]/opengraph-image.tsx
Normal file
82
src/app/(marketing)/blog/[slug]/opengraph-image.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { ImageResponse } from "next/og";
|
||||||
|
import { getPost, POSTS } from "../posts";
|
||||||
|
|
||||||
|
export const alt = "Tia Blog";
|
||||||
|
export const size = { width: 1200, height: 630 };
|
||||||
|
export const contentType = "image/png";
|
||||||
|
|
||||||
|
// Pre-render an OG image for every known post.
|
||||||
|
export function generateStaticParams() {
|
||||||
|
return POSTS.map((p) => ({ slug: p.slug }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function BlogOgImage({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ slug: string }>;
|
||||||
|
}) {
|
||||||
|
const { slug } = await params;
|
||||||
|
const post = getPost(slug);
|
||||||
|
|
||||||
|
const title = post?.title ?? "The Tia Blog";
|
||||||
|
const category = post?.category ?? "Journal";
|
||||||
|
const emoji = post?.emoji ?? "🌸";
|
||||||
|
|
||||||
|
return new ImageResponse(
|
||||||
|
(
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(135deg, #fdf2f2 0%, #fef3c7 50%, #fdf2f2 100%)",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
fontFamily: "sans-serif",
|
||||||
|
padding: 80,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||||
|
<span style={{ fontSize: 40 }}>🌸</span>
|
||||||
|
<span style={{ fontSize: 34, fontWeight: 800, color: "#111827" }}>
|
||||||
|
Tia
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
marginLeft: 12,
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "#f43f5e",
|
||||||
|
background: "#ffe4e6",
|
||||||
|
padding: "6px 18px",
|
||||||
|
borderRadius: 999,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{category}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||||
|
<div style={{ fontSize: 64, marginBottom: 16 }}>{emoji}</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 56,
|
||||||
|
fontWeight: 800,
|
||||||
|
color: "#111827",
|
||||||
|
lineHeight: 1.15,
|
||||||
|
maxWidth: 1000,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ fontSize: 24, color: "#6b7280" }}>
|
||||||
|
tia.manohargupta.com/blog
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
{ ...size }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,11 @@ import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { POSTS, getPost, formatDate } from "../posts";
|
import { POSTS, getPost, formatDate } from "../posts";
|
||||||
import { Breadcrumb } from "@/components/marketing/Breadcrumb";
|
import { Breadcrumb } from "@/components/marketing/Breadcrumb";
|
||||||
|
import {
|
||||||
|
blogPostingSchema,
|
||||||
|
breadcrumbSchema,
|
||||||
|
jsonLdScript,
|
||||||
|
} from "@/lib/seo";
|
||||||
|
|
||||||
// Turn a heading string into a URL-safe anchor ID
|
// Turn a heading string into a URL-safe anchor ID
|
||||||
function slugifyHeading(heading: string): string {
|
function slugifyHeading(heading: string): string {
|
||||||
|
|
@ -26,9 +31,29 @@ export async function generateMetadata({
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
const post = getPost(slug);
|
const post = getPost(slug);
|
||||||
if (!post) return {};
|
if (!post) return {};
|
||||||
|
const url = `/blog/${post.slug}`;
|
||||||
return {
|
return {
|
||||||
title: post.title,
|
title: post.title,
|
||||||
description: post.excerpt,
|
description: post.excerpt,
|
||||||
|
alternates: { canonical: url },
|
||||||
|
openGraph: {
|
||||||
|
type: "article",
|
||||||
|
url,
|
||||||
|
siteName: "Tia",
|
||||||
|
locale: "en_IN",
|
||||||
|
title: post.title,
|
||||||
|
description: post.excerpt,
|
||||||
|
publishedTime: post.date,
|
||||||
|
modifiedTime: post.date,
|
||||||
|
authors: [post.author],
|
||||||
|
section: post.category,
|
||||||
|
// Uses the per-post opengraph-image.tsx in this segment.
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
title: post.title,
|
||||||
|
description: post.excerpt,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,6 +77,18 @@ export default async function BlogPostPage({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-white">
|
||||||
|
{/* Structured data — BlogPosting + breadcrumb trail */}
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={jsonLdScript([
|
||||||
|
blogPostingSchema(post),
|
||||||
|
breadcrumbSchema([
|
||||||
|
{ name: "Home", path: "/" },
|
||||||
|
{ name: "Blog", path: "/blog" },
|
||||||
|
{ name: post.title, path: `/blog/${post.slug}` },
|
||||||
|
]),
|
||||||
|
])}
|
||||||
|
/>
|
||||||
{/* Post hero / header */}
|
{/* Post hero / header */}
|
||||||
<div className="bg-gradient-to-br from-rose-50 to-pink-50 border-b border-rose-100">
|
<div className="bg-gradient-to-br from-rose-50 to-pink-50 border-b border-rose-100">
|
||||||
<div className="max-w-6xl mx-auto px-5 pt-10 pb-10">
|
<div className="max-w-6xl mx-auto px-5 pt-10 pb-10">
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,46 @@ import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { POSTS, formatDate } from "./posts";
|
import { POSTS, formatDate } from "./posts";
|
||||||
import { Breadcrumb } from "@/components/marketing/Breadcrumb";
|
import { Breadcrumb } from "@/components/marketing/Breadcrumb";
|
||||||
|
import {
|
||||||
|
SITE_NAME,
|
||||||
|
absoluteUrl,
|
||||||
|
blogPostingSchema,
|
||||||
|
breadcrumbSchema,
|
||||||
|
jsonLdScript,
|
||||||
|
} from "@/lib/seo";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Blog",
|
title: "Blog — Baby Feeding, Health & Vaccination Guides",
|
||||||
description:
|
description:
|
||||||
"Guides on baby feeding, health milestones, vaccination schedules, and how to make the most of Tia — written for Indian families.",
|
"Guides on baby feeding, health milestones, vaccination schedules, and how to make the most of Tia — written for Indian families.",
|
||||||
|
alternates: { canonical: "/blog" },
|
||||||
|
openGraph: {
|
||||||
|
type: "website",
|
||||||
|
url: "/blog",
|
||||||
|
siteName: SITE_NAME,
|
||||||
|
locale: "en_IN",
|
||||||
|
title: `Blog | ${SITE_NAME}`,
|
||||||
|
description:
|
||||||
|
"Guides on baby feeding, health milestones, vaccination schedules, and how to make the most of Tia — written for Indian families.",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Blog (with each post) + breadcrumb structured data.
|
||||||
|
const blogSchema = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Blog",
|
||||||
|
"@id": `${absoluteUrl("/blog")}#blog`,
|
||||||
|
name: `${SITE_NAME} Blog`,
|
||||||
|
url: absoluteUrl("/blog"),
|
||||||
|
inLanguage: "en-IN",
|
||||||
|
blogPost: POSTS.map((p) => blogPostingSchema(p)),
|
||||||
|
};
|
||||||
|
|
||||||
|
const blogBreadcrumb = breadcrumbSchema([
|
||||||
|
{ name: "Home", path: "/" },
|
||||||
|
{ name: "Blog", path: "/blog" },
|
||||||
|
]);
|
||||||
|
|
||||||
// Derive unique categories + counts from posts
|
// Derive unique categories + counts from posts
|
||||||
const CATEGORIES = Object.values(
|
const CATEGORIES = Object.values(
|
||||||
POSTS.reduce<Record<string, { name: string; color: string; count: number }>>((acc, p) => {
|
POSTS.reduce<Record<string, { name: string; color: string; count: number }>>((acc, p) => {
|
||||||
|
|
@ -23,6 +56,10 @@ const CATEGORIES = Object.values(
|
||||||
export default function BlogPage() {
|
export default function BlogPage() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-white">
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={jsonLdScript([blogSchema, blogBreadcrumb])}
|
||||||
|
/>
|
||||||
{/* Page header */}
|
{/* Page header */}
|
||||||
<div className="bg-gradient-to-br from-rose-50 to-pink-50 border-b border-rose-100">
|
<div className="bg-gradient-to-br from-rose-50 to-pink-50 border-b border-rose-100">
|
||||||
<div className="max-w-6xl mx-auto px-5 py-12 text-center">
|
<div className="max-w-6xl mx-auto px-5 py-12 text-center">
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,38 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { PhoneMockup } from "@/components/marketing/PhoneMockup";
|
import { PhoneMockup } from "@/components/marketing/PhoneMockup";
|
||||||
|
import {
|
||||||
|
jsonLdScript,
|
||||||
|
organizationSchema,
|
||||||
|
websiteSchema,
|
||||||
|
softwareApplicationSchema,
|
||||||
|
} from "@/lib/seo";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Tia — Your baby's digital heirloom",
|
title: "Tia — Baby Tracker & Digital Heirloom for Indian Families",
|
||||||
description:
|
description:
|
||||||
"Tia is a digital heirloom for your baby — not just a tracker. Log daily moments, track the IAP vaccination schedule with Telegram alerts, and build a living archive your child will one day inherit.",
|
"Tia is a digital heirloom for your baby — not just a tracker. Log daily moments, track the IAP vaccination schedule with Telegram alerts, and build a living archive your child will one day inherit. Free during early access.",
|
||||||
|
keywords: [
|
||||||
|
"baby tracker",
|
||||||
|
"baby tracker app India",
|
||||||
|
"IAP vaccination schedule",
|
||||||
|
"baby vaccination tracker",
|
||||||
|
"newborn feed tracker",
|
||||||
|
"baby milestone tracker",
|
||||||
|
"digital baby book",
|
||||||
|
"parenting app India",
|
||||||
|
"baby memory keeper",
|
||||||
|
],
|
||||||
|
alternates: { canonical: "/" },
|
||||||
|
openGraph: {
|
||||||
|
type: "website",
|
||||||
|
url: "/",
|
||||||
|
siteName: "Tia",
|
||||||
|
locale: "en_IN",
|
||||||
|
title: "Tia — Baby Tracker & Digital Heirloom for Indian Families",
|
||||||
|
description:
|
||||||
|
"Log every feed, milestone, and memory. Track the IAP vaccination schedule with Telegram alerts. Built for Indian families. Privacy-first. Free during early access.",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Google G icon (reusable) ────────────────────────────────────
|
// ── Google G icon (reusable) ────────────────────────────────────
|
||||||
|
|
@ -373,6 +400,15 @@ function FinalCTA() {
|
||||||
export default function MarketingHomePage() {
|
export default function MarketingHomePage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* Structured data — Organization, WebSite, and SoftwareApplication */}
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={jsonLdScript([
|
||||||
|
organizationSchema(),
|
||||||
|
websiteSchema(),
|
||||||
|
softwareApplicationSchema(),
|
||||||
|
])}
|
||||||
|
/>
|
||||||
<Hero />
|
<Hero />
|
||||||
<TheProblem />
|
<TheProblem />
|
||||||
<Features />
|
<Features />
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,13 @@ import Link from "next/link";
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Partners",
|
title: "Partners",
|
||||||
description: "Clinics, pediatricians, and organisations partnering with Tia to support Indian families.",
|
description: "Clinics, pediatricians, and organisations partnering with Tia to support Indian families.",
|
||||||
|
alternates: { canonical: "/partners" },
|
||||||
|
openGraph: {
|
||||||
|
type: "website",
|
||||||
|
url: "/partners",
|
||||||
|
title: "Partners | Tia",
|
||||||
|
description: "Clinics, pediatricians, and organisations partnering with Tia to support Indian families.",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PartnersPage() {
|
export default function PartnersPage() {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,14 @@ import Link from "next/link";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Pricing",
|
title: "Pricing",
|
||||||
description: "Founder pricing — early families keep their terms for life.",
|
description: "Founder pricing — early families keep their terms for life. Free during early access.",
|
||||||
|
alternates: { canonical: "/pricing" },
|
||||||
|
openGraph: {
|
||||||
|
type: "website",
|
||||||
|
url: "/pricing",
|
||||||
|
title: "Pricing | Tia",
|
||||||
|
description: "Founder pricing — early families keep their terms for life. Free during early access.",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PricingPage() {
|
export default function PricingPage() {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,13 @@ import type { Metadata } from "next";
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Privacy Policy",
|
title: "Privacy Policy",
|
||||||
description: "How Tia handles your family's data. We don't sell it — we preserve it.",
|
description: "How Tia handles your family's data. We don't sell it — we preserve it.",
|
||||||
|
alternates: { canonical: "/privacy" },
|
||||||
|
openGraph: {
|
||||||
|
type: "website",
|
||||||
|
url: "/privacy",
|
||||||
|
title: "Privacy Policy | Tia",
|
||||||
|
description: "How Tia handles your family's data. We don't sell it — we preserve it.",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PrivacyPage() {
|
export default function PrivacyPage() {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,14 @@ import type { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Terms of Service",
|
title: "Terms of Service",
|
||||||
description: "Terms of service for Tia.",
|
description: "Terms of service for Tia — the baby tracker and digital heirloom for Indian families.",
|
||||||
|
alternates: { canonical: "/terms" },
|
||||||
|
openGraph: {
|
||||||
|
type: "website",
|
||||||
|
url: "/terms",
|
||||||
|
title: "Terms of Service | Tia",
|
||||||
|
description: "Terms of service for Tia — the baby tracker and digital heirloom for Indian families.",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TermsPage() {
|
export default function TermsPage() {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
|
import type { Metadata } from "next";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { verifyAdminSession } from "@/lib/admin-auth";
|
import { verifyAdminSession } from "@/lib/admin-auth";
|
||||||
import AdminSidebar from "./AdminSidebar";
|
import AdminSidebar from "./AdminSidebar";
|
||||||
|
|
||||||
|
// Admin panel — never index.
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
robots: { index: false, follow: false },
|
||||||
|
};
|
||||||
|
|
||||||
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
|
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||||
const auth = await verifyAdminSession();
|
const auth = await verifyAdminSession();
|
||||||
if (!auth.success) {
|
if (!auth.success) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { Metadata, Viewport } from "next";
|
import type { Metadata, Viewport } from "next";
|
||||||
import { Geist, Geist_Mono, Caveat } from "next/font/google";
|
import { Geist, Geist_Mono, Caveat } from "next/font/google";
|
||||||
|
import { SITE_URL, SITE_NAME } from "@/lib/seo";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
|
|
@ -18,17 +19,41 @@ const caveat = Caveat({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Tia — Baby Tracker",
|
// Resolves all relative metadata URLs (OG/Twitter images, canonicals) to
|
||||||
description: "Track feeds, sleep, milestones and memories.",
|
// absolute URLs — required for social previews to work.
|
||||||
|
metadataBase: new URL(SITE_URL),
|
||||||
|
title: {
|
||||||
|
default: "Tia — Baby Tracker & Digital Heirloom",
|
||||||
|
template: `%s | ${SITE_NAME}`,
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
"Track feeds, sleep, milestones, and memories. Tia is a privacy-first baby tracker and digital heirloom built for Indian families.",
|
||||||
|
applicationName: SITE_NAME,
|
||||||
|
authors: [{ name: "Tia" }],
|
||||||
|
creator: "Tia",
|
||||||
|
publisher: "Tia",
|
||||||
|
category: "parenting",
|
||||||
|
formatDetection: { telephone: false, email: false, address: false },
|
||||||
|
// apple-touch-icon is provided by the src/app/apple-icon.png file convention.
|
||||||
icons: {
|
icons: {
|
||||||
icon: "/icon.svg",
|
icon: "/icon.svg",
|
||||||
apple: "/apple-icon.png",
|
|
||||||
},
|
},
|
||||||
appleWebApp: {
|
appleWebApp: {
|
||||||
capable: true,
|
capable: true,
|
||||||
statusBarStyle: "default",
|
statusBarStyle: "default",
|
||||||
title: "Tia",
|
title: "Tia",
|
||||||
},
|
},
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
googleBot: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
"max-image-preview": "large",
|
||||||
|
"max-snippet": -1,
|
||||||
|
"max-video-preview": -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const viewport: Viewport = {
|
export const viewport: Viewport = {
|
||||||
|
|
@ -41,7 +66,7 @@ export default function RootLayout({
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en-IN" suppressHydrationWarning>
|
||||||
<body className={`${geistSans.variable} ${geistMono.variable} ${caveat.variable} min-h-full antialiased`}>
|
<body className={`${geistSans.variable} ${geistMono.variable} ${caveat.variable} min-h-full antialiased`}>
|
||||||
{children}
|
{children}
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,18 @@ import type { MetadataRoute } from "next";
|
||||||
|
|
||||||
export default function manifest(): MetadataRoute.Manifest {
|
export default function manifest(): MetadataRoute.Manifest {
|
||||||
return {
|
return {
|
||||||
name: "Tia — Baby Tracker",
|
name: "Tia — Baby Tracker & Digital Heirloom",
|
||||||
short_name: "Tia",
|
short_name: "Tia",
|
||||||
description: "Track feeds, sleep, milestones and memories.",
|
description:
|
||||||
|
"Track feeds, sleep, milestones, and memories. Tia is a privacy-first baby tracker and digital heirloom built for Indian families — with the IAP vaccination schedule and Telegram alerts.",
|
||||||
|
id: "/home",
|
||||||
start_url: "/home?source=pwa",
|
start_url: "/home?source=pwa",
|
||||||
|
scope: "/",
|
||||||
display: "standalone",
|
display: "standalone",
|
||||||
|
orientation: "portrait",
|
||||||
|
lang: "en-IN",
|
||||||
|
dir: "ltr",
|
||||||
|
categories: ["health", "lifestyle", "parenting", "medical"],
|
||||||
background_color: "#fdf2f2",
|
background_color: "#fdf2f2",
|
||||||
theme_color: "#fb7185",
|
theme_color: "#fb7185",
|
||||||
icons: [
|
icons: [
|
||||||
|
|
|
||||||
41
src/app/robots.ts
Normal file
41
src/app/robots.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import type { MetadataRoute } from "next";
|
||||||
|
import { SITE_URL } from "@/lib/seo";
|
||||||
|
|
||||||
|
export default function robots(): MetadataRoute.Robots {
|
||||||
|
return {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
userAgent: "*",
|
||||||
|
allow: "/",
|
||||||
|
// Private app, auth, and admin areas — keep them out of the index.
|
||||||
|
disallow: [
|
||||||
|
"/api/",
|
||||||
|
"/admin",
|
||||||
|
"/admin-login",
|
||||||
|
"/login",
|
||||||
|
"/verify",
|
||||||
|
"/invite/",
|
||||||
|
"/onboarding",
|
||||||
|
"/home",
|
||||||
|
"/activity",
|
||||||
|
"/ai",
|
||||||
|
"/circle",
|
||||||
|
"/family",
|
||||||
|
"/growth",
|
||||||
|
"/medical",
|
||||||
|
"/memories",
|
||||||
|
"/menu",
|
||||||
|
"/milestones",
|
||||||
|
"/notifications",
|
||||||
|
"/profile",
|
||||||
|
"/settings",
|
||||||
|
"/wardrobe",
|
||||||
|
"/dev",
|
||||||
|
"/m/",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sitemap: `${SITE_URL}/sitemap.xml`,
|
||||||
|
host: SITE_URL,
|
||||||
|
};
|
||||||
|
}
|
||||||
38
src/app/sitemap.ts
Normal file
38
src/app/sitemap.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import type { MetadataRoute } from "next";
|
||||||
|
import { SITE_URL } from "@/lib/seo";
|
||||||
|
import { POSTS } from "@/app/(marketing)/blog/posts";
|
||||||
|
|
||||||
|
export default function sitemap(): MetadataRoute.Sitemap {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// Static marketing routes with curated priorities.
|
||||||
|
const staticRoutes: {
|
||||||
|
path: string;
|
||||||
|
changeFrequency: MetadataRoute.Sitemap[number]["changeFrequency"];
|
||||||
|
priority: number;
|
||||||
|
}[] = [
|
||||||
|
{ path: "/", changeFrequency: "weekly", priority: 1.0 },
|
||||||
|
{ path: "/pricing", changeFrequency: "monthly", priority: 0.8 },
|
||||||
|
{ path: "/about", changeFrequency: "monthly", priority: 0.7 },
|
||||||
|
{ path: "/blog", changeFrequency: "weekly", priority: 0.7 },
|
||||||
|
{ path: "/partners", changeFrequency: "monthly", priority: 0.5 },
|
||||||
|
{ path: "/privacy", changeFrequency: "yearly", priority: 0.3 },
|
||||||
|
{ path: "/terms", changeFrequency: "yearly", priority: 0.3 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const staticEntries: MetadataRoute.Sitemap = staticRoutes.map((r) => ({
|
||||||
|
url: `${SITE_URL}${r.path}`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: r.changeFrequency,
|
||||||
|
priority: r.priority,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const blogEntries: MetadataRoute.Sitemap = POSTS.map((post) => ({
|
||||||
|
url: `${SITE_URL}/blog/${post.slug}`,
|
||||||
|
lastModified: new Date(post.date),
|
||||||
|
changeFrequency: "monthly",
|
||||||
|
priority: 0.6,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [...staticEntries, ...blogEntries];
|
||||||
|
}
|
||||||
135
src/lib/seo.ts
Normal file
135
src/lib/seo.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
// Central SEO constants and JSON-LD structured-data builders.
|
||||||
|
// Used across the marketing site so titles, canonicals, Open Graph,
|
||||||
|
// and schema.org markup all stay consistent.
|
||||||
|
|
||||||
|
/** Canonical production origin. Falls back to the known prod URL. */
|
||||||
|
export const SITE_URL = (
|
||||||
|
process.env.NEXT_PUBLIC_APP_URL || "https://tia.manohargupta.com"
|
||||||
|
).replace(/\/$/, "");
|
||||||
|
|
||||||
|
export const SITE_NAME = "Tia";
|
||||||
|
export const SITE_TAGLINE = "Your baby's digital heirloom";
|
||||||
|
export const SITE_DESCRIPTION =
|
||||||
|
"Tia is a digital heirloom for your baby — not just a tracker. Log every feed, milestone, and memory, track the IAP vaccination schedule with Telegram alerts, and build a living, private archive your child will one day inherit. Built for Indian families.";
|
||||||
|
|
||||||
|
/** Absolute URL helper — keeps canonical/OG urls consistent. */
|
||||||
|
export function absoluteUrl(path = "/"): string {
|
||||||
|
if (path.startsWith("http")) return path;
|
||||||
|
return `${SITE_URL}${path.startsWith("/") ? path : `/${path}`}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── JSON-LD builders ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Organization — publisher identity, reused as the publisher of articles. */
|
||||||
|
export function organizationSchema() {
|
||||||
|
return {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Organization",
|
||||||
|
"@id": `${SITE_URL}/#organization`,
|
||||||
|
name: SITE_NAME,
|
||||||
|
url: SITE_URL,
|
||||||
|
logo: {
|
||||||
|
"@type": "ImageObject",
|
||||||
|
url: absoluteUrl("/icons/512.png"),
|
||||||
|
width: 512,
|
||||||
|
height: 512,
|
||||||
|
},
|
||||||
|
description: SITE_DESCRIPTION,
|
||||||
|
foundingDate: "2026",
|
||||||
|
areaServed: "IN",
|
||||||
|
email: "hello@tia.baby",
|
||||||
|
contactPoint: {
|
||||||
|
"@type": "ContactPoint",
|
||||||
|
telephone: "+91-95548-81799",
|
||||||
|
contactType: "customer support",
|
||||||
|
email: "hello@tia.baby",
|
||||||
|
areaServed: "IN",
|
||||||
|
availableLanguage: ["English", "Hindi"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** WebSite — enables sitelinks/search treatment. */
|
||||||
|
export function websiteSchema() {
|
||||||
|
return {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "WebSite",
|
||||||
|
"@id": `${SITE_URL}/#website`,
|
||||||
|
name: SITE_NAME,
|
||||||
|
url: SITE_URL,
|
||||||
|
description: SITE_DESCRIPTION,
|
||||||
|
inLanguage: "en-IN",
|
||||||
|
publisher: { "@id": `${SITE_URL}/#organization` },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** SoftwareApplication — tells search engines this is an app, free to use. */
|
||||||
|
export function softwareApplicationSchema() {
|
||||||
|
return {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "SoftwareApplication",
|
||||||
|
name: `${SITE_NAME} — Baby Tracker & Digital Heirloom`,
|
||||||
|
operatingSystem: "Web, iOS, Android",
|
||||||
|
applicationCategory: "HealthApplication",
|
||||||
|
description: SITE_DESCRIPTION,
|
||||||
|
url: SITE_URL,
|
||||||
|
image: absoluteUrl("/icons/512.png"),
|
||||||
|
inLanguage: "en-IN",
|
||||||
|
offers: {
|
||||||
|
"@type": "Offer",
|
||||||
|
price: "0",
|
||||||
|
priceCurrency: "INR",
|
||||||
|
availability: "https://schema.org/InStock",
|
||||||
|
},
|
||||||
|
publisher: { "@id": `${SITE_URL}/#organization` },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** BreadcrumbList from an ordered list of { name, path }. */
|
||||||
|
export function breadcrumbSchema(items: { name: string; path: string }[]) {
|
||||||
|
return {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "BreadcrumbList",
|
||||||
|
itemListElement: items.map((item, i) => ({
|
||||||
|
"@type": "ListItem",
|
||||||
|
position: i + 1,
|
||||||
|
name: item.name,
|
||||||
|
item: absoluteUrl(item.path),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** BlogPosting / Article schema for a single post. */
|
||||||
|
export function blogPostingSchema(post: {
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
excerpt: string;
|
||||||
|
date: string;
|
||||||
|
author: string;
|
||||||
|
}) {
|
||||||
|
const url = absoluteUrl(`/blog/${post.slug}`);
|
||||||
|
return {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "BlogPosting",
|
||||||
|
"@id": `${url}#article`,
|
||||||
|
headline: post.title,
|
||||||
|
description: post.excerpt,
|
||||||
|
datePublished: post.date,
|
||||||
|
dateModified: post.date,
|
||||||
|
inLanguage: "en-IN",
|
||||||
|
image: absoluteUrl(`/blog/${post.slug}/opengraph-image`),
|
||||||
|
author: { "@type": "Organization", name: post.author, url: SITE_URL },
|
||||||
|
publisher: { "@id": `${SITE_URL}/#organization` },
|
||||||
|
mainEntityOfPage: { "@type": "WebPage", "@id": url },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the `dangerouslySetInnerHTML` payload for a JSON-LD <script>.
|
||||||
|
* Pass one schema object or an array. Set `type="application/ld+json"` on
|
||||||
|
* the <script> element itself.
|
||||||
|
*/
|
||||||
|
export function jsonLdScript(data: object | object[]): { __html: string } {
|
||||||
|
// JSON.stringify is XSS-safe here because all inputs are our own constants.
|
||||||
|
return { __html: JSON.stringify(data) };
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue