SEO
How to configure site identity, page metadata, JSON-LD structured data, OG images, robots, and sitemap.
1. Site Identity
All site-wide values are centralized in a single config file. Edit this to change your name, job title, description, and social links. Every page references this config.
1 // website/src/shared/config/site.config.ts 2 3 export const siteConfig = { 4 name: "Portfolio", 5 author: "Erik Yuntantyo", 6 email: "erik.yuntantyo@gmail.com", 7 jobTitle: "Software Engineer", 8 description: "Full-stack developer portfolio...", 9 url: "https://erikyuntantyo.com", 10 keywords: ["software engineer", "portfolio", "next.js", "react"], 11 social: { 12 github: "https://github.com/erikyuntantyo", 13 linkedin: "https://linkedin.com/in/erikyuntantyo", 14 twitter: "https://twitter.com/erikyuntantyo", 15 }, 16 };
2. Page Metadata
Every page must call generatePageMetadata() from @/server/seo/seo. This generates the title, description, openGraph, and twitter metadata automatically. No per-page manual meta tags needed.
1 // app/about/page.tsx 2 import { generatePageMetadata } from "@/server/seo/seo"; 3 4 export const metadata = generatePageMetadata({ 5 title: "About", 6 description: "Full-stack developer with experience in...", 7 path: "/about", 8 });
3. JSON-LD Structured Data
JSON-LD is generated per page type using helper functions. This tells search engines the type and structure of each page (website, person, blog, article, etc.).
1 import { 2 generateWebsiteJsonLd, 3 generatePersonJsonLd, 4 generateBlogJsonLd, 5 generateBlogPostJsonLd, 6 generateProfilePageJsonLd, 7 generateBreadcrumbJsonLd, 8 } from "@/server/seo/seo"; 9 10 // Root layout — WebSite + Person 11 <script type="application/ld+json"> 12 {JSON.stringify(generateWebsiteJsonLd())} 13 </script> 14 <script type="application/ld+json"> 15 {JSON.stringify(generatePersonJsonLd())} 16 </script> 17 18 // Blog listing page 19 generateBlogJsonLd(posts) 20 21 // Blog post page 22 generateBlogPostJsonLd({ title, description, url, datePublished }) 23 24 // About page 25 generateProfilePageJsonLd() 26 27 // Any page with breadcrumbs 28 generateBreadcrumbJsonLd([ 29 { name: "Home", url: "/" }, 30 { name: "Blog", url: "/blog" }, 31 { name: postTitle, url: `/blog/${slug}` }, 32 ])
4. OG & Twitter Images
Dynamic images are generated at build time using Next.js ImageResponse. They automatically include the site name, page title, and branding. No manual image creation needed.
1 // app/opengraph-image.tsx — generates OG image (1200x630) 2 // app/twitter-image.tsx — generates Twitter card image 3 4 // No runtime export needed — Node default (Next.js 16 native): 5 export const contentType = "image/png"; 6 export const size = { width: 1200, height: 630 };
5. Robots & Sitemap
Both are generated dynamically. robots.ts controls crawler access. sitemap.ts auto-includes all blog posts. The case study app uses noindex, nofollow with Disallow: / since it's a template catalog, not a public site.
1 // app/robots.ts — website (allow all) 2 export default function robots() { 3 return { 4 rules: { userAgent: "*", allow: "/" }, 5 sitemap: "https://erikyuntantyo.com/sitemap.xml", 6 }; 7 } 8 9 // app/sitemap.ts — auto-includes blog posts 10 export default async function sitemap() { 11 const posts = getAllPosts(); 12 return [ 13 { url: "https://erikyuntantyo.com", lastModified: new Date() }, 14 { url: "https://erikyuntantyo.com/about", lastModified: new Date() }, 15 { url: "https://erikyuntantyo.com/blog", lastModified: new Date() }, 16 ...posts.map((post) => ({ 17 url: `https://erikyuntantyo.com/blog/${post.slug}`, 18 lastModified: new Date(post.date), 19 })), 20 ]; 21 }
Note: The case study app is intentionally hidden from search engines. Its robots.ts uses Disallow: / and all pages have noindex, nofollow. Only the website is meant to be indexed.