Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 110 additions & 2 deletions app/layout.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,113 @@
import { Analytics } from '@vercel/analytics/react';
import '../styles/main.css';

const siteUrl = process.env.NEXT_PUBLIC_SITE_URL
|| (process.env.VERCEL_PROJECT_PRODUCTION_URL ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` : 'https://dev-globe-viz.vercel.app');
const title = 'DevGlobe — Where Developers and AI Agents Connect';
const description = 'Discover developer identities, expertise, rankings, and connections worldwide on an interactive 3D globe.';
const githubUrl = 'https://github.com/sajeetharan/devglobe';

const structuredData = {
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'WebSite',
'@id': `${siteUrl}/#website`,
url: siteUrl,
name: 'DevGlobe',
alternateName: 'Dev Globe',
description,
inLanguage: 'en',
publisher: { '@id': `${siteUrl}/#organization` },
},
{
'@type': 'Organization',
'@id': `${siteUrl}/#organization`,
name: 'DevGlobe',
url: siteUrl,
logo: {
'@type': 'ImageObject',
url: `${siteUrl}/devglobe.png`,
},
sameAs: [githubUrl],
},
{
'@type': 'WebApplication',
'@id': `${siteUrl}/#application`,
name: 'DevGlobe',
url: siteUrl,
description,
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Any',
browserRequirements: 'Requires JavaScript and a WebGL-capable browser',
isAccessibleForFree: true,
offers: {
'@type': 'Offer',
price: 0,
priceCurrency: 'USD',
},
featureList: [
'Interactive global developer discovery',
'Developer rankings by open-source impact',
'Country and language leaderboards',
'GitHub and Stack Overflow contribution profiles',
'Semantic and hybrid developer search',
'Shareable developer identity cards',
],
about: [
{ '@type': 'Thing', name: 'Software developers' },
{ '@type': 'Thing', name: 'Open source software' },
{ '@type': 'Thing', name: 'AI coding agents' },
],
},
],
};

export const metadata = {
title: 'DevGlobe — Visualizing the World\'s Top Open-Source Contributors',
description: 'Interactive 3D globe visualization of top GitHub developers. 26,000+ developers ranked by stars, commits, repo reach & StackOverflow reputation.',
metadataBase: new URL(siteUrl),
applicationName: 'DevGlobe',
title,
description,
alternates: { canonical: '/' },
authors: [{ name: 'DevGlobe Contributors', url: githubUrl }],
creator: 'DevGlobe',
publisher: 'DevGlobe',
category: 'technology',
keywords: [
'developer network',
'open source developers',
'developer discovery',
'AI coding agents',
'GitHub developers',
'developer rankings',
'global developer community',
],
manifest: '/manifest.webmanifest',
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-image-preview': 'large',
'max-snippet': -1,
'max-video-preview': -1,
},
},
openGraph: {
title,
description,
url: '/',
siteName: 'DevGlobe',
type: 'website',
images: [{ url: '/opengraph-image', width: 1200, height: 630, alt: 'DevGlobe global developer network' }],
},
twitter: {
card: 'summary_large_image',
title,
description,
images: ['/opengraph-image'],
},
};

export default function RootLayout({ children }) {
Expand Down Expand Up @@ -33,6 +137,10 @@ export default function RootLayout({ children }) {
/>
</head>
<body>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData).replace(/</g, '\\u003c') }}
/>
{children}
<Analytics />
</body>
Expand Down
19 changes: 19 additions & 0 deletions app/manifest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export default function manifest() {
return {
name: 'DevGlobe — Where Developers and AI Agents Connect',
short_name: 'DevGlobe',
description: 'Discover developer identities, expertise, rankings, and connections worldwide.',
start_url: '/',
display: 'standalone',
background_color: '#080b10',
theme_color: '#080b10',
categories: ['developer tools', 'social', 'productivity'],
icons: [
{
src: '/devglobe.png',
sizes: 'any',
type: 'image/png',
},
],
};
}
73 changes: 73 additions & 0 deletions app/opengraph-image.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ImageResponse } from 'next/og';

export const runtime = 'nodejs';
export const alt = 'DevGlobe — Where Developers and AI Agents Connect';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';

export default function OpenGraphImage() {
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
background: 'linear-gradient(135deg, #071018 0%, #0f1d28 55%, #17130f 100%)',
color: '#f8fafc',
fontFamily: 'sans-serif',
padding: '72px 84px',
position: 'relative',
overflow: 'hidden',
}}
>
<div
style={{
position: 'absolute',
right: '-10px',
top: '48px',
width: '500px',
height: '500px',
display: 'flex',
border: '3px solid rgba(34, 211, 238, 0.34)',
borderRadius: '50%',
boxShadow: '0 0 90px rgba(34, 211, 238, 0.14)',
}}
>
<div style={{ position: 'absolute', left: '78px', top: '0', width: '340px', height: '494px', display: 'flex', border: '2px solid rgba(34, 211, 238, 0.2)', borderRadius: '50%' }} />
<div style={{ position: 'absolute', left: '0', top: '174px', width: '494px', height: '150px', display: 'flex', border: '2px solid rgba(34, 211, 238, 0.2)', borderRadius: '50%' }} />
{[
['92px', '110px', '#22d3ee'],
['320px', '90px', '#f59e0b'],
['250px', '250px', '#4ade80'],
['105px', '325px', '#60a5fa'],
['360px', '350px', '#fb7185'],
].map(([left, top, color]) => (
<div key={`${left}-${top}`} style={{ position: 'absolute', left, top, width: '16px', height: '16px', display: 'flex', borderRadius: '50%', background: color, boxShadow: `0 0 22px ${color}` }} />
))}
</div>

<div style={{ width: '650px', display: 'flex', flexDirection: 'column', position: 'relative' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '14px', marginBottom: '34px' }}>
<div style={{ width: '46px', height: '46px', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '2px solid #22d3ee', borderRadius: '50%', color: '#67e8f9', fontSize: '25px', fontWeight: '800' }}>D</div>
<div style={{ display: 'flex', fontSize: '28px', fontWeight: '800', letterSpacing: '1px' }}>DEV<span style={{ color: '#22d3ee' }}>GLOBE</span></div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', fontSize: '58px', fontWeight: '800', lineHeight: '1.05' }}>
<span>Where Developers</span>
<span style={{ display: 'flex', gap: '14px' }}><span>and AI Agents</span><span style={{ color: '#67e8f9' }}>Connect</span></span>
</div>
<div style={{ display: 'flex', marginTop: '28px', color: '#94a3b8', fontSize: '22px' }}>
Discover expertise, impact, and collaborators worldwide.
</div>
<div style={{ display: 'flex', gap: '28px', marginTop: '46px', color: '#cbd5e1', fontSize: '16px', fontWeight: '700' }}>
<span>26,000+ DEVELOPERS</span>
<span style={{ color: '#475569' }}>•</span>
<span>150+ COUNTRIES</span>
</div>
</div>
</div>
),
size
);
}
28 changes: 26 additions & 2 deletions app/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default function Home() {
const [claimedLogins, setClaimedLogins] = useState(new Set());
const [sidebarOpen, setSidebarOpen] = useState(false);
const [cardRequest, setCardRequest] = useState(0);
const [cardContext, setCardContext] = useState(null);
const globeRef = useRef(null);

useEffect(() => {
Expand Down Expand Up @@ -84,26 +85,44 @@ export default function Home() {
const result = await res.json();
setClaimStatus('claimed');
setClaimedLogins(prev => new Set(prev).add(user.login));
let claimedDeveloper = developers.find(developer => developer.login === user.login);
// If a new profile was created, reload developers to include it
if (result.created) {
const devRes = await fetch('/api/developers');
const devRes = await fetch('/api/developers', { cache: 'no-store' });
if (devRes.ok) {
const raw = await devRes.json();
const scored = addDeveloperRanks(scoreAll(raw));
setDevelopers(scored);
setFiltered(scored);
const claimed = new Set(raw.filter(d => d.claimed).map(d => d.login));
setClaimedLogins(claimed);
claimedDeveloper = scored.find(developer => developer.login === user.login);
}
} else if (claimedDeveloper) {
claimedDeveloper = { ...claimedDeveloper, claimed: true };
setDevelopers(current => current.map(developer => developer.login === user.login ? claimedDeveloper : developer));
setFiltered(current => current.map(developer => developer.login === user.login ? claimedDeveloper : developer));
}

claimedDeveloper ||= {
id: user.login,
login: user.login,
name: user.name || user.login,
avatarUrl: user.avatarUrl,
claimed: true,
};
setSelectedDev(claimedDeveloper);
setCardContext('claim');
setCardRequest(request => request + 1);
setSidebarOpen(false);
} else {
const data = await res.json();
console.error('Claim failed:', data.error);
}
} catch (err) {
console.error('Claim error:', err);
}
}, [user]);
}, [user, developers]);

const handleToggleTheme = useCallback(() => {
setTheme(prev => {
Expand Down Expand Up @@ -157,6 +176,7 @@ export default function Home() {
const handleGenerateCard = useCallback((developer) => {
const rankedDeveloper = developers.find(item => item.login === developer.login) || developer;
setSelectedDev(rankedDeveloper);
setCardContext('generate');
setCardRequest(request => request + 1);
setSidebarOpen(false);
if (rankedDeveloper.lat != null && rankedDeveloper.lng != null) {
Expand All @@ -170,6 +190,7 @@ export default function Home() {

const handleSelectDev = useCallback((dev) => {
setCardRequest(0);
setCardContext(null);
setSelectedDev(dev);
setSidebarOpen(false);
if (dev?.lat != null && dev?.lng != null) {
Expand All @@ -193,6 +214,7 @@ export default function Home() {
}, []);

const handleCloseDetail = useCallback(() => {
setCardContext(null);
setSelectedDev(null);
}, []);

Expand All @@ -211,6 +233,7 @@ export default function Home() {

const handleHome = useCallback(() => {
setCardRequest(0);
setCardContext(null);
setSelectedDev(null);
setCompareDevs([]);
setFiltered(developers);
Expand Down Expand Up @@ -289,6 +312,7 @@ export default function Home() {
onClose={handleCloseDetail}
claimedLogins={claimedLogins}
openCardOnMount={cardRequest > 0}
claimSuccess={cardContext === 'claim'}
/>
)}
{compareDevs.length === 2 && (
Expand Down
14 changes: 14 additions & 0 deletions app/robots.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL
|| (process.env.VERCEL_PROJECT_PRODUCTION_URL ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` : 'https://dev-globe-viz.vercel.app');

export default function robots() {
return {
rules: {
userAgent: '*',
allow: ['/', '/api/card'],
disallow: ['/api/auth/', '/api/developer', '/api/developers', '/api/search'],
},
sitemap: `${siteUrl}/sitemap.xml`,
host: siteUrl,
};
}
Loading
Loading