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
3 changes: 3 additions & 0 deletions api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,13 @@ async def get_cached_github_profile(username: str) -> Dict[str, Any]:
try:
ai_generator = AIDescriptionGenerator()
about_data = ai_generator.generate_profile_summary(basic_profile)
seo_data = ai_generator.generate_seo_contents(basic_profile)
basic_profile['about'] = about_data
basic_profile['seo'] = seo_data
except Exception as e:
print(f"Failed to generate AI description: {str(e)}")
basic_profile['about'] = None
basic_profile['seo'] = None
if Settings.CACHE_ENABLED:
# deep copy the object to avoid modifying the original object
tobe_cached = copy.deepcopy(basic_profile)
Expand Down
85 changes: 72 additions & 13 deletions modules/ai_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,71 @@ def __init__(self):
"""Initialize Groq client"""
self.client = Groq(api_key=Settings.get_groq_key())

def generate_seo_contents(self, profile_data: dict):
"""
Generate a professional SEO-optimized profile content like title, description, keywords

Args:
profile_data (dict): GitHub user profile data

Returns:
dict: AI-generated SEO-optimized profile content
"""
prompt = (
"Generate a concise, professional, and SEO-optimized profile snippet for a developer profile page."
"\n\nReturn the output strictly in the following JSON format (without any additional commentary):"
'\n{\n "title": "<Max 10 words. Format: FirstName (@username). Role passionate about [what they do]>",'
'\n "description": "<Max 30 words (120–160 characters). Meta-style description that highlights skills and invites engagement>",'
'\n "keywords": "<8–15 comma-separated keywords or phrases. Focus on Next.js-related terms, long-tail SEO phrases, and specific skills>"\n}'
"\n\nUse this input data to personalize the content, handling missing or empty fields gracefully:"
f"\n- Name: {profile_data.get('name', 'Anonymous Developer')}"
f"\n- Username: {profile_data.get('username', 'username')}"
f"\n- Followers: {profile_data.get('followers', 0)} (highlight if over 500)"
f"\n- Public Repositories: {profile_data.get('public_repos', 0)} (highlight if over 20)"
f"\n- Bio: {profile_data.get('bio', '')} (infer core skills or passions)"
f"\n- README: {profile_data.get('readme_content', '')} (extract unique traits or standout projects)"
"\n\nIf data is sparse, infer likely skills or focus areas. Avoid filler or generic phrases. Prioritize precision and clarity."
)

response = self.client.chat.completions.create(
messages=[
{
"role": "system",
"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. Your output should be properly formatted JSON when requested, with each field containing well-crafted, SEO-optimized content.",
},
{"role": "user", "content": prompt},
],
model="llama-3.1-8b-instant",
response_format={"type": "json_object"},
)
if not response.choices or response.choices[0].message.content == "":
raise Exception("No response from AI model")
try:
result = json.loads(response.choices[0].message.content)
except json.JSONDecodeError as e:
raise Exception(f"AI response was not valid JSON. Content: {response.choices[0].message.content}") from e

title = result["title"]
description = result["description"]
keywords = result["keywords"]

if not (title and description and keywords):
missing = []
if not title:
missing.append("title")
if not description:
missing.append("description")
if not keywords:
missing.append("keywords")
raise Exception(
f"AI response missing required SEO fields: {', '.join(missing)}. Received: {result}"
)
return {
"title": title,
"description": description,
"keywords": keywords,
}

def generate_profile_summary(self, profile_data):
"""
Generate a professional profile summary
Expand Down Expand Up @@ -42,14 +107,11 @@ def generate_profile_summary(self, profile_data):
messages=[
{
"role": "system",
"content": "You are a professional profile summarizer for GitHub developers. create a professional profile summary without any heading ,list or bullet points."
"content": "You are a professional profile summarizer for GitHub developers. create a professional profile summary without any heading ,list or bullet points.",
},
{
"role": "user",
"content": prompt
}
{"role": "user", "content": prompt},
],
model="llama-3.1-8b-instant"
model="llama-3.1-8b-instant",
)
if not response.choices or response.choices[0].message.content == "":
raise Exception("No response from AI model")
Expand Down Expand Up @@ -97,7 +159,7 @@ def validate_json_response(response):
for repo, details in parsed.items():
if not isinstance(details, dict):
return False
if 'link' not in details or 'summary' not in details:
if "link" not in details or "summary" not in details:
return False

return parsed
Expand All @@ -111,15 +173,12 @@ def validate_json_response(response):
messages=[
{
"role": "system",
"content": "You are a GitHub activity summarizer. Provide a precise JSON summary of repository activities."
"content": "You are a GitHub activity summarizer. Provide a precise JSON summary of repository activities.",
},
{
"role": "user",
"content": construct_prompt(contributions)
}
{"role": "user", "content": construct_prompt(contributions)},
],
model="llama-3.1-8b-instant",
response_format={"type": "json_object"}
response_format={"type": "json_object"},
)

# Extract response content
Expand Down
28 changes: 28 additions & 0 deletions www/app/[username]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
import React from "react";
import { Metadata } from "next";
import { getProfileData } from "@/lib/api";

export async function generateMetadata({
params,
}: {
params: Promise<{ username: string }>;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The type for params is Promise<{ username: string }>. In Next.js App Router's generateMetadata function, params is typically passed as a resolved object (e.g., { username: string }), not a Promise. If this is standard Next.js usage, the type should be params: { username: string }, and await params on line 10 would not be necessary (it would be const { username } = params;). Please verify if this Promise-based type is intentional due to a custom setup or if it should align with the standard Next.js pattern.

  params: { username: string };

}): Promise<Metadata> {
const { username } = await params;
const user = await getProfileData(username);
return {
title: user?.seo?.title
? user.seo.title
: `Devb.io - Build Stunning Developer Portfolios in Minutes`,
description: user?.seo?.description
? user.seo.description
: `Passionate developer skilled in modern technologies, building and learning through real-world projects and daily challenges.`,
keywords: user?.seo?.keywords
? user.seo.keywords
: "Developer Portfolio, Devb.io, Software Engineer, Projects, Resume, GitHub Showcase",

openGraph: {
images: user?.avatar_url,
},
twitter: {
images: user?.avatar_url,
},
};
}
const Layout = ({ children }: { children: React.ReactNode }) => {
return <>{children}</>;
};
Expand Down
7 changes: 7 additions & 0 deletions www/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type Profile = {
social_accounts: SocialAccount[];
readme_content: string;
about: string;
seo: SEOContent;
cached: boolean;
};

Expand Down Expand Up @@ -102,6 +103,12 @@ export type LinkedInProfile = {
education: Education[];
};

export type SEOContent = {
title: string;
description: string;
keywords: string;
}

export interface MediumBlog {
title: string;
link: string;
Expand Down
Loading