name: seo-structured-data version: 3.1.0 description: > Implement production-grade SEO, structured data, Open Graph, and metadata for scardubu.dev. Use when: adding JSON-LD schemas, writing meta descriptions, configuring Open Graph images, generating sitemaps, managing robots.txt, auditing SEO, implementing canonical URLs, or setting up RSS feeds for the blog. Portfolio context: scardubu.dev targeting Google search for "ML engineer Lagos", "full-stack engineer Nigeria", and discovery via LinkedIn/Twitter/GitHub shares. Triggers: "SEO", "metadata", "JSON-LD", "structured data", "Open Graph", "sitemap", "canonical", "robots.txt", "OG image", "meta description", "/seo". Do NOT use for: component styling (use 03-frontend-design-SKILL.md), animation design (use 06-animation-SKILL.md), or API design. stack: Next.js 15 Metadata API · next-sitemap · schema.org · OpenGraph Protocol portfolio: scardubu.dev
For a portfolio targeting Stripe, Cloudflare, and Vercel engineering leads, SEO is not about ranking on "hire me" keyword queries. It is about:
- Looking credible when a recruiter Googles "Oscar Ndugbu"
- Rendering beautifully when shared on LinkedIn, Twitter, and WhatsApp
- Appearing in search results for "TaxBridge Lagos fintech" and "SabiScore ML"
- Passing structured data validation so rich results appear in Google
Every page is a recruiting tool. Metadata is its packaging.
PHASE 1 — NEXT.JS 15 METADATA ARCHITECTURE
1.1 Root Layout Metadata (Base Layer)
// src/app/layout.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
// Title template — child pages fill the %s slot
title: {
template: '%s — Oscar Ndugbu',
default: 'Oscar Ndugbu — Staff Full-Stack ML Engineer, Lagos',
},
description:
'Staff Full-Stack ML Engineer building fintech infrastructure in Lagos. ' +
'Creator of TaxBridge, SabiScore, and Hashablanca. ' +
'Open to Staff/Principal roles at Stripe, Cloudflare, Coinbase.',
// Canonical + robots
metadataBase: new URL('https://scardubu.dev'),
alternates: { canonical: '/' },
robots: {
index: true,
follow: true,
googleBot: { index: true, follow: true, 'max-image-preview': 'large' },
},
// Open Graph — base layer (page-level overrides cascade)
openGraph: {
type: 'website',
url: 'https://scardubu.dev',
siteName: 'Oscar Ndugbu — CONVICTION ENGINE',
title: 'Oscar Ndugbu — Staff Full-Stack ML Engineer',
description:
'Building fintech infrastructure that Nigerian SMEs actually use. ' +
'TaxBridge · SabiScore · Hashablanca.',
images: [
{
url: '/og/default.png', // 1200×630, generated by Edge Route
width: 1200,
height: 630,
alt: 'Oscar Ndugbu — Staff Full-Stack ML Engineer — scardubu.dev',
},
],
},
// Twitter / X Card
twitter: {
card: 'summary_large_image',
site: '@Scardubu',
creator: '@Scardubu',
title: 'Oscar Ndugbu — Staff Full-Stack ML Engineer',
description:
'Building fintech infrastructure in Lagos. TaxBridge · SabiScore · Hashablanca.',
images: ['/og/default.png'],
},
// Authors + verification
authors: [{ name: 'Oscar Ndugbu', url: 'https://scardubu.dev' }],
creator: 'Oscar Ndugbu',
publisher: 'Oscar Ndugbu',
verification: {
google: 'YOUR_GOOGLE_SEARCH_CONSOLE_VERIFICATION_TOKEN',
},
// App-specific meta
applicationName: 'CONVICTION ENGINE',
keywords: [
'full-stack engineer Lagos',
'ML engineer Nigeria',
'fintech engineer Africa',
'TaxBridge',
'SabiScore',
'Next.js engineer',
'TypeScript engineer',
'Oscar Ndugbu',
'Scardubu',
],
};
1.2 Page-Level Metadata (Cascades Over Base)
// src/app/projects/taxbridge/page.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'TaxBridge — Tax Filing Infrastructure for Lagos SMEs',
description:
'TaxBridge reduces VAT/CIT/PIT filing from 4 hours to 15 minutes for Nigerian SMEs. ' +
'Built with Fastify 5, PostgreSQL 15 RLS, BullMQ, and Paystack/Remita integrations.',
alternates: { canonical: '/projects/taxbridge' },
openGraph: {
title: 'TaxBridge — Tax Filing in 15 Minutes',
description:
'How I built tax infrastructure that Lagos SMEs actually use. ' +
'4h → 15min. Paystack + FIRS + Remita + Youverify.',
images: [
{
url: '/og/projects/taxbridge.png',
width: 1200,
height: 630,
alt: 'TaxBridge — Nigerian SME tax filing platform by Oscar Ndugbu',
},
],
type: 'article',
},
};
1.3 Dynamic OG Image Generation (Edge Route)
// src/app/og/projects/[slug]/route.tsx
// Generates 1200×630 OG images dynamically at the edge
import { ImageResponse } from 'next/og';
import { type NextRequest } from 'next/server';
export const runtime = 'edge';
const projects: Record<string, { title: string; metric: string; stack: string }> = {
taxbridge: {
title: 'TaxBridge',
metric: '4h → 15min filing',
stack: 'Fastify 5 · PostgreSQL · BullMQ',
},
sabiscore: {
title: 'SabiScore',
metric: 'ML credit scoring',
stack: 'XGBoost · LightGBM · CatBoost',
},
hashablanca: {
title: 'Hashablanca',
metric: 'Privacy as primitive',
stack: 'ZK-SNARKs · Multi-chain',
},
};
export async function GET(
request: NextRequest,
{ params }: { params: { slug: string } }
) {
const project = projects[params.slug];
if (!project) return new Response('Not found', { status: 404 });
return new ImageResponse(
(
<div
style={{
width: '1200px',
height: '630px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
padding: '64px',
background: '#0a0a0a',
fontFamily: 'monospace',
}}
>
{/* Top: site attribution */}
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#737373', fontSize: '18px' }}>scardubu.dev</span>
<span style={{ color: '#737373', fontSize: '18px' }}>@Scardubu</span>
</div>
{/* Center: project info */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<span style={{ color: '#f97316', fontSize: '22px' }}>{project.metric}</span>
<span style={{ color: '#f5f5f5', fontSize: '72px', fontWeight: 900, lineHeight: 1 }}>
{project.title}
</span>
<span style={{ color: '#737373', fontSize: '22px' }}>{project.stack}</span>
</div>
{/* Bottom: author */}
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div
style={{
width: '48px', height: '48px', borderRadius: '50%',
background: '#f97316', display: 'flex',
alignItems: 'center', justifyContent: 'center',
color: '#0a0a0a', fontWeight: 900, fontSize: '20px',
}}
>
ON
</div>
<span style={{ color: '#f5f5f5', fontSize: '20px' }}>Oscar Ndugbu</span>
</div>
</div>
),
{ width: 1200, height: 630 }
);
}
PHASE 2 — JSON-LD STRUCTURED DATA
2.1 Person Schema (Homepage — Always Present)
// src/components/seo/PersonSchema.tsx
// Renders as <script type="application/ld+json"> in document head
export function PersonSchema() {
const schema = {
'@context': 'https://schema.org',
'@type': 'Person',
name: 'Oscar Ndugbu',
alternateName: 'Scardubu',
url: 'https://scardubu.dev',
image: 'https://scardubu.dev/images/oscar-ndugbu.jpg',
jobTitle: 'Staff Full-Stack ML Engineer',
worksFor: {
'@type': 'Organization',
name: 'UBEC',
url: 'https://ubec.gov.ng',
},
address: {
'@type': 'PostalAddress',
addressLocality: 'Lagos',
addressCountry: 'NG',
},
sameAs: [
'https://github.com/Scardubu',
'https://linkedin.com/in/oscar-ndugbu',
'https://twitter.com/Scardubu',
],
knowsAbout: [
'TypeScript', 'Next.js', 'Machine Learning', 'Fintech Infrastructure',
'PostgreSQL', 'Distributed Systems', 'ZK-SNARKs', 'React',
],
description:
'Staff Full-Stack ML Engineer building fintech infrastructure in Lagos, Nigeria. ' +
'Creator of TaxBridge, SabiScore, and Hashablanca.',
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema, null, 0) }}
/>
);
}
2.2 CreativeWork Schema (Per Project Page)
// src/components/seo/ProjectSchema.tsx
interface ProjectSchemaProps {
name: string;
description: string;
url: string;
repoUrl: string;
dateCreated: string; // ISO 8601
technologies: string[];
screenshot?: string;
}
export function ProjectSchema({
name, description, url, repoUrl, dateCreated, technologies, screenshot,
}: ProjectSchemaProps) {
const schema = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name,
description,
url,
codeRepository: repoUrl,
dateCreated,
author: {
'@type': 'Person',
name: 'Oscar Ndugbu',
url: 'https://scardubu.dev',
},
applicationCategory: 'WebApplication',
operatingSystem: 'Any',
programmingLanguage: technologies,
...(screenshot && {
screenshot: { '@type': 'ImageObject', url: screenshot, width: 1200, height: 800 },
}),
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema, null, 0) }}
/>
);
}
2.3 BlogPosting Schema (Blog Posts)
// Usage in src/app/blog/[slug]/page.tsx
const blogSchema = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
description: post.excerpt,
datePublished: post.publishedAt,
dateModified: post.updatedAt ?? post.publishedAt,
author: {
'@type': 'Person',
name: 'Oscar Ndugbu',
url: 'https://scardubu.dev',
},
publisher: {
'@type': 'Person',
name: 'Oscar Ndugbu',
url: 'https://scardubu.dev',
},
url: `https://scardubu.dev/blog/${post.slug}`,
image: `https://scardubu.dev/og/blog/${post.slug}.png`,
mainEntityOfPage: { '@type': 'WebPage', '@id': `https://scardubu.dev/blog/${post.slug}` },
keywords: post.tags.join(', '),
};
PHASE 3 — SITEMAP & ROBOTS
3.1 next-sitemap Configuration
// next-sitemap.config.js
/** @type {import('next-sitemap').IConfig} */
module.exports = {
siteUrl: 'https://scardubu.dev',
generateRobotsTxt: true,
sitemapSize: 5000,
changefreq: 'weekly',
priority: 0.7,
// Custom priorities by section
transform: async (config, path) => {
const priorities = {
'/': 1.0,
'/projects': 0.9,
'/projects/taxbridge': 0.95,
'/projects/sabiscore': 0.95,
'/projects/hashablanca': 0.95,
'/blog': 0.8,
'/about': 0.7,
'/open-source': 0.7,
};
return {
loc: path,
changefreq: path === '/' ? 'daily' : 'weekly',
priority: priorities[path] ?? 0.6,
lastmod: new Date().toISOString(),
};
},
robotsTxtOptions: {
policies: [
{ userAgent: '*', allow: '/' },
{ userAgent: '*', disallow: ['/api/', '/_next/'] },
],
additionalSitemaps: ['https://scardubu.dev/blog-sitemap.xml'],
},
// Exclude private/internal paths
exclude: ['/api/*', '/admin/*', '/404', '/500'],
};
3.2 PowerShell Commands for SEO Workflow
# Generate sitemap + robots.txt
pnpm next-sitemap
# Validate structured data (requires internet connection)
# Open: https://validator.schema.org/ and paste the JSON-LD
# Check Open Graph rendering
# Open: https://www.opengraph.xyz/?url=https://scardubu.dev
# Lighthouse SEO audit
pnpm lhci autorun --collect.url=https://scardubu.dev
# Check meta tags in CLI
Invoke-WebRequest -Uri "https://scardubu.dev" -UseBasicParsing |
Select-Object -ExpandProperty Content |
Select-String -Pattern '<meta'
PHASE 4 — QUALITY GATE
Metadata (every page):
□ <title> present — unique per page, ≤ 60 chars, includes name or site name
□ <meta name="description"> — ≤ 160 chars, compelling, unique per page
□ Canonical URL — no trailing slash inconsistency
□ robots: index, follow (or noindex for /api/ routes)
□ No duplicate title tags (check via site:scardubu.dev in Google)
Open Graph (every page):
□ og:title — present, ≤ 60 chars
□ og:description — present, ≤ 155 chars
□ og:image — present, 1200×630, accessible URL (not localhost)
□ og:image:alt — descriptive text for screen readers
□ og:type — website / article as appropriate
□ og:url — canonical URL (not with query params)
Twitter Card:
□ twitter:card = summary_large_image
□ twitter:site = @Scardubu
□ twitter:image — same or separate from OG image
JSON-LD (by page type):
□ Homepage: Person schema (validated at schema.org/validator)
□ Project pages: SoftwareApplication schema
□ Blog posts: BlogPosting schema
□ No errors in structured data validator
Sitemap & robots:
□ sitemap.xml present at /sitemap.xml
□ robots.txt present at /robots.txt
□ /api/* routes disallowed in robots.txt
□ Sitemap referenced in robots.txt
Performance (SEO-adjacent):
□ LCP ≤ 2.5s (Lighthouse — affects Core Web Vitals ranking signal)
□ No mobile usability issues (text too small, tap targets too close)
□ HTTPS only (no mixed content)
□ No broken links (crawl with tool)
SEO for a developer portfolio is not about tricking Google. It is about ensuring that when a Stripe recruiter searches "Oscar Ndugbu", they see exactly what you want them to see — structured, credible, and fast. The technical foundation makes the first impression.