Skip to content

Commit b8871b7

Browse files
seo+geo: per-user metadata, WebSite SearchAction, BreadcrumbList, expanded sitemap
Now that the site is in Google Search Console, the biggest gaps are closed in one pass. /user/[username] (the largest indexable surface) was a pure client component with zero metadata. Every graveyard URL was sharing the homepage title in Google. Split into: - page.tsx: server component with generateMetadata, sanitizes the username and emits per-user title, description, canonical, OG. - UserPageContent.tsx: the original client UI, untouched logic. Root layout JSON-LD expanded into a graph with three linked entities: - WebSite: adds SearchAction so Google can render a sitelinks search box pointing at /?repo={query}. Free traffic capture for any repo-style search. - WebApplication: kept, now uses @id refs to share creator/publisher. - Organization: gains sameAs with the public social handles so Google can verify Dot Systems identity across X / IG / GitHub. Each subpage (about, faq, legal, pricing) now emits a BreadcrumbList JSON-LD via a tiny shared BreadcrumbJsonLd component. Helps Google build a richer SERP path. Sitemap gains six sample /user/* entries (dotsystemsdevs plus famous casualty orgs like atom, angular, apache, facebookarchive, YahooArchive) so crawlers discover the dynamic route shape and we don't rely on SearchConsole to guess that /user/X exists. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent d3c7085 commit b8871b7

9 files changed

Lines changed: 194 additions & 58 deletions

File tree

src/app/about/page.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Metadata } from 'next'
22
import AboutContent from './AboutContent'
3+
import BreadcrumbJsonLd from '@/components/BreadcrumbJsonLd'
34

45
export const metadata: Metadata = {
56
title: 'About · Commitment Issues · How It Works',
@@ -13,5 +14,10 @@ export const metadata: Metadata = {
1314
}
1415

1516
export default function AboutPage() {
16-
return <AboutContent />
17+
return (
18+
<>
19+
<BreadcrumbJsonLd trail={[{ name: 'About', path: '/about' }]} />
20+
<AboutContent />
21+
</>
22+
)
1723
}

src/app/faq/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Metadata } from 'next'
22
import FAQContent from './FAQContent'
3+
import BreadcrumbJsonLd from '@/components/BreadcrumbJsonLd'
34

45
export const metadata: Metadata = {
56
title: 'FAQ · Commitment Issues',
@@ -85,6 +86,7 @@ function FAQJsonLd() {
8586
export default function FAQPage() {
8687
return (
8788
<>
89+
<BreadcrumbJsonLd trail={[{ name: 'FAQ', path: '/faq' }]} />
8890
<FAQJsonLd />
8991
<FAQContent items={FAQ} />
9092
</>

src/app/layout.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,25 @@ export const metadata: Metadata = {
6767
const jsonLd = {
6868
'@context': 'https://schema.org',
6969
'@graph': [
70+
{
71+
'@type': 'WebSite',
72+
'@id': 'https://commitmentissues.dev/#website',
73+
url: 'https://commitmentissues.dev',
74+
name: 'Commitment Issues',
75+
description: 'Death certificate generator for abandoned GitHub repositories.',
76+
publisher: { '@id': 'https://commitmentissues.dev/#org' },
77+
potentialAction: {
78+
'@type': 'SearchAction',
79+
target: {
80+
'@type': 'EntryPoint',
81+
urlTemplate: 'https://commitmentissues.dev/?repo={search_term_string}',
82+
},
83+
'query-input': 'required name=search_term_string',
84+
},
85+
},
7086
{
7187
'@type': 'WebApplication',
88+
'@id': 'https://commitmentissues.dev/#app',
7289
name: 'Commitment Issues',
7390
alternateName: 'commitmentissues.dev',
7491
description: 'Free tool that generates official death certificates for abandoned GitHub repos. Paste any public repo URL to see the cause of death, last commit as last words, and full repo stats.',
@@ -83,12 +100,18 @@ const jsonLd = {
83100
'README badge embed',
84101
],
85102
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
86-
creator: { '@type': 'Organization', name: 'Dot Systems', url: 'https://github.com/dotsystemsdevs' },
103+
creator: { '@id': 'https://commitmentissues.dev/#org' },
87104
},
88105
{
89106
'@type': 'Organization',
107+
'@id': 'https://commitmentissues.dev/#org',
90108
name: 'Dot Systems',
91109
url: 'https://github.com/dotsystemsdevs',
110+
sameAs: [
111+
'https://github.com/dotsystemsdevs',
112+
'https://x.com/Dotsystemsdevs',
113+
'https://www.instagram.com/dotsystemsdevs/',
114+
],
92115
},
93116
],
94117
}

src/app/legal/page.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Metadata } from 'next'
22
import LegalContent from './legalContent'
3+
import BreadcrumbJsonLd from '@/components/BreadcrumbJsonLd'
34

45
export const metadata: Metadata = {
56
title: 'Legal · Commitment Issues',
@@ -13,6 +14,11 @@ export const metadata: Metadata = {
1314
}
1415

1516
export default function LegalPage() {
16-
return <LegalContent />
17+
return (
18+
<>
19+
<BreadcrumbJsonLd trail={[{ name: 'Legal', path: '/legal' }]} />
20+
<LegalContent />
21+
</>
22+
)
1723
}
1824

src/app/pricing/page.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Metadata } from 'next'
22
import PricingContent from './PricingContent'
3+
import BreadcrumbJsonLd from '@/components/BreadcrumbJsonLd'
34

45
export const metadata: Metadata = {
56
title: 'Pricing · Commitment Issues · Death Is Free',
@@ -13,5 +14,10 @@ export const metadata: Metadata = {
1314
}
1415

1516
export default function PricingPage() {
16-
return <PricingContent />
17+
return (
18+
<>
19+
<BreadcrumbJsonLd trail={[{ name: 'Pricing', path: '/pricing' }]} />
20+
<PricingContent />
21+
</>
22+
)
1723
}

src/app/sitemap.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@ import { MetadataRoute } from 'next'
22

33
const BASE_URL = 'https://commitmentissues.dev'
44

5+
// Sample user-graveyard URLs to expose the dynamic /user/[name] route shape
6+
// to crawlers. These pages have their own metadata (per-user title/description).
7+
const SAMPLE_USERS = [
8+
'dotsystemsdevs',
9+
'atom',
10+
'angular',
11+
'apache',
12+
'facebookarchive',
13+
'YahooArchive',
14+
]
15+
516
export default function sitemap(): MetadataRoute.Sitemap {
617
const now = new Date()
718
return [
@@ -10,5 +21,11 @@ export default function sitemap(): MetadataRoute.Sitemap {
1021
{ url: `${BASE_URL}/pricing`, lastModified: now, changeFrequency: 'monthly', priority: 0.7 },
1122
{ url: `${BASE_URL}/about`, lastModified: now, changeFrequency: 'monthly', priority: 0.6 },
1223
{ url: `${BASE_URL}/legal`, lastModified: now, changeFrequency: 'yearly', priority: 0.3 },
24+
...SAMPLE_USERS.map((u) => ({
25+
url: `${BASE_URL}/user/${u}`,
26+
lastModified: now,
27+
changeFrequency: 'weekly' as const,
28+
priority: 0.5,
29+
})),
1330
]
1431
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
'use client'
2+
3+
import { useEffect, useState } from 'react'
4+
import { useParams, useRouter } from 'next/navigation'
5+
import type { UserRepoSummary } from '@/lib/types'
6+
import UserDashboard from '@/components/UserDashboard'
7+
import LoadingState from '@/components/LoadingState'
8+
import SubpageShell from '@/components/SubpageShell'
9+
10+
const MONO = `var(--font-courier), system-ui, sans-serif`
11+
12+
export default function UserPageContent() {
13+
const params = useParams()
14+
const router = useRouter()
15+
const username = typeof params.username === 'string' ? params.username : ''
16+
17+
const [repos, setRepos] = useState<UserRepoSummary[] | null>(null)
18+
const [error, setError] = useState<string | null>(null)
19+
const [loading, setLoading] = useState(true)
20+
21+
useEffect(() => {
22+
if (!username) return
23+
setLoading(true)
24+
setError(null)
25+
fetch(`/api/user?username=${encodeURIComponent(username)}`)
26+
.then(r => r.json())
27+
.then((d: { repos?: UserRepoSummary[]; error?: string }) => {
28+
if (d.error) { setError(d.error); return }
29+
setRepos(d.repos ?? [])
30+
})
31+
.catch(() => setError('Something went wrong. Try again.'))
32+
.finally(() => setLoading(false))
33+
}, [username])
34+
35+
return (
36+
<SubpageShell
37+
title={undefined}
38+
subtitle={null}
39+
microcopy={null}
40+
hideHero
41+
>
42+
{loading && <LoadingState />}
43+
44+
{error && (
45+
<div style={{ textAlign: 'center', padding: '40px 0' }}>
46+
<p style={{ fontFamily: MONO, fontSize: '13px', color: 'var(--c-red)', marginBottom: '20px' }}>{error}</p>
47+
<button
48+
onClick={() => router.push('/')}
49+
style={{ fontFamily: MONO, fontSize: '13px', fontWeight: 700, background: 'none', border: 'none', textDecoration: 'underline', textUnderlineOffset: '3px', color: 'var(--c-ink)', cursor: 'pointer', minHeight: '44px', padding: '10px 0' }}
50+
>
51+
← examine another subject
52+
</button>
53+
</div>
54+
)}
55+
56+
{repos && !loading && <UserDashboard repos={repos} username={username} />}
57+
</SubpageShell>
58+
)
59+
}

src/app/user/[username]/page.tsx

Lines changed: 46 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,51 @@
1-
'use client'
1+
import type { Metadata } from 'next'
2+
import UserPageContent from './UserPageContent'
23

3-
import { useEffect, useState } from 'react'
4-
import { useParams, useRouter } from 'next/navigation'
5-
import type { UserRepoSummary } from '@/lib/types'
6-
import UserDashboard from '@/components/UserDashboard'
7-
import LoadingState from '@/components/LoadingState'
8-
import SubpageShell from '@/components/SubpageShell'
4+
const VALID_USERNAME = /^[a-zA-Z0-9_.-]+$/
95

10-
const MONO = `var(--font-courier), system-ui, sans-serif`
11-
12-
export default function UserPage() {
13-
const params = useParams()
14-
const router = useRouter()
15-
const username = typeof params.username === 'string' ? params.username : ''
16-
17-
const [repos, setRepos] = useState<UserRepoSummary[] | null>(null)
18-
const [error, setError] = useState<string | null>(null)
19-
const [loading, setLoading] = useState(true)
20-
21-
useEffect(() => {
22-
if (!username) return
23-
setLoading(true)
24-
setError(null)
25-
fetch(`/api/user?username=${encodeURIComponent(username)}`)
26-
.then(r => r.json())
27-
.then((d: { repos?: UserRepoSummary[]; error?: string }) => {
28-
if (d.error) { setError(d.error); return }
29-
setRepos(d.repos ?? [])
30-
})
31-
.catch(() => setError('Something went wrong. Try again.'))
32-
.finally(() => setLoading(false))
33-
}, [username])
34-
35-
return (
36-
<SubpageShell
37-
title={undefined}
38-
subtitle={null}
39-
microcopy={null}
40-
hideHero
41-
>
42-
{loading && <LoadingState />}
6+
function sanitizeUsername(raw: string): string {
7+
const trimmed = raw.slice(0, 39)
8+
return VALID_USERNAME.test(trimmed) ? trimmed : ''
9+
}
4310

44-
{error && (
45-
<div style={{ textAlign: 'center', padding: '40px 0' }}>
46-
<p style={{ fontFamily: MONO, fontSize: '13px', color: '#8B0000', marginBottom: '20px' }}>{error}</p>
47-
<button
48-
onClick={() => router.push('/')}
49-
style={{ fontFamily: MONO, fontSize: '13px', fontWeight: 700, background: 'none', border: 'none', textDecoration: 'underline', textUnderlineOffset: '3px', color: '#160A06', cursor: 'pointer', minHeight: '44px', padding: '10px 0' }}
50-
>
51-
← examine another subject
52-
</button>
53-
</div>
54-
)}
11+
export async function generateMetadata(
12+
{ params }: { params: Promise<{ username: string }> }
13+
): Promise<Metadata> {
14+
const { username } = await params
15+
const safe = sanitizeUsername(username)
16+
17+
if (!safe) {
18+
return {
19+
title: 'Graveyard · Commitment Issues',
20+
description: 'Browse the GitHub graveyard for any developer on Commitment Issues.',
21+
robots: { index: false, follow: true },
22+
}
23+
}
24+
25+
const title = `@${safe}'s GitHub Graveyard · Commitment Issues`
26+
const description = `See how many of @${safe}'s GitHub repositories are alive, on life support, or deceased. Free abandonment analysis with death certificates for any developer's projects.`
27+
const canonical = `https://commitmentissues.dev/user/${safe}`
28+
29+
return {
30+
title,
31+
description,
32+
alternates: { canonical },
33+
openGraph: {
34+
title,
35+
description,
36+
url: canonical,
37+
type: 'profile',
38+
images: [{ url: '/opengraph-image', width: 1200, height: 630 }],
39+
},
40+
twitter: {
41+
card: 'summary_large_image',
42+
title,
43+
description,
44+
images: ['/opengraph-image'],
45+
},
46+
}
47+
}
5548

56-
{repos && !loading && <UserDashboard repos={repos} username={username} />}
57-
</SubpageShell>
58-
)
49+
export default function UserPage() {
50+
return <UserPageContent />
5951
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const BASE = 'https://commitmentissues.dev'
2+
3+
type Crumb = { name: string; path: string }
4+
5+
export default function BreadcrumbJsonLd({ trail }: { trail: Crumb[] }) {
6+
const data = {
7+
'@context': 'https://schema.org',
8+
'@type': 'BreadcrumbList',
9+
itemListElement: [
10+
{ '@type': 'ListItem', position: 1, name: 'Home', item: BASE },
11+
...trail.map((c, i) => ({
12+
'@type': 'ListItem',
13+
position: i + 2,
14+
name: c.name,
15+
item: `${BASE}${c.path}`,
16+
})),
17+
],
18+
}
19+
return (
20+
<script
21+
type="application/ld+json"
22+
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
23+
/>
24+
)
25+
}

0 commit comments

Comments
 (0)