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
414 changes: 414 additions & 0 deletions SEO_CHECKLIST.md

Large diffs are not rendered by default.

588 changes: 588 additions & 0 deletions SEO_IMPLEMENTATION_REPORT.md

Large diffs are not rendered by default.

639 changes: 639 additions & 0 deletions SEO_SEMANTIC_HTML_GUIDE.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
"react-dom": "^19.0.0",
"react-i18next": "16.5.1",
"react-router-dom": "^7.0.0",
"tailwind-merge": "^2.6.0"
"tailwind-merge": "^2.6.0",
"web-vitals": "5.1.0"
},
"devDependencies": {
"@biomejs/biome": "2.3.10",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 41 additions & 0 deletions public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# DevCard - GitHub Readme Stats Generator
# robots.txt

# Allow all bots to crawl and index the site
User-agent: *
Allow: /
Allow: /stats
Allow: /languages
Allow: /pin

# Disallow crawling of private paths (if any)
# Disallow: /admin

# Crawl delay to be respectful to server resources
Crawl-delay: 1

# Sitemaps
Sitemap: https://devcard.pavegy.workers.dev/sitemap.xml

# Allow specific search engine bots with different crawl delays
User-agent: Googlebot
Allow: /
Crawl-delay: 0.5

User-agent: Bingbot
Allow: /
Crawl-delay: 1

# Block aggressive crawlers
User-agent: AhrefsBot
Disallow: /

User-agent: SemrushBot
Disallow: /

User-agent: DotBot
Disallow: /

# Rule for other search engines
User-agent: *
Allow: /
62 changes: 62 additions & 0 deletions public/sitemap.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0">

<!-- Home Page -->
<url>
<loc>https://devcard.pavegy.workers.dev/</loc>
<lastmod>2026-01-02</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
<image:image>
<image:loc>https://devcard.pavegy.workers.dev/og-image.png</image:loc>
<image:title>DevCard - GitHub Readme Stats Generator</image:title>
</image:image>
</url>

<!-- Stats Generator Page -->
<url>
<loc>https://devcard.pavegy.workers.dev/stats</loc>
<lastmod>2026-01-02</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
<image:image>
<image:loc>https://devcard.pavegy.workers.dev/stats-preview.png</image:loc>
<image:title>GitHub Stats Generator</image:title>
</image:image>
</url>

<!-- Languages Generator Page -->
<url>
<loc>https://devcard.pavegy.workers.dev/languages</loc>
<lastmod>2026-01-02</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
<image:image>
<image:loc>https://devcard.pavegy.workers.dev/languages-preview.png</image:loc>
<image:title>Programming Languages Card Generator</image:title>
</image:image>
</url>

<!-- Pin Generator Page -->
<url>
<loc>https://devcard.pavegy.workers.dev/pin</loc>
<lastmod>2026-01-02</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
<image:image>
<image:loc>https://devcard.pavegy.workers.dev/pin-preview.png</image:loc>
<image:title>Pinned Repository Card Generator</image:title>
</image:image>
</url>

<!-- Privacy Policy Page -->
<url>
<loc>https://devcard.pavegy.workers.dev/privacy</loc>
<lastmod>2026-01-02</lastmod>
<changefreq>monthly</changefreq>
<priority>0.3</priority>
</url>

</urlset>
66 changes: 66 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createRepoCard } from '../cards/repo';
import { createStatsCard } from '../cards/stats';
import { fetchRepo, fetchTopLanguages, fetchUserStats } from '../fetchers/github';
import type { Env, LanguagesCardOptions, RepoCardOptions, StatsCardOptions } from '../types';
import { AnalyticsCollector } from '../utils/analytics';
import { CACHE_TTL_EXPORT, CacheManager, getCacheHeaders } from '../utils/cache';

const api = new Hono<{ Bindings: Env }>();
Expand Down Expand Up @@ -168,4 +169,69 @@ api.get('/pin', async (c) => {
}
});

// Analytics endpoints for frontend observability
api.post('/analytics/vitals', async (c) => {
try {
const data = await c.req.json();
const analytics = new AnalyticsCollector(c.env);

await analytics.recordWebVital({
name: data.name,
value: data.value,
rating: data.rating,
page: data.page,
timestamp: data.timestamp,
userAgent: c.req.header('User-Agent'),
country: c.req.header('CF-IPCountry'),
});

return c.json({ success: true }, 200);
} catch (error) {
console.error('Failed to record web vital:', error);
return c.json({ success: false }, 500);
}
});

api.post('/analytics/error', async (c) => {
try {
const data = await c.req.json();
const analytics = new AnalyticsCollector(c.env);

await analytics.recordError({
message: data.message,
stack: data.stack,
componentStack: data.componentStack,
page: data.page,
userAgent: data.userAgent,
timestamp: data.timestamp,
ip: c.req.header('CF-Connecting-IP'),
country: c.req.header('CF-IPCountry'),
});

return c.json({ success: true }, 200);
} catch (error) {
console.error('Failed to record error:', error);
return c.json({ success: false }, 500);
}
});

api.post('/analytics/custom', async (c) => {
try {
const data = await c.req.json();
const analytics = new AnalyticsCollector(c.env);

await analytics.recordCustomMetric({
name: data.name,
value: data.value,
metadata: data.metadata,
timestamp: data.timestamp,
});

return c.json({ success: true }, 200);
} catch (error) {
console.error('Failed to record custom metric:', error);
return c.json({ success: false }, 500);
}
});

export { api };
9 changes: 9 additions & 0 deletions src/client/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const LanguagesGenerator = lazy(() =>
const PinGenerator = lazy(() =>
import('./pages/PinGenerator').then((m) => ({ default: m.PinGenerator }))
);
const Privacy = lazy(() => import('./pages/Privacy').then((m) => ({ default: m.Privacy })));

function App() {
return (
Expand Down Expand Up @@ -51,6 +52,14 @@ function App() {
</Suspense>
}
/>
<Route
path="/privacy"
element={
<Suspense fallback={<PageLoader />}>
<Privacy />
</Suspense>
}
/>
</Route>
</Routes>
);
Expand Down
99 changes: 99 additions & 0 deletions src/client/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { AlertTriangle, RefreshCw } from 'lucide-react';
import { Component, type ErrorInfo, type ReactNode } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';

interface Props {
children: ReactNode;
fallback?: ReactNode;
}

interface State {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
}

/**
* Error Boundary component to catch and handle React errors
* Reports errors to analytics in production
*/
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}

static getDerivedStateFromError(error: Error): Partial<State> {
return { hasError: true, error };
}

componentDidCatch(error: Error, errorInfo: ErrorInfo) {
this.setState({ errorInfo });

// Log error in development
if (import.meta.env.DEV) {
console.error('[ErrorBoundary] Caught error:', error, errorInfo);
}

// Report error to analytics in production
if (import.meta.env.PROD && 'sendBeacon' in navigator) {
const errorData = {
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
page: window.location.pathname,
userAgent: navigator.userAgent,
timestamp: Date.now(),
};

navigator.sendBeacon('/api/analytics/error', JSON.stringify(errorData));
}
}

handleReset = () => {
this.setState({ hasError: false, error: null, errorInfo: null });
window.location.reload();
};

render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}

return (
<div className="flex min-h-[400px] items-center justify-center p-6">
<Card className="max-w-md border-destructive/50 bg-destructive/5">
<CardHeader className="text-center">
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-destructive/10">
<AlertTriangle className="h-6 w-6 text-destructive" />
</div>
<CardTitle className="text-destructive">Something went wrong</CardTitle>
<CardDescription>
An unexpected error occurred. Please try refreshing the page.
</CardDescription>
</CardHeader>
<CardContent className="text-center">
{import.meta.env.DEV && this.state.error && (
<details className="mb-4 rounded-lg bg-muted p-3 text-left text-xs">
<summary className="cursor-pointer font-medium">Error Details</summary>
<pre className="mt-2 overflow-auto whitespace-pre-wrap text-destructive">
{this.state.error.message}
{this.state.error.stack && `\n\n${this.state.error.stack}`}
</pre>
</details>
)}
<Button onClick={this.handleReset} className="gap-2">
<RefreshCw className="h-4 w-4" />
Refresh Page
</Button>
</CardContent>
</Card>
</div>
);
}

return this.props.children;
}
}
32 changes: 21 additions & 11 deletions src/client/components/layout/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Github, Heart } from 'lucide-react';
import { Github, Heart, Shield } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';

export function Footer() {
const { t } = useTranslation();
Expand All @@ -10,7 +11,7 @@ export function Footer() {
<div className="flex flex-col items-center justify-between gap-4 sm:flex-row">
<p className="flex items-center gap-1.5 text-sm text-muted-foreground">
{t('footer.madeBy')}
<Heart className="h-3.5 w-3.5 text-red-500" />
<Heart className="h-3.5 w-3.5 text-red-500" aria-label={t('footer.love', 'love')} />
<a
href="https://github.com/paveg"
target="_blank"
Expand All @@ -20,15 +21,24 @@ export function Footer() {
paveg
</a>
</p>
<a
href="https://github.com/paveg/devcard"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-sm text-muted-foreground transition-colors hover:text-foreground"
>
<Github className="h-4 w-4" />
{t('footer.viewOnGitHub')}
</a>
<div className="flex items-center gap-4">
<Link
to="/privacy"
className="flex items-center gap-2 text-sm text-muted-foreground transition-colors hover:text-foreground"
>
<Shield className="h-4 w-4" aria-hidden="true" />
{t('footer.privacy', 'Privacy')}
</Link>
<a
href="https://github.com/paveg/devcard"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-sm text-muted-foreground transition-colors hover:text-foreground"
>
<Github className="h-4 w-4" aria-hidden="true" />
{t('footer.viewOnGitHub')}
</a>
</div>
</div>
</div>
</footer>
Expand Down
Loading