From 7bab3c197f70e1a2e708ec229d02b7d00fd7fbcf Mon Sep 17 00:00:00 2001 From: Nayan Date: Fri, 13 Jun 2025 10:48:51 +0530 Subject: [PATCH 1/2] feat(profile): generate user-specific SEO-friendly content using GitHub profile data and Gen AI --- api/main.py | 3 ++ modules/ai_generator.py | 66 ++++++++++++++++++++++++++++------- www/app/[username]/layout.tsx | 28 +++++++++++++++ www/types/types.ts | 7 ++++ 4 files changed, 91 insertions(+), 13 deletions(-) diff --git a/api/main.py b/api/main.py index 83e52485..bdaeb2a6 100644 --- a/api/main.py +++ b/api/main.py @@ -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) diff --git a/modules/ai_generator.py b/modules/ai_generator.py index ca08cf52..6443d938 100644 --- a/modules/ai_generator.py +++ b/modules/ai_generator.py @@ -12,6 +12,52 @@ def __init__(self): """Initialize Groq client""" self.client = Groq(api_key=Settings.get_groq_key()) + def generate_seo_contents(self, profile_data): + """ + Generate a professional SEO-optimized profile content like title, description, keywords + + Args: + profile_data (dict): GitHub user profile data + + Returns: + str: 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": "",' + '\n "description": "",' + '\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.", + }, + {"role": "user", "content": prompt}, + ], + model="llama-3.1-8b-instant", + ) + if not response.choices or response.choices[0].message.content == "": + raise Exception("No response from AI model") + + result = json.loads(response.choices[0].message.content) + return { + "title": result["title"], + "description": result["description"], + "keywords": result["keywords"], + } + def generate_profile_summary(self, profile_data): """ Generate a professional profile summary @@ -42,14 +88,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") @@ -97,7 +140,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 @@ -111,15 +154,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 diff --git a/www/app/[username]/layout.tsx b/www/app/[username]/layout.tsx index d7c3ee81..bcb633e4 100644 --- a/www/app/[username]/layout.tsx +++ b/www/app/[username]/layout.tsx @@ -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 }>; +}): Promise { + const { username } = await params; + const user = await getProfileData(username); + return { + title: user + ? user.seo.title + : `Devb.io - Build Stunning Developer Portfolios in Minutes`, + description: user + ? user.seo.description + : `Passionate developer skilled in modern technologies, building and learning through real-world projects and daily challenges.`, + keywords: user + ? 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}; }; diff --git a/www/types/types.ts b/www/types/types.ts index 0481ffcf..639968eb 100644 --- a/www/types/types.ts +++ b/www/types/types.ts @@ -24,6 +24,7 @@ export type Profile = { social_accounts: SocialAccount[]; readme_content: string; about: string; + seo: SEOContent; cached: boolean; }; @@ -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; From d922b774f7515fd1ad06b75c27d355a8892fbb73 Mon Sep 17 00:00:00 2001 From: Nayan Date: Sat, 14 Jun 2025 08:57:36 +0530 Subject: [PATCH 2/2] fix(profile): added review changes --- modules/ai_generator.py | 35 +++++++++++++++++++++++++++-------- www/app/[username]/layout.tsx | 6 +++--- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/modules/ai_generator.py b/modules/ai_generator.py index 6443d938..d27e4bd7 100644 --- a/modules/ai_generator.py +++ b/modules/ai_generator.py @@ -12,7 +12,7 @@ def __init__(self): """Initialize Groq client""" self.client = Groq(api_key=Settings.get_groq_key()) - def generate_seo_contents(self, profile_data): + def generate_seo_contents(self, profile_data: dict): """ Generate a professional SEO-optimized profile content like title, description, keywords @@ -20,7 +20,7 @@ def generate_seo_contents(self, profile_data): profile_data (dict): GitHub user profile data Returns: - str: AI-generated SEO-optimized profile content + dict: AI-generated SEO-optimized profile content """ prompt = ( "Generate a concise, professional, and SEO-optimized profile snippet for a developer profile page." @@ -42,20 +42,39 @@ def generate_seo_contents(self, profile_data): 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.", + "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") - - result = json.loads(response.choices[0].message.content) + 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": result["title"], - "description": result["description"], - "keywords": result["keywords"], + "title": title, + "description": description, + "keywords": keywords, } def generate_profile_summary(self, profile_data): diff --git a/www/app/[username]/layout.tsx b/www/app/[username]/layout.tsx index bcb633e4..a5b1e90e 100644 --- a/www/app/[username]/layout.tsx +++ b/www/app/[username]/layout.tsx @@ -10,13 +10,13 @@ export async function generateMetadata({ const { username } = await params; const user = await getProfileData(username); return { - title: user + title: user?.seo?.title ? user.seo.title : `Devb.io - Build Stunning Developer Portfolios in Minutes`, - description: user + 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 + keywords: user?.seo?.keywords ? user.seo.keywords : "Developer Portfolio, Devb.io, Software Engineer, Projects, Resume, GitHub Showcase",