Skip to content

Commit 7bab3c1

Browse files
committed
feat(profile): generate user-specific SEO-friendly content using GitHub profile data and Gen AI
1 parent 0ae911c commit 7bab3c1

4 files changed

Lines changed: 91 additions & 13 deletions

File tree

api/main.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,13 @@ async def get_cached_github_profile(username: str) -> Dict[str, Any]:
6868
try:
6969
ai_generator = AIDescriptionGenerator()
7070
about_data = ai_generator.generate_profile_summary(basic_profile)
71+
seo_data = ai_generator.generate_seo_contents(basic_profile)
7172
basic_profile['about'] = about_data
73+
basic_profile['seo'] = seo_data
7274
except Exception as e:
7375
print(f"Failed to generate AI description: {str(e)}")
7476
basic_profile['about'] = None
77+
basic_profile['seo'] = None
7578
if Settings.CACHE_ENABLED:
7679
# deep copy the object to avoid modifying the original object
7780
tobe_cached = copy.deepcopy(basic_profile)

modules/ai_generator.py

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,52 @@ def __init__(self):
1212
"""Initialize Groq client"""
1313
self.client = Groq(api_key=Settings.get_groq_key())
1414

15+
def generate_seo_contents(self, profile_data):
16+
"""
17+
Generate a professional SEO-optimized profile content like title, description, keywords
18+
19+
Args:
20+
profile_data (dict): GitHub user profile data
21+
22+
Returns:
23+
str: AI-generated SEO-optimized profile content
24+
"""
25+
prompt = (
26+
"Generate a concise, professional, and SEO-optimized profile snippet for a developer profile page."
27+
"\n\nReturn the output strictly in the following JSON format (without any additional commentary):"
28+
'\n{\n "title": "<Max 10 words. Format: FirstName (@username). Role passionate about [what they do]>",'
29+
'\n "description": "<Max 30 words (120–160 characters). Meta-style description that highlights skills and invites engagement>",'
30+
'\n "keywords": "<8–15 comma-separated keywords or phrases. Focus on Next.js-related terms, long-tail SEO phrases, and specific skills>"\n}'
31+
"\n\nUse this input data to personalize the content, handling missing or empty fields gracefully:"
32+
f"\n- Name: {profile_data.get('name', 'Anonymous Developer')}"
33+
f"\n- Username: {profile_data.get('username', 'username')}"
34+
f"\n- Followers: {profile_data.get('followers', 0)} (highlight if over 500)"
35+
f"\n- Public Repositories: {profile_data.get('public_repos', 0)} (highlight if over 20)"
36+
f"\n- Bio: {profile_data.get('bio', '')} (infer core skills or passions)"
37+
f"\n- README: {profile_data.get('readme_content', '')} (extract unique traits or standout projects)"
38+
"\n\nIf data is sparse, infer likely skills or focus areas. Avoid filler or generic phrases. Prioritize precision and clarity."
39+
)
40+
41+
response = self.client.chat.completions.create(
42+
messages=[
43+
{
44+
"role": "system",
45+
"content": "You are an SEO-optimized profile content generator for developer portfolios and GitHub profiles. Create search engine friendly, professional profile summaries that enhance discoverability and professional presence. Generate content in natural paragraph format without headings, lists, or bullet points. Focus on keyword integration, meta-friendly descriptions, and compelling copy that drives engagement and showcases technical expertise effectively.",
46+
},
47+
{"role": "user", "content": prompt},
48+
],
49+
model="llama-3.1-8b-instant",
50+
)
51+
if not response.choices or response.choices[0].message.content == "":
52+
raise Exception("No response from AI model")
53+
54+
result = json.loads(response.choices[0].message.content)
55+
return {
56+
"title": result["title"],
57+
"description": result["description"],
58+
"keywords": result["keywords"],
59+
}
60+
1561
def generate_profile_summary(self, profile_data):
1662
"""
1763
Generate a professional profile summary
@@ -42,14 +88,11 @@ def generate_profile_summary(self, profile_data):
4288
messages=[
4389
{
4490
"role": "system",
45-
"content": "You are a professional profile summarizer for GitHub developers. create a professional profile summary without any heading ,list or bullet points."
91+
"content": "You are a professional profile summarizer for GitHub developers. create a professional profile summary without any heading ,list or bullet points.",
4692
},
47-
{
48-
"role": "user",
49-
"content": prompt
50-
}
93+
{"role": "user", "content": prompt},
5194
],
52-
model="llama-3.1-8b-instant"
95+
model="llama-3.1-8b-instant",
5396
)
5497
if not response.choices or response.choices[0].message.content == "":
5598
raise Exception("No response from AI model")
@@ -97,7 +140,7 @@ def validate_json_response(response):
97140
for repo, details in parsed.items():
98141
if not isinstance(details, dict):
99142
return False
100-
if 'link' not in details or 'summary' not in details:
143+
if "link" not in details or "summary" not in details:
101144
return False
102145

103146
return parsed
@@ -111,15 +154,12 @@ def validate_json_response(response):
111154
messages=[
112155
{
113156
"role": "system",
114-
"content": "You are a GitHub activity summarizer. Provide a precise JSON summary of repository activities."
157+
"content": "You are a GitHub activity summarizer. Provide a precise JSON summary of repository activities.",
115158
},
116-
{
117-
"role": "user",
118-
"content": construct_prompt(contributions)
119-
}
159+
{"role": "user", "content": construct_prompt(contributions)},
120160
],
121161
model="llama-3.1-8b-instant",
122-
response_format={"type": "json_object"}
162+
response_format={"type": "json_object"},
123163
)
124164

125165
# Extract response content

www/app/[username]/layout.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,33 @@
11
import React from "react";
2+
import { Metadata } from "next";
3+
import { getProfileData } from "@/lib/api";
24

5+
export async function generateMetadata({
6+
params,
7+
}: {
8+
params: Promise<{ username: string }>;
9+
}): Promise<Metadata> {
10+
const { username } = await params;
11+
const user = await getProfileData(username);
12+
return {
13+
title: user
14+
? user.seo.title
15+
: `Devb.io - Build Stunning Developer Portfolios in Minutes`,
16+
description: user
17+
? user.seo.description
18+
: `Passionate developer skilled in modern technologies, building and learning through real-world projects and daily challenges.`,
19+
keywords: user
20+
? user.seo.keywords
21+
: "Developer Portfolio, Devb.io, Software Engineer, Projects, Resume, GitHub Showcase",
22+
23+
openGraph: {
24+
images: user?.avatar_url,
25+
},
26+
twitter: {
27+
images: user?.avatar_url,
28+
},
29+
};
30+
}
331
const Layout = ({ children }: { children: React.ReactNode }) => {
432
return <>{children}</>;
533
};

www/types/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export type Profile = {
2424
social_accounts: SocialAccount[];
2525
readme_content: string;
2626
about: string;
27+
seo: SEOContent;
2728
cached: boolean;
2829
};
2930

@@ -102,6 +103,12 @@ export type LinkedInProfile = {
102103
education: Education[];
103104
};
104105

106+
export type SEOContent = {
107+
title: string;
108+
description: string;
109+
keywords: string;
110+
}
111+
105112
export interface MediumBlog {
106113
title: string;
107114
link: string;

0 commit comments

Comments
 (0)