diff --git a/KEYWORD_RESEARCH_FEATURES.md b/KEYWORD_RESEARCH_FEATURES.md new file mode 100644 index 0000000..335dbd5 --- /dev/null +++ b/KEYWORD_RESEARCH_FEATURES.md @@ -0,0 +1,209 @@ +# Keyword Research & Secondary Keyword Analysis Features + +This document describes the new keyword research and secondary keyword analysis features integrated into ContentSwift, inspired by ALwrity's SEO capabilities. + +## Overview + +ContentSwift now includes comprehensive keyword research tools to help you: +- Discover LSI (Latent Semantic Indexing) keywords +- Find co-occurring keywords +- Identify question-based keywords +- Analyze keyword difficulty +- Get related keyword suggestions +- (Optional) Access keyword metrics like search volume and CPC + +## Features + +### 1. LSI Keywords +**What**: Semantically related terms found in competitor content using TF-IDF analysis. +**Why**: Using LSI keywords helps search engines understand your content better and can improve rankings. +**How**: Automatically extracted from competitor articles, showing relevance scores. + +### 2. Co-occurring Keywords +**What**: Terms that frequently appear near your primary keyword in competitor content. +**Why**: These keywords provide context and natural language patterns that improve content quality. +**How**: Analyzes a 50-word window around your primary keyword in all competitor content. + +### 3. Question Keywords +**What**: Question-based search queries extracted from Google autocomplete. +**Why**: Answers user questions and captures featured snippet opportunities. +**How**: Automatically categorizes questions by intent (informational, instructional, analytical, etc.). + +### 4. Related Keywords +**What**: Keywords from Google's "Related Searches" and "People Also Ask" sections. +**Why**: Expands topical coverage and targets additional search queries. +**How**: Pulled directly from SerpAPI search results. + +### 5. Keyword Difficulty Analysis +**What**: Competitive analysis showing how frequently competitors use your target keyword. +**Why**: Helps you understand the optimization level needed to compete. +**How**: Calculates average keyword count, density, and provides an Easy/Medium/Hard rating. + +### 6. Keyword Metrics (Optional) +**What**: Search volume, competition level, and CPC data from DataForSEO. +**Why**: Make data-driven decisions about which keywords to target. +**How**: Requires DataForSEO API credentials (see Configuration below). + +## How to Use + +### In the UI + +1. **Search for a keyword** on the home page +2. **Select competitor articles** to analyze +3. **Wait for scraping** to complete (Terms and Outline tabs) +4. **Click the "Keywords" tab** in the sidebar +5. **View comprehensive keyword analysis** including: + - Primary keyword difficulty rating + - LSI keywords with relevance scores + - Co-occurring keywords with frequencies + - Question-based keywords organized by intent + - Related keywords from Google + +### Via API + +#### Get Keyword Research +```bash +GET /keyword-research/{post_id} +``` + +Returns comprehensive keyword analysis for a post. + +**Example Response:** +```json +{ + "status": "success", + "data": { + "primary_keyword": "content marketing", + "lsi_keywords": [ + { + "keyword": "digital marketing", + "relevance_score": 0.8234, + "type": "lsi" + } + ], + "co_occurring_keywords": [ + { + "keyword": "strategy", + "frequency": 45, + "type": "co_occurring" + } + ], + "question_keywords": [ + { + "keyword": "what is content marketing", + "type": "question", + "intent": "informational" + } + ], + "related_keywords": [ + { + "keyword": "content marketing strategy", + "type": "related_search" + } + ], + "keyword_difficulty": { + "keyword": "content marketing", + "difficulty": "Medium", + "avg_keyword_count": 5.2, + "avg_keyword_density": 1.3 + } + } +} +``` + +#### Analyze Specific Keywords +```bash +POST /keyword-analyze/ +Content-Type: application/json + +{ + "keywords": ["content marketing", "SEO", "digital marketing"], + "location": "United States" +} +``` + +Gets keyword metrics (requires DataForSEO API credentials). + +## Configuration + +### Required +- `SERPAPI_KEY` - Already configured for basic Google search features + +### Optional (for keyword metrics) +Add to your `.env` file: +``` +DATAFORSEO_LOGIN=your_login +DATAFORSEO_PASSWORD=your_password +``` + +**Note**: Keyword metrics from DataForSEO are optional. All other features work without these credentials. + +## Technical Implementation + +### Backend +- **Service Module**: `backend-crt/src/services/keyword_research.py` +- **API Endpoints**: Added to `backend-crt/src/main.py` +- **Database**: New `keyword_research` JSON field in Post model +- **Dependencies**: Added `scikit-learn` for TF-IDF analysis + +### Frontend +- **Component**: `frontend-crt/src/components/KeywordResearch.tsx` +- **Integration**: New "Keywords" tab in Sidebar component +- **Styling**: Uses existing TailwindCSS theme + +### Algorithms Used + +1. **TF-IDF (Term Frequency-Inverse Document Frequency)** + - Identifies semantically important terms + - Scores keywords by relevance across competitor content + +2. **N-gram Analysis** + - Extracts unigrams, bigrams, and trigrams + - Captures multi-word keyword phrases + +3. **Co-occurrence Analysis** + - Window-based keyword proximity detection + - Frequency-weighted relevance scoring + +4. **Intent Classification** + - Categorizes question keywords by user intent + - Helps prioritize content structure + +## Benefits + +### For Content Writers +- Discover related topics naturally +- Answer user questions comprehensively +- Write more semantically rich content + +### For SEO Specialists +- Understand keyword difficulty before writing +- Target featured snippet opportunities +- Expand topical authority systematically + +### For Content Strategists +- Identify content gaps +- Prioritize high-value keywords +- Plan content clusters effectively + +## Future Enhancements + +Potential improvements for future versions: +- Keyword trend analysis over time +- Competitor keyword gap analysis +- Automated content brief generation +- Keyword clustering and grouping +- Search intent classification +- Seasonal keyword patterns + +## Support + +For issues or questions about keyword research features: +1. Check that competitor content has been scraped first +2. Verify your SERPAPI_KEY is configured +3. Review the browser console for any API errors +4. Check backend logs for detailed error messages + +## Credits + +These features were inspired by ALwrity's comprehensive SEO and keyword research capabilities, adapted for ContentSwift's content optimization workflow. diff --git a/README.md b/README.md index 09d3f5e..3d0c970 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,19 @@ Watch [Demo Video at Youtube](https://www.youtube.com/watch?v=HL9sPYXd1Ws&t=115s Using this tool, you'll get relevant information regarding specific keyword searches and hints on what other top-ranking results did with their article/page. +## ✨ New: Advanced Keyword Research Features + +ContentSwift now includes comprehensive keyword research and secondary keyword analysis capabilities: + +- **LSI Keywords** - Discover semantically related terms using TF-IDF analysis +- **Co-occurring Keywords** - Find terms that frequently appear near your primary keyword +- **Question Keywords** - Identify question-based search queries from Google autocomplete +- **Related Keywords** - Get suggestions from "Related Searches" and "People Also Ask" +- **Keyword Difficulty Analysis** - Understand competitive landscape before writing +- **Keyword Metrics** (optional) - Access search volume and CPC data with DataForSEO integration + +See [KEYWORD_RESEARCH_FEATURES.md](./KEYWORD_RESEARCH_FEATURES.md) for detailed documentation. + ![sample content optimization tool - ContentSwift](/assets/sample-content-optimization-tool.webp) > Right now we're focusing on Google SERP only diff --git a/backend-crt/.env.example b/backend-crt/.env.example new file mode 100644 index 0000000..f58543a --- /dev/null +++ b/backend-crt/.env.example @@ -0,0 +1,16 @@ +# ContentSwift Environment Variables + +# Required: SerpAPI for Google Search results +SERPAPI_KEY=your_serpapi_key_here + +# Optional: DataForSEO for keyword metrics (search volume, CPC, difficulty) +# Get credentials at: https://dataforseo.com/ +DATAFORSEO_LOGIN=your_dataforseo_login +DATAFORSEO_PASSWORD=your_dataforseo_password + +# Database (configured in docker-compose.yml) +# DB_HOST=db +# DB_PORT=5432 +# DB_NAME=crtool +# DB_USER=postgres +# DB_PASSWORD=password diff --git a/backend-crt/src/db/migrations/add_keyword_research.py b/backend-crt/src/db/migrations/add_keyword_research.py new file mode 100644 index 0000000..d348a12 --- /dev/null +++ b/backend-crt/src/db/migrations/add_keyword_research.py @@ -0,0 +1,47 @@ +""" +Database migration script to add keyword_research column to posts table +Run this script to update existing database schema +""" + +import sys +import os + +# Add parent directory to path to import models +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) + +from sqlalchemy import text +from db.models import engine + +def migrate(): + """Add keyword_research column to posts table if it doesn't exist""" + + print("Starting migration: add_keyword_research") + + with engine.connect() as conn: + # Check if column already exists + result = conn.execute(text(""" + SELECT column_name + FROM information_schema.columns + WHERE table_name='post' AND column_name='keyword_research'; + """)) + + if result.fetchone(): + print("Column 'keyword_research' already exists. Skipping migration.") + return + + # Add the column + print("Adding 'keyword_research' column to 'post' table...") + conn.execute(text(""" + ALTER TABLE post + ADD COLUMN keyword_research JSON DEFAULT NULL; + """)) + conn.commit() + + print("Migration completed successfully!") + +if __name__ == "__main__": + try: + migrate() + except Exception as e: + print(f"Migration failed: {e}") + sys.exit(1) diff --git a/backend-crt/src/db/models.py b/backend-crt/src/db/models.py index 6e756ac..052ffc9 100644 --- a/backend-crt/src/db/models.py +++ b/backend-crt/src/db/models.py @@ -12,6 +12,7 @@ class Post(SQLModel, table=True): search_result: dict = Field(sa_column=Column(JSON)) choosen_links: dict = Field(sa_column=Column(JSON)) links_scrape_result: dict = Field(sa_column=Column(JSON)) + keyword_research: dict = Field(default=None, sa_column=Column(JSON)) createdAt: datetime = Field(default_factory=datetime.utcnow) updatedAt: datetime = Field(default_factory=datetime.utcnow) diff --git a/backend-crt/src/main.py b/backend-crt/src/main.py index 3f95f55..26d9f95 100644 --- a/backend-crt/src/main.py +++ b/backend-crt/src/main.py @@ -18,6 +18,7 @@ import nltk from .db.post import add_post, save_links, get_post, get_all_post, remove_post, update_post +from .services.keyword_research import KeywordResearchService @@ -44,6 +45,10 @@ nltk.download('punkt') nltk.download('stopwords') +nltk.download('punkt_tab') + +# Initialize keyword research service +keyword_service = KeywordResearchService() # API Routes @app.get("/") @@ -262,6 +267,7 @@ def _scrape_article(link, lang): "keywords": keywords, "title": content_title, "headings": headings, + "content": content # Store full content for keyword analysis } def _getTopKeywords(allKeywords): @@ -283,4 +289,95 @@ def _getTopKeywords(allKeywords): # sort the dictionary by the frequency in descending order and get the first 10 items sorted_topKeywords = sorted(topKeywords.items(), key=operator.itemgetter(1), reverse=True)[:MAX_KEYWORD] - return sorted_topKeywords \ No newline at end of file + return sorted_topKeywords + +@app.get('/keyword-research/{post_id}') +async def getKeywordResearch(post_id: str): + """ + Get comprehensive keyword research including: + - LSI keywords + - Co-occurring keywords + - Question-based keywords + - Related keywords + - Keyword difficulty analysis + """ + post = get_post(post_id) + + # Return early if keyword research already exists + if hasattr(post, 'keyword_research') and post.keyword_research: + return { + "status": "success", + "data": post.keyword_research + } + + # Get primary keyword from post title + primary_keyword = post.title + + # Get autocomplete suggestions from search result + autocomplete_suggestions = [] + if post.search_result and 'autocomplete' in post.search_result: + autocomplete_suggestions = [ + item.get('value', '') + for item in post.search_result['autocomplete'] + ] + + # Get content from scraped articles + content_list = [] + if post.links_scrape_result and 'contentInfo' in post.links_scrape_result: + for article in post.links_scrape_result['contentInfo']: + # Use full content if available, otherwise use headings + if 'content' in article and article['content']: + content_list.append(article['content']) + else: + # Fallback to headings for older data + heading_text = ' '.join([h['text'] for h in article.get('headings', [])]) + if heading_text: + content_list.append(heading_text) + + # If we don't have content yet, scrape first + if not content_list: + return { + "status": "error", + "message": "Please scrape competitor content first" + } + + # Get comprehensive keyword research + keyword_research = keyword_service.get_secondary_keywords_comprehensive( + primary_keyword=primary_keyword, + content_list=content_list, + autocomplete_suggestions=autocomplete_suggestions, + include_metrics=False # Set to True if API credentials are configured + ) + + # Save to database + update_post(post_id, { + "keyword_research": keyword_research + }) + + return { + "status": "success", + "data": keyword_research + } + +@app.post('/keyword-analyze/') +def analyzeKeywords(data: dict): + """ + Analyze specific keywords for metrics + Endpoint for on-demand keyword analysis + """ + keywords = data.get('keywords', []) + location = data.get('location', 'United States') + + if not keywords: + return { + "status": "error", + "message": "No keywords provided" + } + + # Get keyword metrics + metrics = keyword_service.get_keyword_metrics_dataforseo(keywords, location) + + return { + "status": "success", + "data": metrics + } \ No newline at end of file diff --git a/backend-crt/src/requirements.txt b/backend-crt/src/requirements.txt index c88ed90..0fab9a0 100644 --- a/backend-crt/src/requirements.txt +++ b/backend-crt/src/requirements.txt @@ -33,3 +33,4 @@ tldextract==3.6.0 tqdm==4.66.1 typing_extensions==4.8.0 urllib3==2.0.5 +scikit-learn==1.3.2 diff --git a/backend-crt/src/services/keyword_research.py b/backend-crt/src/services/keyword_research.py new file mode 100644 index 0000000..6816a9a --- /dev/null +++ b/backend-crt/src/services/keyword_research.py @@ -0,0 +1,396 @@ +""" +Keyword Research Service +Provides advanced keyword research capabilities including: +- Secondary keyword extraction (LSI keywords) +- Keyword metrics (volume, difficulty, CPC) +- Related keyword suggestions +- Question-based keywords +""" + +import os +import requests +from typing import List, Dict, Optional +from dotenv import load_dotenv +from nltk.corpus import stopwords +from nltk.tokenize import word_tokenize +from sklearn.feature_extraction.text import TfidfVectorizer +from collections import Counter +import re + +load_dotenv() + +class KeywordResearchService: + """Service for keyword research and analysis""" + + def __init__(self): + self.serpapi_key = os.getenv('SERPAPI_KEY') + self.dataforseo_login = os.getenv('DATAFORSEO_LOGIN') + self.dataforseo_password = os.getenv('DATAFORSEO_PASSWORD') + + def extract_lsi_keywords(self, content_list: List[str], primary_keyword: str, top_n: int = 20) -> List[Dict]: + """ + Extract LSI (Latent Semantic Indexing) keywords from content + Uses TF-IDF to find semantically related terms + + Args: + content_list: List of content strings to analyze + primary_keyword: The main keyword to find related terms for + top_n: Number of LSI keywords to return + + Returns: + List of dictionaries with LSI keywords and their scores + """ + if not content_list: + return [] + + # Combine all content + combined_content = ' '.join(content_list) + + # Initialize TF-IDF vectorizer + vectorizer = TfidfVectorizer( + max_features=100, + stop_words='english', + ngram_range=(1, 3), # Include unigrams, bigrams, and trigrams + min_df=1, + max_df=0.8 + ) + + try: + # Fit and transform the content + tfidf_matrix = vectorizer.fit_transform(content_list) + feature_names = vectorizer.get_feature_names_out() + + # Calculate average TF-IDF scores across all documents + avg_scores = tfidf_matrix.mean(axis=0).A1 + + # Create keyword-score pairs + keyword_scores = [] + primary_lower = primary_keyword.lower() + + for idx, score in enumerate(avg_scores): + keyword = feature_names[idx] + # Exclude the primary keyword itself + if keyword.lower() != primary_lower and score > 0: + keyword_scores.append({ + 'keyword': keyword, + 'relevance_score': round(float(score), 4), + 'type': 'lsi' + }) + + # Sort by relevance score and return top N + keyword_scores.sort(key=lambda x: x['relevance_score'], reverse=True) + return keyword_scores[:top_n] + + except Exception as e: + print(f"Error extracting LSI keywords: {e}") + return [] + + def extract_co_occurring_keywords(self, content: str, primary_keyword: str, window_size: int = 50, top_n: int = 15) -> List[Dict]: + """ + Extract keywords that frequently co-occur with the primary keyword + + Args: + content: Text content to analyze + primary_keyword: Main keyword to find co-occurrences for + window_size: Number of words around primary keyword to consider + top_n: Number of co-occurring keywords to return + + Returns: + List of co-occurring keywords with frequency + """ + try: + # Tokenize and clean + words = word_tokenize(content.lower()) + stop_words = set(stopwords.words('english')) + + # Find positions of primary keyword + primary_lower = primary_keyword.lower() + primary_positions = [i for i, word in enumerate(words) if primary_lower in word] + + # Collect words within window of primary keyword + co_occurring_words = [] + for pos in primary_positions: + start = max(0, pos - window_size) + end = min(len(words), pos + window_size + 1) + window_words = words[start:end] + + # Filter out stopwords and non-alphabetic + filtered_words = [ + w for w in window_words + if w.isalpha() and w not in stop_words and w != primary_lower + ] + co_occurring_words.extend(filtered_words) + + # Count frequencies + word_freq = Counter(co_occurring_words) + + # Return top N with frequency + top_words = word_freq.most_common(top_n) + return [ + { + 'keyword': word, + 'frequency': freq, + 'type': 'co_occurring' + } + for word, freq in top_words + ] + + except Exception as e: + print(f"Error extracting co-occurring keywords: {e}") + return [] + + def extract_question_keywords(self, autocomplete_suggestions: List[str]) -> List[Dict]: + """ + Extract question-based keywords from autocomplete suggestions + + Args: + autocomplete_suggestions: List of autocomplete suggestions + + Returns: + List of question keywords + """ + question_words = ['what', 'why', 'how', 'when', 'where', 'who', 'which', 'can', 'is', 'are', 'do', 'does'] + + question_keywords = [] + for suggestion in autocomplete_suggestions: + suggestion_lower = suggestion.lower() + if any(suggestion_lower.startswith(q) for q in question_words): + question_keywords.append({ + 'keyword': suggestion, + 'type': 'question', + 'intent': self._classify_question_intent(suggestion_lower) + }) + + return question_keywords + + def _classify_question_intent(self, question: str) -> str: + """Classify the intent of a question keyword""" + if question.startswith(('what', 'which')): + return 'informational' + elif question.startswith(('how',)): + return 'instructional' + elif question.startswith(('why',)): + return 'analytical' + elif question.startswith(('where', 'when')): + return 'locational' + elif question.startswith(('can', 'is', 'are', 'do', 'does')): + return 'verification' + else: + return 'general' + + def get_keyword_metrics_dataforseo(self, keywords: List[str], location: str = "United States") -> List[Dict]: + """ + Get keyword metrics (volume, difficulty, CPC) from DataForSEO API + + Args: + keywords: List of keywords to get metrics for + location: Target location for keyword data + + Returns: + List of keyword metrics + """ + if not self.dataforseo_login or not self.dataforseo_password: + print("DataForSEO credentials not configured") + return [] + + try: + # DataForSEO API endpoint + url = "https://api.dataforseo.com/v3/keywords_data/google_ads/search_volume/live" + + # Prepare request + auth = (self.dataforseo_login, self.dataforseo_password) + headers = {'Content-Type': 'application/json'} + + payload = [{ + "keywords": keywords[:100], # Limit to 100 keywords per request + "location_name": location, + "language_name": "English" + }] + + response = requests.post(url, json=payload, auth=auth, headers=headers) + + if response.status_code == 200: + data = response.json() + if data.get('tasks') and data['tasks'][0].get('result'): + results = data['tasks'][0]['result'] + return [ + { + 'keyword': item.get('keyword'), + 'search_volume': item.get('search_volume', 0), + 'competition': item.get('competition', 'N/A'), + 'cpc': item.get('cpc', 0), + 'type': 'metric' + } + for item in results if item + ] + else: + print(f"DataForSEO API error: {response.status_code}") + + except Exception as e: + print(f"Error fetching keyword metrics: {e}") + + return [] + + def get_related_keywords_serpapi(self, keyword: str, location: str = "us") -> List[Dict]: + """ + Get related keywords from Google using SerpAPI + + Args: + keyword: Primary keyword + location: Target location code (e.g., 'us', 'uk') + + Returns: + List of related keywords + """ + if not self.serpapi_key: + print("SERPAPI_KEY not configured") + return [] + + try: + from serpapi import GoogleSearch + + search = GoogleSearch({ + "api_key": self.serpapi_key, + "engine": "google", + "q": keyword, + "gl": location, + "num": 10 + }) + + results = search.get_dict() + related_keywords = [] + + # Extract related searches + if 'related_searches' in results: + for item in results['related_searches']: + related_keywords.append({ + 'keyword': item.get('query', ''), + 'type': 'related_search' + }) + + # Extract "People also ask" questions + if 'related_questions' in results: + for item in results['related_questions']: + related_keywords.append({ + 'keyword': item.get('question', ''), + 'type': 'people_also_ask', + 'snippet': item.get('snippet', '') + }) + + return related_keywords + + except Exception as e: + print(f"Error fetching related keywords: {e}") + return [] + + def analyze_keyword_difficulty(self, keyword: str, competitors_content: List[str]) -> Dict: + """ + Analyze keyword difficulty based on competitor content + + Args: + keyword: Target keyword + competitors_content: List of competitor content + + Returns: + Dictionary with difficulty metrics + """ + keyword_lower = keyword.lower() + + # Count keyword occurrences in competitor content + occurrences = [] + for content in competitors_content: + count = content.lower().count(keyword_lower) + words = len(content.split()) + density = (count / words * 100) if words > 0 else 0 + occurrences.append({ + 'count': count, + 'density': round(density, 2) + }) + + if not occurrences: + return {} + + # Calculate average metrics + avg_count = sum(o['count'] for o in occurrences) / len(occurrences) + avg_density = sum(o['density'] for o in occurrences) / len(occurrences) + + # Determine difficulty level + if avg_count < 3 and avg_density < 0.5: + difficulty = 'Easy' + elif avg_count < 8 and avg_density < 1.5: + difficulty = 'Medium' + else: + difficulty = 'Hard' + + return { + 'keyword': keyword, + 'difficulty': difficulty, + 'avg_keyword_count': round(avg_count, 2), + 'avg_keyword_density': round(avg_density, 2), + 'competitor_usage': occurrences + } + + def get_secondary_keywords_comprehensive( + self, + primary_keyword: str, + content_list: List[str], + autocomplete_suggestions: List[str] = None, + include_metrics: bool = False + ) -> Dict: + """ + Get comprehensive secondary keyword analysis + + Args: + primary_keyword: Main target keyword + content_list: List of competitor content to analyze + autocomplete_suggestions: Google autocomplete suggestions + include_metrics: Whether to fetch keyword metrics (requires API) + + Returns: + Dictionary with all secondary keyword data + """ + result = { + 'primary_keyword': primary_keyword, + 'lsi_keywords': [], + 'co_occurring_keywords': [], + 'question_keywords': [], + 'related_keywords': [], + 'keyword_difficulty': {} + } + + # Extract LSI keywords + if content_list: + result['lsi_keywords'] = self.extract_lsi_keywords(content_list, primary_keyword) + + # Extract co-occurring keywords from combined content + combined_content = ' '.join(content_list) + result['co_occurring_keywords'] = self.extract_co_occurring_keywords( + combined_content, + primary_keyword + ) + + # Analyze keyword difficulty + result['keyword_difficulty'] = self.analyze_keyword_difficulty( + primary_keyword, + content_list + ) + + # Extract question keywords + if autocomplete_suggestions: + result['question_keywords'] = self.extract_question_keywords( + autocomplete_suggestions + ) + + # Get related keywords from SERP + result['related_keywords'] = self.get_related_keywords_serpapi(primary_keyword) + + # Get keyword metrics if requested + if include_metrics: + all_keywords = [primary_keyword] + all_keywords.extend([k['keyword'] for k in result['lsi_keywords'][:10]]) + all_keywords.extend([k['keyword'] for k in result['question_keywords'][:10]]) + + metrics = self.get_keyword_metrics_dataforseo(all_keywords) + result['keyword_metrics'] = metrics + + return result diff --git a/frontend-crt/src/components/KeywordResearch.tsx b/frontend-crt/src/components/KeywordResearch.tsx new file mode 100644 index 0000000..1cec500 --- /dev/null +++ b/frontend-crt/src/components/KeywordResearch.tsx @@ -0,0 +1,198 @@ +"use client" +import { useEffect, useState } from "react"; + +interface KeywordData { + keyword: string; + relevance_score?: number; + frequency?: number; + type: string; + intent?: string; + snippet?: string; +} + +interface KeywordDifficulty { + keyword: string; + difficulty: string; + avg_keyword_count: number; + avg_keyword_density: number; +} + +interface KeywordResearchData { + primary_keyword: string; + lsi_keywords: KeywordData[]; + co_occurring_keywords: KeywordData[]; + question_keywords: KeywordData[]; + related_keywords: KeywordData[]; + keyword_difficulty: KeywordDifficulty; +} + +export default function KeywordResearch({ id }: { id: string }) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchKeywordResearch = async () => { + try { + const res = await fetch(`http://localhost:8000/keyword-research/${id}`); + + if (!res.ok) { + const errorData = await res.json(); + throw new Error(errorData.message || 'Failed to fetch keyword research'); + } + + const responseData = await res.json(); + setData(responseData.data); + setLoading(false); + } catch (err: any) { + console.error(err); + setError(err.message); + setLoading(false); + } + }; + + fetchKeywordResearch(); + }, [id]); + + if (loading) { + return ( +
+

Analyzing keywords... (This may take a moment)

+
+ ); + } + + if (error) { + return ( +
+

Error: {error}

+

Make sure you've scraped competitor content first.

+
+ ); + } + + if (!data) { + return ( +
+

No keyword research data available.

+
+ ); + } + + return ( +
+ {/* Primary Keyword Info */} +
+

Primary Keyword

+

{data.primary_keyword}

+
+ + {/* Keyword Difficulty */} + {data.keyword_difficulty && Object.keys(data.keyword_difficulty).length > 0 && ( +
+

Keyword Difficulty

+
+

+ Difficulty: + + {data.keyword_difficulty.difficulty} + +

+

+ Avg. Usage: + {data.keyword_difficulty.avg_keyword_count} times per article +

+

+ Avg. Density: + {data.keyword_difficulty.avg_keyword_density}% +

+
+
+ )} + + {/* LSI Keywords */} + {data.lsi_keywords && data.lsi_keywords.length > 0 && ( +
+

LSI Keywords

+

Semantically related terms found in competitor content

+
+ {data.lsi_keywords.slice(0, 15).map((item, index) => ( +
+ {item.keyword} + + {(item.relevance_score! * 100).toFixed(1)}% relevant + +
+ ))} +
+
+ )} + + {/* Co-occurring Keywords */} + {data.co_occurring_keywords && data.co_occurring_keywords.length > 0 && ( +
+

Co-occurring Keywords

+

Terms frequently appearing near the primary keyword

+
+ {data.co_occurring_keywords.slice(0, 10).map((item, index) => ( +
+ {item.keyword} + + {item.frequency} times + +
+ ))} +
+
+ )} + + {/* Question Keywords */} + {data.question_keywords && data.question_keywords.length > 0 && ( +
+

Question Keywords

+

Question-based search queries to address

+
+ {data.question_keywords.map((item, index) => ( +
+

{item.keyword}

+ {item.intent && ( + + Intent: {item.intent} + + )} +
+ ))} +
+
+ )} + + {/* Related Keywords */} + {data.related_keywords && data.related_keywords.length > 0 && ( +
+

Related Keywords

+

From Google related searches and "People also ask"

+
+ {data.related_keywords.map((item, index) => ( +
+

{item.keyword}

+ {item.snippet && ( +

+ {item.snippet} +

+ )} + + {item.type === 'people_also_ask' ? 'People also ask' : 'Related search'} + +
+ ))} +
+
+ )} +
+ ); +} diff --git a/frontend-crt/src/components/Sidebar.tsx b/frontend-crt/src/components/Sidebar.tsx index c901bb0..9abffbf 100644 --- a/frontend-crt/src/components/Sidebar.tsx +++ b/frontend-crt/src/components/Sidebar.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import Terms from "./Terms"; import Outline from "./Outline"; +import KeywordResearch from "./KeywordResearch"; export default function Sidebar({ @@ -38,22 +39,27 @@ export default function Sidebar({ return (
-
+
{/*

Terms

*/} -

{setMenu('terms')}} +

{setMenu('terms')}} className={` - cursor-pointer hover:text-emerald-500 - ${menu === 'terms' ? 'text-emerald-700 border-b-2 border-b-emerald-700' : ''} + cursor-pointer hover:text-emerald-500 whitespace-nowrap + ${menu === 'terms' ? 'text-emerald-700 border-b-2 border-b-emerald-700' : ''} `}>Terms

-

{setMenu('research')}} +

{setMenu('keywords')}} className={` - cursor-pointer hover:text-emerald-500 + cursor-pointer hover:text-emerald-500 whitespace-nowrap + ${menu === 'keywords' ? 'text-emerald-700 border-b-2 border-b-emerald-700' : ''} + `}>Keywords

+

{setMenu('research')}} + className={` + cursor-pointer hover:text-emerald-500 whitespace-nowrap ${menu === 'research' ? 'text-emerald-700 border-b-2 border-b-emerald-700' : ''} `}> Research

-

{setMenu('outline')}} +

{setMenu('outline')}} className={` - cursor-pointer hover:text-emerald-500 + cursor-pointer hover:text-emerald-500 whitespace-nowrap ${menu === 'outline' ? 'text-emerald-700 border-b-2 border-b-emerald-700' : ''} `}>Outline

@@ -165,17 +171,23 @@ export default function Sidebar({ menu == 'terms' && ( <> { - loading ?

Loading ... (Up to 1 minutes)

: + loading ?

Loading ... (Up to 1 minutes)

: } ) } + { + menu == 'keywords' && ( + + ) + } + { menu == 'outline' && ( <> { - loading ?

Loading ... (Up to 1 minutes)

: + loading ?

Loading ... (Up to 1 minutes)

: } )