initial-commit
This commit is contained in:
commit
6e82a55219
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
dist
|
||||
.DS_Store
|
||||
*.log
|
||||
.env
|
||||
.env.local
|
||||
|
||||
286
API_QUICK_REFERENCE.md
Normal file
286
API_QUICK_REFERENCE.md
Normal file
@ -0,0 +1,286 @@
|
||||
# SEMrush API Quick Reference
|
||||
|
||||
## ⚠️ IMPORTANT: Costs Are ESTIMATED
|
||||
|
||||
**All costs below are ESTIMATES. Verify against your actual usage:**
|
||||
- Check actual consumption: [https://www.semrush.com/api-units/](https://www.semrush.com/api-units/)
|
||||
- Run calibration after first analysis (see below)
|
||||
- Update `API_UNIT_COSTS` in `semrushApi.js` if needed
|
||||
|
||||
### ✅ Verified from Documentation:
|
||||
- **phrase_this (Keyword Overview)**: 10 units/line
|
||||
- **Batch Keyword Overview**: 10 units/line
|
||||
- **Keyword Difficulty (phrase_kdi)**: 50 units/line
|
||||
- **domain_organic**: 10 units/line
|
||||
|
||||
### ⚠️ Estimated (needs verification):
|
||||
- domain_domains, domain_ranks, backlinks_* endpoints
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Single Analysis Cost (ESTIMATED)
|
||||
```
|
||||
1 client + 3 competitors + 1000 keyword limit = ~$0.37 per analysis
|
||||
(Verify with actual usage after first run!)
|
||||
```
|
||||
|
||||
### API Unit Costs Cheat Sheet
|
||||
```
|
||||
✅ VERIFIED:
|
||||
phrase_this: 10 units/line (Keyword Overview)
|
||||
domain_organic: 10 units/line (Organic keywords)
|
||||
phrase_kdi: 50 units/line (Keyword Difficulty)
|
||||
|
||||
⚠️ ESTIMATED (verify with actual usage):
|
||||
domain_domains: 10 units/line
|
||||
domain_ranks: 10 units/domain
|
||||
backlinks_overview: 10 units/domain
|
||||
backlinks_refdomains: 10 units/line
|
||||
```
|
||||
|
||||
## 📊 Cost Calculator
|
||||
|
||||
### By Display Limit
|
||||
| Limit | Cost/Analysis |
|
||||
|-------|---------------|
|
||||
| 100 | $0.04 |
|
||||
| 500 | $0.20 |
|
||||
| 1,000 | $0.37 |
|
||||
| 2,000 | $0.72 |
|
||||
| 5,000 | $1.77 |
|
||||
|
||||
### By Number of Analyses
|
||||
| Analyses | API Cost | Total w/ Plan |
|
||||
|----------|----------|---------------|
|
||||
| 1 | $0.37 | $500.32 |
|
||||
| 10 | $3.74 | $503.69 |
|
||||
| 100 | $37.40 | $537.35 |
|
||||
| 1,000 | $374.00 | $873.95 |
|
||||
|
||||
## 🎯 First-Time Setup: Calibrate Costs
|
||||
|
||||
### 1. Run Small Test Analysis
|
||||
```javascript
|
||||
const result = await fetchUnifiedAnalysis({
|
||||
clientUrl: 'mysite.com',
|
||||
competitors: ['competitor1.com'],
|
||||
database: 'us',
|
||||
apiKey: process.env.SEMRUSH_API_KEY,
|
||||
displayLimit: 100 // Keep it small for testing
|
||||
})
|
||||
|
||||
console.log('Estimated units:', result.apiUsage.totalUnits)
|
||||
```
|
||||
|
||||
### 2. Check Actual Usage
|
||||
Go to [https://www.semrush.com/api-units/](https://www.semrush.com/api-units/) and check API Query Log
|
||||
|
||||
### 3. Calibrate
|
||||
```javascript
|
||||
import { calibrateApiCosts } from './services/semrushApi'
|
||||
|
||||
const actualUnits = 856 // From SEMrush dashboard
|
||||
const calibration = calibrateApiCosts(actualUnits, {
|
||||
linesReturned: 85
|
||||
})
|
||||
|
||||
console.log(`Accuracy: ${calibration.accuracyPercent}%`)
|
||||
```
|
||||
|
||||
### 4. Update if Needed
|
||||
If accuracy < 90%, update `API_UNIT_COSTS` in `semrushApi.js`
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Usage Examples
|
||||
|
||||
### Basic Analysis
|
||||
```javascript
|
||||
import { fetchUnifiedAnalysis, getApiUnitReport } from './services/semrushApi'
|
||||
|
||||
const result = await fetchUnifiedAnalysis({
|
||||
clientUrl: 'mysite.com',
|
||||
competitors: ['competitor1.com', 'competitor2.com'],
|
||||
database: 'us',
|
||||
apiKey: process.env.SEMRUSH_API_KEY,
|
||||
displayLimit: 1000
|
||||
})
|
||||
|
||||
// Check API usage
|
||||
console.log('Units used:', result.apiUsage.totalUnits)
|
||||
console.log('Cost:', result.apiUsage.totalCost)
|
||||
```
|
||||
|
||||
### Estimate Before Running
|
||||
```javascript
|
||||
import { estimateApiUnits } from './services/semrushApi'
|
||||
|
||||
const estimate = estimateApiUnits({
|
||||
clientUrl: 'mysite.com',
|
||||
competitors: ['comp1.com', 'comp2.com', 'comp3.com'],
|
||||
displayLimit: 1000
|
||||
})
|
||||
|
||||
console.log(`Estimated cost: ${estimate.total.cost}`)
|
||||
console.log(`Cost per 100 analyses: ${estimate.total.per100Analyses}`)
|
||||
```
|
||||
|
||||
### Check Single Keyword Intent
|
||||
```javascript
|
||||
import { fetchPhraseIntent } from './services/semrushApi'
|
||||
|
||||
const intent = await fetchPhraseIntent('cloud security tips', 'us', apiKey)
|
||||
console.log(intent.intentLabel) // "Informational"
|
||||
console.log('Cost: $0.005 (100 units)')
|
||||
```
|
||||
|
||||
## ⚠️ Common Pitfalls
|
||||
|
||||
### 1. Use Built-in Intent Filter (More Efficient)
|
||||
```javascript
|
||||
// ✅ BEST - Intent filter built into domain_domains (no extra cost)
|
||||
const result = await fetchDomainVsDomain(...) // Intent=1 filter included
|
||||
|
||||
// ⚠️ WORKS BUT REDUNDANT - phrase_this costs 10 units/keyword ($0.50 for 1000)
|
||||
const keywords = await filterInformationalKeywords(keywords, 'us', apiKey)
|
||||
// Only use if you need to double-validate Intent values
|
||||
```
|
||||
|
||||
### 2. Don't Set Display Limit Too High
|
||||
```javascript
|
||||
// ❌ BAD - Costs $1.77 per analysis
|
||||
displayLimit: 5000
|
||||
|
||||
// ✅ GOOD - Costs $0.37 per analysis, usually sufficient
|
||||
displayLimit: 1000
|
||||
```
|
||||
|
||||
### 3. Don't Forget Business Plan Cost
|
||||
```
|
||||
API units alone: $0.37 per analysis
|
||||
But monthly cost: $499.95 + API units
|
||||
First analysis: $500.32 total
|
||||
```
|
||||
|
||||
## 📈 Optimization Tips
|
||||
|
||||
### 1. Use Intent Filter (Built-in)
|
||||
Reduces results by ~30% = saves 3,000 units per analysis
|
||||
|
||||
### 2. Batch Multiple Clients
|
||||
Run 100 analyses together instead of one-off
|
||||
|
||||
### 3. Cache Results
|
||||
Store results for 24-48 hours to avoid re-analysis
|
||||
|
||||
### 4. Optimize Display Limit
|
||||
Start with 500, increase only if needed
|
||||
|
||||
### 5. Monitor Monthly Usage
|
||||
Set alerts at 80% of purchased units
|
||||
|
||||
## 🎯 Break-Even Analysis
|
||||
|
||||
### Monthly Business Plan: $499.95
|
||||
|
||||
If you run:
|
||||
- **1 analysis/month**: $500.32 total
|
||||
- **100 analyses/month**: $537.35 total ($5.37 per analysis)
|
||||
- **1,000 analyses/month**: $873.95 total ($0.87 per analysis)
|
||||
|
||||
### Per-Analysis Cost Breakdown
|
||||
```
|
||||
Fixed: $499.95 / analyses_per_month
|
||||
Variable: ~$0.37 per analysis
|
||||
|
||||
1 analysis: $500.32 per analysis
|
||||
10 analyses: $50.37 per analysis
|
||||
100 analyses: $5.37 per analysis
|
||||
1,000 analyses: $0.87 per analysis
|
||||
```
|
||||
|
||||
## 💰 Budget Planning
|
||||
|
||||
### Conservative Budget (100 analyses/month)
|
||||
```
|
||||
Business Plan: $499.95
|
||||
API Units: $37.40
|
||||
─────────────────────────
|
||||
Monthly Total: $537.35
|
||||
Per Analysis: $5.37
|
||||
```
|
||||
|
||||
### Aggressive Budget (1,000 analyses/month)
|
||||
```
|
||||
Business Plan: $499.95
|
||||
API Units: $374.00
|
||||
─────────────────────────
|
||||
Monthly Total: $873.95
|
||||
Per Analysis: $0.87
|
||||
```
|
||||
|
||||
## 🔍 Monitoring Commands
|
||||
|
||||
### In Your Code
|
||||
```javascript
|
||||
// Get detailed report
|
||||
const report = getApiUnitReport()
|
||||
console.log(report.summary)
|
||||
|
||||
// Check specific API
|
||||
console.log(report.breakdown.domain_domains)
|
||||
```
|
||||
|
||||
### Console Output
|
||||
Look for these log messages:
|
||||
```
|
||||
[API UNITS] domain_domains: 7000 units (~$0.3500)
|
||||
[API UNIT TRACKER] Session started
|
||||
============================================================
|
||||
API UNIT USAGE REPORT
|
||||
============================================================
|
||||
```
|
||||
|
||||
## 📋 Checklist Before Production
|
||||
|
||||
- [ ] Business Plan subscription active ($499.95/month)
|
||||
- [ ] API units purchased (recommend 400,000+ units)
|
||||
- [ ] Display limit optimized (test with 100, use 1000 in prod)
|
||||
- [ ] API usage tracking enabled (automatic in fetchUnifiedAnalysis)
|
||||
- [ ] Monthly usage alerts configured
|
||||
- [ ] Caching strategy implemented
|
||||
- [ ] Error handling for "insufficient units" errors
|
||||
- [ ] Budget approved for scale (see break-even analysis)
|
||||
|
||||
## 🆘 Emergency Actions
|
||||
|
||||
### Running Out of Units
|
||||
1. Purchase more units immediately ($1 = 20,000 units)
|
||||
2. Reduce display_limit temporarily
|
||||
3. Cache results more aggressively
|
||||
4. Pause non-critical analyses
|
||||
|
||||
### Unexpected High Usage
|
||||
1. Check getApiUnitReport() output
|
||||
2. Look for phrase_this calls (100 units each!)
|
||||
3. Verify display_limit settings
|
||||
4. Review number of competitors per analysis
|
||||
|
||||
## 📞 Quick Links
|
||||
|
||||
- [Buy API Units](https://www.semrush.com/api-units/)
|
||||
- [Check Balance](https://www.semrush.com/api-units/)
|
||||
- [API Documentation](https://developer.semrush.com/)
|
||||
- [Support](https://www.semrush.com/kb/5-api)
|
||||
|
||||
---
|
||||
|
||||
**TL;DR**:
|
||||
- Need Business Plan ($499.95/month)
|
||||
- Each analysis costs ~$0.37 in API units
|
||||
- Built-in tracking shows exact usage
|
||||
- Avoid phrase_this for bulk operations
|
||||
- 100+ analyses/month = cost-effective
|
||||
|
||||
351
README.md
Normal file
351
README.md
Normal file
@ -0,0 +1,351 @@
|
||||
# 🚀 SEO Keyword Gap Analyzer
|
||||
|
||||
A simple tool that helps you find keyword opportunities by comparing your website with your competitors. Think of it as a "spy tool" that shows you which keywords your competitors are ranking for, but you're not!
|
||||
|
||||
---
|
||||
|
||||
## 🎯 What Does This Tool Do?
|
||||
|
||||
Imagine you run a website and want to know:
|
||||
- **What keywords are my competitors ranking for that I'm missing?**
|
||||
- **How much traffic could I get from those keywords?**
|
||||
- **Which keywords are easiest to rank for?**
|
||||
|
||||
This tool answers all these questions automatically! It:
|
||||
|
||||
1. ✅ Analyzes your website and up to 5 competitor websites
|
||||
2. ✅ Finds "gap keywords" (keywords competitors rank for, but you don't)
|
||||
3. ✅ Shows you important metrics like search volume, keyword difficulty, and more
|
||||
4. ✅ Filters for "informational" keywords (educational content like guides, how-tos, etc.)
|
||||
5. ✅ Displays domain metrics (authority score, traffic, backlinks)
|
||||
6. ✅ Can send results directly to Hookpilot for content planning
|
||||
|
||||
---
|
||||
|
||||
## 📋 What You Need Before Starting
|
||||
|
||||
### 1. **A Computer**
|
||||
- Works on Windows, Mac, or Linux
|
||||
|
||||
### 2. **Internet Connection**
|
||||
- You'll need internet to download the tool and fetch data from SEMrush
|
||||
|
||||
### 3. **SEMrush API Key** 🔑
|
||||
- This is like a "password" that lets you access SEMrush data
|
||||
- **How to get it:**
|
||||
1. Go to [SEMrush](https://www.semrush.com/)
|
||||
2. Sign up for an account (you'll need a paid Business plan)
|
||||
3. Go to your account settings
|
||||
4. Find "API" section and copy your API key
|
||||
- **Cost:** SEMrush Business plan costs around 499.95 per month
|
||||
|
||||
### 4. **Node.js Installed on Your Computer**
|
||||
- Node.js is a program that lets you run this tool
|
||||
- **How to check if you have it:**
|
||||
- Open "Terminal" (Mac) or "Command Prompt" (Windows)
|
||||
- Type: `node --version`
|
||||
- If you see a version number (like v18.0.0), you're good!
|
||||
- **If you don't have it:**
|
||||
- Go to [nodejs.org](https://nodejs.org/)
|
||||
- Download and install the "LTS" version (it's the recommended one)
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ How to Install and Run the Tool
|
||||
|
||||
Follow these simple steps:
|
||||
|
||||
### Step 1: Download the Project
|
||||
1. Open "Terminal" (Mac) or "Command Prompt" (Windows)
|
||||
2. Navigate to where you want to save the project (for example, your Documents folder)
|
||||
3. If you have the project as a zip file, unzip it
|
||||
4. If you received it from GitHub, type:
|
||||
```
|
||||
git clone [project-url]
|
||||
```
|
||||
|
||||
### Step 2: Open the Project Folder
|
||||
1. In Terminal/Command Prompt, type:
|
||||
```
|
||||
cd semrush-vinita
|
||||
```
|
||||
(This moves you into the project folder)
|
||||
|
||||
### Step 3: Install Required Components
|
||||
1. Type this command and press Enter:
|
||||
```
|
||||
npm install
|
||||
```
|
||||
2. Wait for it to finish (it might take 2-3 minutes)
|
||||
3. This downloads all the necessary "helpers" the tool needs to work
|
||||
|
||||
### Step 4: Start the Tool
|
||||
1. Type this command:
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
2. You should see a message like:
|
||||
```
|
||||
Local: http://localhost:5173/
|
||||
```
|
||||
3. Open your web browser (Chrome, Firefox, Safari, etc.)
|
||||
4. Go to: `http://localhost:5173/`
|
||||
5. You should see the tool's homepage! 🎉
|
||||
|
||||
---
|
||||
|
||||
## 📖 How to Use the Tool
|
||||
|
||||
### Step 1: Fill Out the Form
|
||||
|
||||
You'll see a form with several fields:
|
||||
|
||||
#### **Client Website URL** (Required)
|
||||
- Type your website address here
|
||||
- Example: `mywebsite.com` or `www.mywebsite.com`
|
||||
- Don't include `http://` or `https://`
|
||||
|
||||
#### **Competitor Websites** (At least 1 required)
|
||||
- Type your competitors' website addresses
|
||||
- You can add up to 5 competitors
|
||||
- Example: `competitor1.com`, `competitor2.com`
|
||||
|
||||
#### **Search Database**
|
||||
- Choose your country/region
|
||||
- Most people use `us` for United States
|
||||
- Other options: `uk`, `ca`, `in`, etc.
|
||||
|
||||
#### **Display Limit**
|
||||
- How many keywords do you want to analyze?
|
||||
- **Default: 10** (recommended for beginners)
|
||||
- Higher numbers = more keywords but costs more API units
|
||||
- Range: 1 to 10,000
|
||||
|
||||
#### **SEMrush API Key** 🔑
|
||||
- Paste your SEMrush API key here
|
||||
- This is the "password" you got from SEMrush
|
||||
|
||||
### Step 2: Click "Start Analysis"
|
||||
|
||||
1. Click the big blue button
|
||||
2. The tool will show a progress bar
|
||||
3. Wait for the analysis to complete (usually 10-30 seconds)
|
||||
|
||||
### Step 3: Review the Results
|
||||
|
||||
The tool shows you several sections:
|
||||
|
||||
#### **📊 API Usage Report** (Top)
|
||||
- **Total Units:** How many SEMrush API units were used
|
||||
- **Duration:** How long the analysis took
|
||||
- This helps you track your SEMrush usage
|
||||
|
||||
#### **📈 Domain Metrics** (Cards)
|
||||
Shows information about each website analyzed:
|
||||
- **Authority Score:** How "trustworthy" the website is (0-100, higher is better)
|
||||
- **Organic Traffic:** How many visitors from Google
|
||||
- **Organic Keywords:** How many keywords the site ranks for
|
||||
- **Total Backlinks:** How many other websites link to this site
|
||||
- **Referring Domains:** How many unique websites link to this site
|
||||
|
||||
**💡 Tip:** Click on any domain card to see detailed information:
|
||||
- Top 10 Organic Keywords
|
||||
- Top 10 Referring Domains
|
||||
|
||||
#### **🎯 Informational Gap Keywords** (Main Results)
|
||||
This is the most important section! It shows keywords that:
|
||||
- ✅ Your competitors rank for
|
||||
- ❌ You don't rank for yet
|
||||
- ✅ Are "informational" (educational content)
|
||||
|
||||
For each keyword, you see:
|
||||
- **Keyword:** The actual search term
|
||||
- **Intent:** Should always be "Informational"
|
||||
- **Search Volume:** How many people search for this per month (higher = more potential traffic)
|
||||
- **Keyword Difficulty:** How hard it is to rank (0-100, lower = easier)
|
||||
- **Competitors Found In:** Which competitors rank for this keyword
|
||||
|
||||
#### **Actions You Can Take:**
|
||||
1. **Sort:** Click column headers to sort by volume, difficulty, etc.
|
||||
2. **Export:** Download results as CSV file for Excel/Google Sheets
|
||||
3. **Send to Hookpilot:** Send the data to Hookpilot for content planning
|
||||
|
||||
---
|
||||
|
||||
## 💰 Understanding API Costs
|
||||
|
||||
SEMrush charges based on "API units." Here's what the tool uses:
|
||||
|
||||
| Action | Cost per Item |
|
||||
|--------|---------------|
|
||||
| Keyword gap analysis | 80 units per keyword |
|
||||
| Domain metrics | 10 units per domain |
|
||||
| Keyword validation | 10 units per keyword |
|
||||
| Backlink overview | 40 units per domain |
|
||||
| Referring domains | 40 units per domain (×10) |
|
||||
| Organic keywords | 10 units per domain (×10) |
|
||||
|
||||
**Example Cost (with 10 keywords, 2 competitors):**
|
||||
- Keyword gap: 10 × 80 = 800 units
|
||||
- Keyword validation: 10 × 10 = 100 units
|
||||
- Domain metrics: 3 × 10 = 30 units
|
||||
- Backlinks: 3 × 40 = 120 units
|
||||
- Referring domains: 3 × 10 × 40 = 1,200 units
|
||||
- Organic keywords: 3 × 10 × 10 = 300 units
|
||||
- **Total: ~2,550 units** (about 0.13 from your monthly SEMrush allowance)
|
||||
|
||||
**💡 Tip:** Start with a small display limit (10-20) to test the tool before running large analyses.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Troubleshooting
|
||||
|
||||
### Problem: "Cannot find module" error
|
||||
**Solution:**
|
||||
1. Make sure you ran `npm install` first
|
||||
2. Try running it again
|
||||
|
||||
### Problem: "Port already in use" error
|
||||
**Solution:**
|
||||
1. Another program is using the same port
|
||||
2. Close the other program or change the port in `vite.config.js`
|
||||
|
||||
### Problem: "ERROR 50 :: NOTHING FOUND"
|
||||
**Possible causes:**
|
||||
1. **Empty Client URL:** Make sure you entered your website address
|
||||
2. **Invalid domain:** Check that website addresses are correct (no http://, no trailing slashes)
|
||||
3. **No keywords found:** Try increasing the display limit
|
||||
4. **Wrong API key:** Make sure your SEMrush API key is correct
|
||||
|
||||
### Problem: "Network Error" or "Failed to fetch"
|
||||
**Solution:**
|
||||
1. Check your internet connection
|
||||
2. Make sure your SEMrush API key is valid
|
||||
3. Verify your SEMrush subscription is active
|
||||
|
||||
### Problem: Tool is running slow
|
||||
**Solution:**
|
||||
1. Reduce the display limit (try 10-20 instead of 100+)
|
||||
2. Analyze fewer competitors
|
||||
3. Close other browser tabs
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Tips for Best Results
|
||||
|
||||
### For Beginners:
|
||||
1. **Start small:** Use display limit of 10-20 for your first analysis
|
||||
2. **Test with 1-2 competitors:** Don't analyze 5 competitors right away
|
||||
3. **Check your API usage:** The tool shows how many units you used
|
||||
|
||||
### For Advanced Users:
|
||||
1. **Higher display limits:** Use 50-100 for comprehensive analysis
|
||||
2. **Multiple competitors:** Compare against 3-5 competitors for better insights
|
||||
3. **Export data:** Download CSV files for deeper analysis in Excel
|
||||
|
||||
### Interpreting Results:
|
||||
- **High Volume + Low Difficulty = Great Opportunity!** 🎯
|
||||
- These keywords are searched a lot but easier to rank for
|
||||
|
||||
- **Low Volume + High Difficulty = Skip for now** ⚠️
|
||||
- Not worth the effort for beginners
|
||||
|
||||
- **Focus on Informational Keywords:** 📚
|
||||
- These are perfect for blog posts, guides, and tutorials
|
||||
- Usually easier to rank than commercial keywords
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Need Help?
|
||||
|
||||
If you're stuck or confused:
|
||||
|
||||
1. **Check the Console Logs:**
|
||||
- In your browser, press F12 (Windows) or Cmd+Option+I (Mac)
|
||||
- Click on "Console" tab
|
||||
- You'll see detailed logs of what's happening
|
||||
|
||||
2. **Common Questions:**
|
||||
- **Q: How do I stop the tool?**
|
||||
- A: Press `Ctrl+C` in the Terminal/Command Prompt window
|
||||
|
||||
- **Q: Can I run multiple analyses at once?**
|
||||
- A: No, wait for one to finish before starting another
|
||||
|
||||
- **Q: Where are my results saved?**
|
||||
- A: Results are shown on screen. Use "Export CSV" to save them
|
||||
|
||||
- **Q: How often can I use this?**
|
||||
- A: As much as you want, but watch your SEMrush API unit balance
|
||||
|
||||
---
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
semrush-vinita/
|
||||
├── src/
|
||||
│ ├── components/ # Visual components (forms, results display)
|
||||
│ ├── services/ # Logic for talking to SEMrush API
|
||||
│ ├── App.jsx # Main application
|
||||
│ └── main.jsx # Entry point
|
||||
├── package.json # List of required components
|
||||
├── vite.config.js # Configuration file
|
||||
└── README.md # This file!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Privacy & Security
|
||||
|
||||
- Your SEMrush API key is stored only in your browser (not on any server)
|
||||
- All data analysis happens in your browser
|
||||
- No data is collected or stored by this tool
|
||||
- Only sent to Hookpilot if you click "Send to Hookpilot" button
|
||||
|
||||
---
|
||||
|
||||
## 📝 Quick Start Checklist
|
||||
|
||||
Before your first analysis, make sure you have:
|
||||
|
||||
- [ ] Node.js installed on your computer
|
||||
- [ ] Downloaded/cloned this project
|
||||
- [ ] Ran `npm install` successfully
|
||||
- [ ] Started the tool with `npm run dev`
|
||||
- [ ] Opened `http://localhost:5173/` in your browser
|
||||
- [ ] Have your SEMrush API key ready
|
||||
- [ ] Know your website URL and competitor URLs
|
||||
|
||||
---
|
||||
|
||||
## 🎉 You're All Set!
|
||||
|
||||
Start analyzing keywords and finding content opportunities! Remember:
|
||||
|
||||
1. **Start small** (10 keywords, 1-2 competitors)
|
||||
2. **Watch your API usage** (shown in the results)
|
||||
3. **Export valuable data** (CSV download button)
|
||||
4. **Focus on high-volume, low-difficulty keywords** for quick wins
|
||||
|
||||
Happy keyword hunting! 🔍✨
|
||||
|
||||
---
|
||||
|
||||
## 📞 Technical Support
|
||||
|
||||
For SEMrush API issues:
|
||||
- Visit: [SEMrush API Documentation](https://developer.semrush.com/api)
|
||||
- Email: mail@semrush.com
|
||||
- Phone: +1 (800) 815-9959 (12:00 PM - 5:00 PM EST/EDT, Monday - Friday)
|
||||
|
||||
For tool-specific issues:
|
||||
- Check the troubleshooting section above
|
||||
- Review console logs in your browser (F12)
|
||||
- Make sure all prerequisites are installed correctly
|
||||
|
||||
---
|
||||
|
||||
**Version:** 1.0
|
||||
**Last Updated:** November 2025
|
||||
**Made with ❤️ for SEO professionals and content creators**
|
||||
201
SIMPLIFIED_FLOW.md
Normal file
201
SIMPLIFIED_FLOW.md
Normal file
@ -0,0 +1,201 @@
|
||||
# ✅ SIMPLIFIED FLOW - Informational Keywords Only
|
||||
|
||||
## 🎯 What Changed
|
||||
|
||||
### 1. **Fixed Domain Format** (Critical!)
|
||||
**Before (WRONG):**
|
||||
```
|
||||
*|or|competitor1|+|or|competitor2|-|or|client
|
||||
```
|
||||
|
||||
**After (CORRECT per [SEMrush Tutorial](https://developer.semrush.com/api/basics/api-tutorials/analytics-api/#analyze-keyword-gaps-among-competitors/)):**
|
||||
```
|
||||
-|or|client|+|or|competitor1|+|or|competitor2
|
||||
```
|
||||
|
||||
### 2. **Removed Unnecessary Complexity**
|
||||
- ❌ Removed "At Least One Competitor" tab
|
||||
- ❌ Removed "All Competitors Have" tab
|
||||
- ✅ **Just show: Informational Gap Keywords (Intent=1)**
|
||||
|
||||
### 3. **Added phrase_this Validation**
|
||||
- ✅ Checkbox enabled by default
|
||||
- ✅ Each keyword hits: `https://api.semrush.com/?type=phrase_this&phrase=KEYWORD&export_columns=In`
|
||||
- ✅ Cost: 10 units per keyword
|
||||
- ✅ Shows in Network tab
|
||||
|
||||
### 4. **Simplified Output**
|
||||
- ✅ Domain Metrics (kept)
|
||||
- ✅ Informational Gap Keywords only
|
||||
- ✅ API Usage Report with phrase_this breakdown
|
||||
- ❌ No complex tabs or multiple views
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Complete Flow (Simplified)
|
||||
|
||||
```
|
||||
USER FILLS FORM
|
||||
├─ Client: evendigit.com
|
||||
├─ Competitor 1: infidigit.com
|
||||
├─ Competitor 2: ignitevisibility.com
|
||||
├─ Display Limit: 100
|
||||
└─ ✅ Validate with phrase_this (checked)
|
||||
↓
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ STEP 1: domain_domains API Call │
|
||||
└──────────────────────────────────────────────┘
|
||||
GET /api/semrush/?type=domain_domains
|
||||
&domains=-|or|evendigit.com|+|or|infidigit.com|+|or|ignitevisibility.com
|
||||
&display_limit=100
|
||||
&export_columns=Ph,P0,P1,P2,In,Nq,Kd,Co,Cp
|
||||
|
||||
Response: 68 keywords (all intents)
|
||||
Cost: 680 units
|
||||
↓
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ STEP 2: phrase_this API Validation │
|
||||
│ (EACH keyword validated) │
|
||||
└──────────────────────────────────────────────┘
|
||||
For EACH of the 68 keywords:
|
||||
|
||||
1. GET /api/semrush/?type=phrase_this&phrase=seo+tips&export_columns=In
|
||||
Response: Intent=1 → ✅ KEEP
|
||||
|
||||
2. GET /api/semrush/?type=phrase_this&phrase=buy+seo+tools&export_columns=In
|
||||
Response: Intent=3 → ❌ REMOVE
|
||||
|
||||
3. GET /api/semrush/?type=phrase_this&phrase=seo+guide&export_columns=In
|
||||
Response: Intent=1 → ✅ KEEP
|
||||
|
||||
... (65 more API calls)
|
||||
|
||||
Total phrase_this calls: 68
|
||||
Cost: 68 × 10 = 680 units
|
||||
Result: 46 keywords with Intent=1
|
||||
↓
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ STEP 3: Fetch Domain Metrics │
|
||||
└──────────────────────────────────────────────┘
|
||||
Parallel calls for 3 domains:
|
||||
- domain_ranks (3 calls × 10 = 30 units)
|
||||
- backlinks_overview (3 calls × 10 = 30 units)
|
||||
- backlinks_refdomains (3 calls × 100 = 300 units)
|
||||
|
||||
Cost: 360 units
|
||||
↓
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ STEP 4: Display Results │
|
||||
└──────────────────────────────────────────────┘
|
||||
📊 API Usage Report
|
||||
├─ Total Units: 1,720
|
||||
├─ Cost: $0.086
|
||||
└─ phrase_this: 68 calls, 680 units
|
||||
|
||||
🎯 Informational Gap Keywords: 46
|
||||
├─ Keywords competitors have, client doesn't
|
||||
└─ All verified as Intent=1 (Informational)
|
||||
|
||||
📍 Domain Metrics (3 domains)
|
||||
├─ Client: evendigit.com
|
||||
│ ├─ Authority Score: 29
|
||||
│ ├─ Organic Traffic: 73
|
||||
│ └─ Organic Keywords: 641
|
||||
├─ Competitor 1: infidigit.com
|
||||
└─ Competitor 2: ignitevisibility.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Console Output Example
|
||||
|
||||
When you run with `validateWithPhrasethis=true`, you'll see:
|
||||
|
||||
```bash
|
||||
══════════════════════════════════════════════════════════
|
||||
🔍 PHRASE_THIS API VALIDATION - Intent Verification
|
||||
══════════════════════════════════════════════════════════
|
||||
📊 Keywords to validate: 68
|
||||
💰 Cost: 68 keywords × 10 units = 680 units
|
||||
💵 Estimated: $0.0340
|
||||
🔍 API: https://api.semrush.com/?type=phrase_this&phrase=KEYWORD&export_columns=In
|
||||
══════════════════════════════════════════════════════════
|
||||
|
||||
[BATCH 1/7] Validating keywords 1-10...
|
||||
1. "seo tips" (Volume: 5400)
|
||||
2. "seo guide" (Volume: 3200)
|
||||
3. "content marketing" (Volume: 2100)
|
||||
...
|
||||
|
||||
[API CALL] phrase_this for "seo tips"
|
||||
URL: /api/semrush/?type=phrase_this&phrase=seo+tips&database=us&export_columns=In&key=***
|
||||
Cost: 10 units
|
||||
Response: Intent = 1
|
||||
[RESULT] "seo tips" - Intent: Informational (1) ✅ INFORMATIONAL
|
||||
|
||||
✅ "seo tips" → Intent=1 (Informational) - KEPT
|
||||
❌ "buy seo tools" → Intent=3 (Transactional) - FILTERED OUT
|
||||
✅ "seo guide" → Intent=1 (Informational) - KEPT
|
||||
|
||||
[BATCH 1] ✅ Complete: 7/10 keywords are Informational
|
||||
|
||||
... (batches 2-7)
|
||||
|
||||
══════════════════════════════════════════════════════════
|
||||
📊 VALIDATION COMPLETE
|
||||
══════════════════════════════════════════════════════════
|
||||
📥 Keywords received: 68
|
||||
🔍 API calls made: 68
|
||||
✅ Keywords validated (Intent=1): 46
|
||||
❌ Keywords filtered out: 22
|
||||
💰 Total units consumed: 680 units
|
||||
💵 Total cost: $0.0340
|
||||
══════════════════════════════════════════════════════════
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Network Tab - You'll See
|
||||
|
||||
```
|
||||
Name Status Type Size
|
||||
─────────────────────────────────────────────────────────────────
|
||||
domain_domains?key=...&domains=-|or|... 200 xhr 2.1kB
|
||||
phrase_this?phrase=seo+tips&... 200 xhr 15B
|
||||
phrase_this?phrase=buy+seo+tools&... 200 xhr 15B
|
||||
phrase_this?phrase=seo+guide&... 200 xhr 15B
|
||||
phrase_this?phrase=content+marketing&... 200 xhr 15B
|
||||
... (64 more phrase_this calls)
|
||||
domain_ranks?domain=evendigit.com&... 200 xhr 156B
|
||||
domain_ranks?domain=infidigit.com&... 200 xhr 156B
|
||||
domain_ranks?domain=ignitevisibility.com&... 200 xhr 156B
|
||||
backlinks_overview?target=evendigit.com&... 200 xhr 245B
|
||||
... (more backlink calls)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ What You Get
|
||||
|
||||
1. **API Usage Report** - Shows total units, cost, and phrase_this breakdown
|
||||
2. **Informational Gap Keywords** - ONLY Intent=1 keywords (simplified!)
|
||||
3. **Domain Metrics** - Authority Score, Traffic, Keywords, Backlinks
|
||||
4. **No unnecessary tabs** - Clean, single view
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Ready to Test!
|
||||
|
||||
1. **Refresh browser** (Cmd+Shift+R)
|
||||
2. **Fill form:**
|
||||
- Client: `evendigit.com`
|
||||
- Competitor 1: `infidigit.com`
|
||||
- Competitor 2: `ignitevisibility.com`
|
||||
- Display Limit: `100`
|
||||
- ✅ **Validate with phrase_this** (checked by default)
|
||||
3. **Open Network tab**
|
||||
4. **Click "Run Unified Analysis"**
|
||||
5. **Watch phrase_this calls appear!** 🎉
|
||||
|
||||
Each keyword will hit the phrase_this API and you'll see it in the Network tab!
|
||||
|
||||
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>SEMrush Blog Keyword Research</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
1909
package-lock.json
generated
Normal file
1909
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
package.json
Normal file
20
package.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "semrush-keyword-research",
|
||||
"version": "1.0.0",
|
||||
"description": "SEMrush Blog Keyword Research Tool",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"axios": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
412
semrussh.postman_collection.json
Normal file
412
semrussh.postman_collection.json
Normal file
@ -0,0 +1,412 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "11ba170b-37d1-4619-b7e8-328ac6420564",
|
||||
"name": "semrussh",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json",
|
||||
"_exporter_id": "39591404"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "phrase this",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "https://api.semrush.com/?type=phrase_this&key=61b5d6f509d57c3b7671d431ac5d7306&phrase=cloud security tips&database=us&export_columns=In",
|
||||
"protocol": "https",
|
||||
"host": [
|
||||
"api",
|
||||
"semrush",
|
||||
"com"
|
||||
],
|
||||
"path": [
|
||||
""
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "type",
|
||||
"value": "phrase_this"
|
||||
},
|
||||
{
|
||||
"key": "key",
|
||||
"value": "61b5d6f509d57c3b7671d431ac5d7306"
|
||||
},
|
||||
{
|
||||
"key": "phrase",
|
||||
"value": "cloud security tips"
|
||||
},
|
||||
{
|
||||
"key": "database",
|
||||
"value": "us"
|
||||
},
|
||||
{
|
||||
"key": "export_columns",
|
||||
"value": "In"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "competitor",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "https://api.semrush.com/?type=domain_domains&key=61b5d6f509d57c3b7671d431ac5d7306&database=us&domains=%2A%7Cor%7Cdigitalocean.com%7C%2B%7Cor%7Ccloud.google.com%7C%2D%7Cor%7Caws.amazon.com&display_sort=nq_desc&export_columns=Ph%2CP0%2CP1%2CP2%2CNq%2CKd%2CCo%2CCp&display_limit=100",
|
||||
"protocol": "https",
|
||||
"host": [
|
||||
"api",
|
||||
"semrush",
|
||||
"com"
|
||||
],
|
||||
"path": [
|
||||
""
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "type",
|
||||
"value": "domain_domains"
|
||||
},
|
||||
{
|
||||
"key": "key",
|
||||
"value": "61b5d6f509d57c3b7671d431ac5d7306"
|
||||
},
|
||||
{
|
||||
"key": "database",
|
||||
"value": "us"
|
||||
},
|
||||
{
|
||||
"key": "domains",
|
||||
"value": "%2A%7Cor%7Cdigitalocean.com%7C%2B%7Cor%7Ccloud.google.com%7C%2D%7Cor%7Caws.amazon.com"
|
||||
},
|
||||
{
|
||||
"key": "display_sort",
|
||||
"value": "nq_desc"
|
||||
},
|
||||
{
|
||||
"key": "export_columns",
|
||||
"value": "Ph%2CP0%2CP1%2CP2%2CNq%2CKd%2CCo%2CCp"
|
||||
},
|
||||
{
|
||||
"key": "display_limit",
|
||||
"value": "100"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "test",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "hapi/semrush/?type=domain_domains&key=61b5d6f509d57c3b7671d431ac5d7306&database=us&domains=*%7Cor%7Cdigitalocean.com%7Cor%7Ccloud.google.com%7C%2B%7Cor%7Caws.amazon.com%7C-%7Cor%7Ccloud.google.com&display_sort=nq_desc&export_columns=Ph,P0,P1,P2,Nq,Kd,Co,Cp&display_limit=100",
|
||||
"host": [
|
||||
"hapi"
|
||||
],
|
||||
"path": [
|
||||
"semrush",
|
||||
""
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "type",
|
||||
"value": "domain_domains"
|
||||
},
|
||||
{
|
||||
"key": "key",
|
||||
"value": "61b5d6f509d57c3b7671d431ac5d7306"
|
||||
},
|
||||
{
|
||||
"key": "database",
|
||||
"value": "us"
|
||||
},
|
||||
{
|
||||
"key": "domains",
|
||||
"value": "*%7Cor%7Cdigitalocean.com%7Cor%7Ccloud.google.com%7C%2B%7Cor%7Caws.amazon.com%7C-%7Cor%7Ccloud.google.com"
|
||||
},
|
||||
{
|
||||
"key": "display_sort",
|
||||
"value": "nq_desc"
|
||||
},
|
||||
{
|
||||
"key": "export_columns",
|
||||
"value": "Ph,P0,P1,P2,Nq,Kd,Co,Cp"
|
||||
},
|
||||
{
|
||||
"key": "display_limit",
|
||||
"value": "100"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "domain-rank",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "https://api.semrush.com/?type=domain_ranks&key=61b5d6f509d57c3b7671d431ac5d7306&domain=tech4bizsolutions.com&export_columns=Db,Dn,Rk,Or,Ot,Oc,Ad,At,Ac&database=in",
|
||||
"protocol": "https",
|
||||
"host": [
|
||||
"api",
|
||||
"semrush",
|
||||
"com"
|
||||
],
|
||||
"path": [
|
||||
""
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "type",
|
||||
"value": "domain_ranks"
|
||||
},
|
||||
{
|
||||
"key": "key",
|
||||
"value": "61b5d6f509d57c3b7671d431ac5d7306"
|
||||
},
|
||||
{
|
||||
"key": "domain",
|
||||
"value": "tech4bizsolutions.com"
|
||||
},
|
||||
{
|
||||
"key": "export_columns",
|
||||
"value": "Db,Dn,Rk,Or,Ot,Oc,Ad,At,Ac"
|
||||
},
|
||||
{
|
||||
"key": "database",
|
||||
"value": "in"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "organic-keyword",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "https://api.semrush.com/?type=domain_organic&key=61b5d6f509d57c3b7671d431ac5d7306&domain=tech4bizsolutions.com&database=us&export_columns=Ph,Po,Nq,Cp,Co,Kd&display_limit=100",
|
||||
"protocol": "https",
|
||||
"host": [
|
||||
"api",
|
||||
"semrush",
|
||||
"com"
|
||||
],
|
||||
"path": [
|
||||
""
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "type",
|
||||
"value": "domain_organic"
|
||||
},
|
||||
{
|
||||
"key": "key",
|
||||
"value": "61b5d6f509d57c3b7671d431ac5d7306"
|
||||
},
|
||||
{
|
||||
"key": "domain",
|
||||
"value": "tech4bizsolutions.com"
|
||||
},
|
||||
{
|
||||
"key": "database",
|
||||
"value": "us"
|
||||
},
|
||||
{
|
||||
"key": "export_columns",
|
||||
"value": "Ph,Po,Nq,Cp,Co,Kd"
|
||||
},
|
||||
{
|
||||
"key": "display_limit",
|
||||
"value": "100"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "authority-backlink",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "https://api.semrush.com/analytics/v1/?key=61b5d6f509d57c3b7671d431ac5d7306&type=backlinks_overview&target=tech4bizsolutions.com&target_type=root_domain&export_columns=ascore,total,domains_num,urls_num,ips_num",
|
||||
"protocol": "https",
|
||||
"host": [
|
||||
"api",
|
||||
"semrush",
|
||||
"com"
|
||||
],
|
||||
"path": [
|
||||
"analytics",
|
||||
"v1",
|
||||
""
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "key",
|
||||
"value": "61b5d6f509d57c3b7671d431ac5d7306"
|
||||
},
|
||||
{
|
||||
"key": "type",
|
||||
"value": "backlinks_overview"
|
||||
},
|
||||
{
|
||||
"key": "target",
|
||||
"value": "tech4bizsolutions.com"
|
||||
},
|
||||
{
|
||||
"key": "target_type",
|
||||
"value": "root_domain"
|
||||
},
|
||||
{
|
||||
"key": "export_columns",
|
||||
"value": "ascore,total,domains_num,urls_num,ips_num"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "refer-domaIn",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "https://api.semrush.com/analytics/v1/?key=61b5d6f509d57c3b7671d431ac5d7306&type=backlinks_refdomains&target=tech4bizsolutions.com&target_type=root_domain&export_columns=domain_ascore,domain,backlinks_num",
|
||||
"protocol": "https",
|
||||
"host": [
|
||||
"api",
|
||||
"semrush",
|
||||
"com"
|
||||
],
|
||||
"path": [
|
||||
"analytics",
|
||||
"v1",
|
||||
""
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "key",
|
||||
"value": "61b5d6f509d57c3b7671d431ac5d7306"
|
||||
},
|
||||
{
|
||||
"key": "type",
|
||||
"value": "backlinks_refdomains"
|
||||
},
|
||||
{
|
||||
"key": "target",
|
||||
"value": "tech4bizsolutions.com"
|
||||
},
|
||||
{
|
||||
"key": "target_type",
|
||||
"value": "root_domain"
|
||||
},
|
||||
{
|
||||
"key": "export_columns",
|
||||
"value": "domain_ascore,domain,backlinks_num"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "backlink list",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "https://api.semrush.com/analytics/v1/?key=61b5d6f509d57c3b7671d431ac5d7306&type=backlinks&target=tech4bizsolutions.com&target_type=root_domain&export_columns=page_ascore,source_url,target_url,anchor&display_limit=200",
|
||||
"protocol": "https",
|
||||
"host": [
|
||||
"api",
|
||||
"semrush",
|
||||
"com"
|
||||
],
|
||||
"path": [
|
||||
"analytics",
|
||||
"v1",
|
||||
""
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "key",
|
||||
"value": "61b5d6f509d57c3b7671d431ac5d7306"
|
||||
},
|
||||
{
|
||||
"key": "type",
|
||||
"value": "backlinks"
|
||||
},
|
||||
{
|
||||
"key": "target",
|
||||
"value": "tech4bizsolutions.com"
|
||||
},
|
||||
{
|
||||
"key": "target_type",
|
||||
"value": "root_domain"
|
||||
},
|
||||
{
|
||||
"key": "export_columns",
|
||||
"value": "page_ascore,source_url,target_url,anchor"
|
||||
},
|
||||
{
|
||||
"key": "display_limit",
|
||||
"value": "200"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "traffic-summary",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "https://api.semrush.com/analytics/ta/api/v3/toppages?key=61b5d6f509d57c3b7671d431ac5d7306&target=tech4bizsolutions.com&export_columns=page,traffic,traffic_share",
|
||||
"protocol": "https",
|
||||
"host": [
|
||||
"api",
|
||||
"semrush",
|
||||
"com"
|
||||
],
|
||||
"path": [
|
||||
"analytics",
|
||||
"ta",
|
||||
"api",
|
||||
"v3",
|
||||
"toppages"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "key",
|
||||
"value": "61b5d6f509d57c3b7671d431ac5d7306"
|
||||
},
|
||||
{
|
||||
"key": "target",
|
||||
"value": "tech4bizsolutions.com"
|
||||
},
|
||||
{
|
||||
"key": "export_columns",
|
||||
"value": "page,traffic,traffic_share"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
}
|
||||
181
src/App.css
Normal file
181
src/App.css
Normal file
@ -0,0 +1,181 @@
|
||||
.app {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
text-align: center;
|
||||
color: white;
|
||||
margin-bottom: 40px;
|
||||
animation: fadeInDown 0.6s ease-out;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 40px 20px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.app-header h1 {
|
||||
font-size: clamp(1.75rem, 5vw, 2.5rem);
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.app-header p {
|
||||
font-size: clamp(0.9rem, 2.5vw, 1.1rem);
|
||||
opacity: 0.95;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.mode-toggle {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin: 2rem 0;
|
||||
max-width: 1000px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.mode-btn {
|
||||
padding: 1rem 1.5rem;
|
||||
border: 2px solid #e2e8f0;
|
||||
background: white;
|
||||
color: #4a5568;
|
||||
border-radius: 12px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
min-height: 60px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.mode-btn:hover:not(:disabled) {
|
||||
border-color: #667eea;
|
||||
color: #667eea;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 12px rgba(102, 126, 234, 0.15);
|
||||
}
|
||||
|
||||
.mode-btn.active {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-color: #667eea;
|
||||
color: white;
|
||||
box-shadow: 0 6px 12px rgba(102, 126, 234, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.mode-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.mode-btn.active:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
margin-bottom: 30px;
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
text-align: center;
|
||||
color: #555;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #fee;
|
||||
color: #c33;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
box-shadow: 0 4px 12px rgba(204, 51, 51, 0.1);
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
.error-message svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.app {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.mode-toggle {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.mode-btn {
|
||||
padding: 0.875rem 1.25rem;
|
||||
font-size: 0.9rem;
|
||||
min-height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.mode-btn {
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
min-height: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
98
src/App.jsx
Normal file
98
src/App.jsx
Normal file
@ -0,0 +1,98 @@
|
||||
import React, { useState } from 'react'
|
||||
import './App.css'
|
||||
import KeywordForm from './components/KeywordForm'
|
||||
import UnifiedAnalysis from './components/UnifiedAnalysis'
|
||||
import {
|
||||
fetchUnifiedAnalysis
|
||||
} from './services/semrushApi'
|
||||
|
||||
function App() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [unifiedAnalysis, setUnifiedAnalysis] = useState(null)
|
||||
const [error, setError] = useState(null)
|
||||
const [progress, setProgress] = useState({ current: 0, total: 0 })
|
||||
|
||||
const handleSearch = async (formData) => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
setUnifiedAnalysis(null)
|
||||
|
||||
// Calculate total steps based on display limit
|
||||
const numDomains = 1 + formData.competitors.filter(c => c && c.trim()).length
|
||||
const displayLimit = formData.displayLimit || 10
|
||||
|
||||
// Progress steps:
|
||||
// 1. domain_domains API call (1 step)
|
||||
// 2. phrase_this validation (displayLimit steps)
|
||||
// 3. Domain metrics (numDomains steps)
|
||||
const totalSteps = 1 + displayLimit + numDomains
|
||||
let currentStep = 0
|
||||
|
||||
setProgress({ current: 0, total: totalSteps })
|
||||
|
||||
try {
|
||||
console.log('Starting unified analysis...')
|
||||
|
||||
// Fetch unified analysis
|
||||
setProgress({ current: 1, total: totalSteps }) // Starting domain_domains
|
||||
const analysisData = await fetchUnifiedAnalysis(formData)
|
||||
|
||||
setUnifiedAnalysis(analysisData)
|
||||
setProgress({ current: totalSteps, total: totalSteps })
|
||||
|
||||
} catch (err) {
|
||||
setError(err.message || 'An error occurred while fetching data')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<div className="app-header">
|
||||
<h1>🚀 Unified SEO Analysis Tool</h1>
|
||||
<p>Comprehensive metrics + Keyword research + Industry detection + Hookpilot integration</p>
|
||||
</div>
|
||||
|
||||
<KeywordForm
|
||||
onSearch={handleSearch}
|
||||
loading={loading}
|
||||
/>
|
||||
|
||||
{loading && (
|
||||
<div className="progress-container">
|
||||
<div className="progress-bar">
|
||||
<div
|
||||
className="progress-fill"
|
||||
style={{
|
||||
width: `${progress.total > 0 ? (progress.current / progress.total) * 100 : 0}%`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p className="progress-text">
|
||||
{progress.total > 0
|
||||
? `Analyzing ${progress.total} operations (Keywords validation + Domain metrics)...`
|
||||
: 'Initializing analysis...'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="error-message">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" fill="currentColor"/>
|
||||
</svg>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{unifiedAnalysis && (
|
||||
<UnifiedAnalysis
|
||||
analysisData={unifiedAnalysis}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
471
src/components/ComprehensiveMetrics.css
Normal file
471
src/components/ComprehensiveMetrics.css
Normal file
@ -0,0 +1,471 @@
|
||||
.comprehensive-metrics {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.metrics-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-content h2 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.header-content p {
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.hookpilot-status {
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.hookpilot-status.success {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
color: #4caf50;
|
||||
border: 1px solid rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.hookpilot-status.error {
|
||||
background: rgba(244, 67, 54, 0.2);
|
||||
color: #f44336;
|
||||
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||
}
|
||||
|
||||
/* Table Container */
|
||||
.metrics-table-container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e5e7eb;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.metrics-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.metrics-table th {
|
||||
background: #f9fafb;
|
||||
padding: 16px 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.metrics-table td {
|
||||
padding: 16px 12px;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.metrics-table tr:hover {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.metrics-table tr.error-row {
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.metrics-table tr.error-row:hover {
|
||||
background: #fee2e2;
|
||||
}
|
||||
|
||||
/* Cell Styles */
|
||||
.actions-cell {
|
||||
width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.expand-button {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.expand-button:hover {
|
||||
background: #2563eb;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.domain-cell {
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.client-badge {
|
||||
margin-left: 8px;
|
||||
padding: 2px 8px;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.metric-cell {
|
||||
text-align: right;
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.keywords-cell, .categories-cell {
|
||||
max-width: 300px;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.stage-cell {
|
||||
text-align: center;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.stage-badge {
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* Keywords List */
|
||||
.keywords-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.keyword-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 6px 8px;
|
||||
background: #f3f4f6;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #3b82f6;
|
||||
}
|
||||
|
||||
.keyword-phrase {
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.keyword-metrics {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.more-keywords {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
/* Categories List */
|
||||
.categories-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 6px 8px;
|
||||
background: #f0f9ff;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #0ea5e9;
|
||||
}
|
||||
|
||||
.category-url {
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.category-traffic {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
color: #9ca3af;
|
||||
font-style: italic;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Expanded Row */
|
||||
.expanded-row td {
|
||||
padding: 0;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.detailed-view {
|
||||
padding: 24px;
|
||||
background: white;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.detailed-sections {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.section h4 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 16px;
|
||||
color: #1f2937;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: #f3f4f6;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
/* Detailed Keywords */
|
||||
.keywords-detailed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.keyword-detailed {
|
||||
padding: 12px;
|
||||
background: #f3f4f6;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #3b82f6;
|
||||
}
|
||||
|
||||
.keyword-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.keyword-rank {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.keyword-text {
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.keyword-stats {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* Detailed Categories */
|
||||
.categories-detailed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.category-detailed {
|
||||
padding: 12px;
|
||||
background: #f0f9ff;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #0ea5e9;
|
||||
}
|
||||
|
||||
.category-url {
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
font-size: 13px;
|
||||
margin-bottom: 6px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.category-stats {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* Classification Legend */
|
||||
.classification-legend {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.classification-legend h4 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 18px;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.legend-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.legend-item strong {
|
||||
color: #1f2937;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1200px) {
|
||||
.detailed-sections {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.metrics-table th,
|
||||
.metrics-table td {
|
||||
padding: 12px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.comprehensive-metrics {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.detailed-sections {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.metrics-table {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.metrics-table th,
|
||||
.metrics-table td {
|
||||
padding: 8px 6px;
|
||||
}
|
||||
|
||||
.keywords-cell, .categories-cell {
|
||||
max-width: 200px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.keyword-item, .category-item {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.keyword-phrase, .category-url {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.keyword-metrics, .category-traffic {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.metrics-table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.metrics-table {
|
||||
min-width: 800px;
|
||||
}
|
||||
|
||||
.legend-items {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
padding: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
261
src/components/ComprehensiveMetrics.jsx
Normal file
261
src/components/ComprehensiveMetrics.jsx
Normal file
@ -0,0 +1,261 @@
|
||||
import React, { useState } from 'react'
|
||||
import './ComprehensiveMetrics.css'
|
||||
|
||||
function ComprehensiveMetrics({ metricsData, hookpilotStatus }) {
|
||||
const [expandedDomain, setExpandedDomain] = useState(null)
|
||||
|
||||
if (!metricsData || metricsData.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const toggleDomainDetails = (domain) => {
|
||||
setExpandedDomain(expandedDomain === domain ? null : domain)
|
||||
}
|
||||
|
||||
const getStageColor = (stage) => {
|
||||
switch (stage) {
|
||||
case 'NEW': return '#ff6b6b'
|
||||
case 'GROWING': return '#ffa726'
|
||||
case 'ESTABLISHED': return '#66bb6a'
|
||||
case 'ERROR': return '#9e9e9e'
|
||||
default: return '#9e9e9e'
|
||||
}
|
||||
}
|
||||
|
||||
const getStageIcon = (stage) => {
|
||||
switch (stage) {
|
||||
case 'NEW': return '🌱'
|
||||
case 'GROWING': return '📈'
|
||||
case 'ESTABLISHED': return '🏆'
|
||||
case 'ERROR': return '❌'
|
||||
default: return '❓'
|
||||
}
|
||||
}
|
||||
|
||||
const renderTopKeywords = (keywords) => {
|
||||
if (!keywords || keywords.length === 0) {
|
||||
return <span className="no-data">No keywords data</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="keywords-list">
|
||||
{keywords.slice(0, 5).map((keyword, index) => (
|
||||
<div key={index} className="keyword-item">
|
||||
<span className="keyword-phrase">{keyword.phrase}</span>
|
||||
<span className="keyword-metrics">
|
||||
SV: {keyword.searchVolume?.toLocaleString() || 'N/A'} |
|
||||
KD: {keyword.keywordDifficulty || 'N/A'} |
|
||||
Pos: {keyword.position || 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{keywords.length > 5 && (
|
||||
<div className="more-keywords">+{keywords.length - 5} more keywords</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderTopCategories = (categories) => {
|
||||
if (!categories || categories.length === 0) {
|
||||
return <span className="no-data">No categories data</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="categories-list">
|
||||
{categories.map((category, index) => (
|
||||
<div key={index} className="category-item">
|
||||
<span className="category-url">{category.url}</span>
|
||||
<span className="category-traffic">{category.traffic?.toLocaleString() || 0} visits</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderDetailedView = (domainData) => {
|
||||
if (!domainData || domainData.error) return null
|
||||
|
||||
return (
|
||||
<div className="detailed-view">
|
||||
<div className="detailed-sections">
|
||||
<div className="section">
|
||||
<h4>📊 Complete Metrics</h4>
|
||||
<div className="metrics-grid">
|
||||
<div className="metric-item">
|
||||
<span className="metric-label">Authority Score:</span>
|
||||
<span className="metric-value">{domainData.authorityScore}</span>
|
||||
</div>
|
||||
<div className="metric-item">
|
||||
<span className="metric-label">Organic Traffic:</span>
|
||||
<span className="metric-value">{domainData.organicTraffic?.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="metric-item">
|
||||
<span className="metric-label">Organic Keywords:</span>
|
||||
<span className="metric-value">{domainData.organicKeywords?.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="metric-item">
|
||||
<span className="metric-label">Paid Traffic:</span>
|
||||
<span className="metric-value">{domainData.paidTraffic?.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="metric-item">
|
||||
<span className="metric-label">Paid Keywords:</span>
|
||||
<span className="metric-value">{domainData.paidKeywords?.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="metric-item">
|
||||
<span className="metric-label">Total Backlinks:</span>
|
||||
<span className="metric-value">{domainData.totalBacklinks?.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h4>🔍 Top 10 Keywords</h4>
|
||||
<div className="keywords-detailed">
|
||||
{domainData.top10Keywords?.map((keyword, index) => (
|
||||
<div key={index} className="keyword-detailed">
|
||||
<div className="keyword-main">
|
||||
<span className="keyword-rank">#{index + 1}</span>
|
||||
<span className="keyword-text">{keyword.phrase}</span>
|
||||
</div>
|
||||
<div className="keyword-stats">
|
||||
<span>SV: {keyword.searchVolume?.toLocaleString() || 'N/A'}</span>
|
||||
<span>KD: {keyword.keywordDifficulty || 'N/A'}</span>
|
||||
<span>Pos: {keyword.position || 'N/A'}</span>
|
||||
<span>CPC: ${keyword.cpc?.toFixed(2) || 'N/A'}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h4>📄 Top Categories/Pages</h4>
|
||||
<div className="categories-detailed">
|
||||
{domainData.topCategories?.map((category, index) => (
|
||||
<div key={index} className="category-detailed">
|
||||
<div className="category-url">{category.url}</div>
|
||||
<div className="category-stats">
|
||||
<span>Traffic: {category.traffic?.toLocaleString() || 0}</span>
|
||||
<span>Keywords: {category.positionCount || 1}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="comprehensive-metrics">
|
||||
<div className="metrics-header">
|
||||
<div className="header-content">
|
||||
<h2>📊 Comprehensive Domain Metrics</h2>
|
||||
<p>Complete SEO analysis with all required metrics for SEO maturity assessment</p>
|
||||
</div>
|
||||
{hookpilotStatus && (
|
||||
<div className={`hookpilot-status ${hookpilotStatus.success ? 'success' : 'error'}`}>
|
||||
{hookpilotStatus.success ? '✅ Sent to Hookpilot' : '❌ Hookpilot Error'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="metrics-table-container">
|
||||
<table className="metrics-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Actions</th>
|
||||
<th>Domain</th>
|
||||
<th>Authority Score</th>
|
||||
<th>Organic Keywords</th>
|
||||
<th>Top 10 Keywords</th>
|
||||
<th>Referring Domains</th>
|
||||
<th>Monthly Traffic</th>
|
||||
<th>Top Categories</th>
|
||||
<th>Stage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{metricsData.map((domainData, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<tr className={domainData.error ? 'error-row' : ''}>
|
||||
<td className="actions-cell">
|
||||
{!domainData.error && (
|
||||
<button
|
||||
className="expand-button"
|
||||
onClick={() => toggleDomainDetails(domainData.domain)}
|
||||
title="View detailed metrics"
|
||||
>
|
||||
{expandedDomain === domainData.domain ? '▼' : '▶'}
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
<td className="domain-cell">
|
||||
<strong>{domainData.domain}</strong>
|
||||
{index === 0 && <span className="client-badge">Client</span>}
|
||||
</td>
|
||||
<td className="metric-cell">
|
||||
{domainData.error ? 'Error' : domainData.authorityScore?.toLocaleString()}
|
||||
</td>
|
||||
<td className="metric-cell">
|
||||
{domainData.error ? 'Error' : domainData.organicKeywords?.toLocaleString()}
|
||||
</td>
|
||||
<td className="keywords-cell">
|
||||
{domainData.error ? 'Error' : renderTopKeywords(domainData.top10Keywords)}
|
||||
</td>
|
||||
<td className="metric-cell">
|
||||
{domainData.error ? 'Error' : domainData.referringDomains?.toLocaleString()}
|
||||
</td>
|
||||
<td className="metric-cell">
|
||||
{domainData.error ? 'Error' : domainData.monthlyTraffic?.toLocaleString()}
|
||||
</td>
|
||||
<td className="categories-cell">
|
||||
{domainData.error ? 'Error' : renderTopCategories(domainData.topCategories)}
|
||||
</td>
|
||||
<td className="stage-cell">
|
||||
<span
|
||||
className="stage-badge"
|
||||
style={{ backgroundColor: getStageColor(domainData.stage) }}
|
||||
>
|
||||
{getStageIcon(domainData.stage)} {domainData.stage}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{expandedDomain === domainData.domain && !domainData.error && (
|
||||
<tr className="expanded-row">
|
||||
<td colSpan="9">
|
||||
{renderDetailedView(domainData)}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Classification Legend */}
|
||||
<div className="classification-legend">
|
||||
<h4>📋 SEO Maturity Classification Criteria</h4>
|
||||
<div className="legend-items">
|
||||
<div className="legend-item">
|
||||
<span className="legend-color" style={{ backgroundColor: '#ff6b6b' }}></span>
|
||||
<strong>NEW:</strong> AS < 20 OR Organic KW < 500 OR < 10 Ref Domains
|
||||
</div>
|
||||
<div className="legend-item">
|
||||
<span className="legend-color" style={{ backgroundColor: '#ffa726' }}></span>
|
||||
<strong>GROWING:</strong> AS 20–40 OR Organic KW 500–3000
|
||||
</div>
|
||||
<div className="legend-item">
|
||||
<span className="legend-color" style={{ backgroundColor: '#66bb6a' }}></span>
|
||||
<strong>ESTABLISHED:</strong> AS > 40 OR Organic KW > 3000 OR > 100 Ref Domains
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ComprehensiveMetrics
|
||||
169
src/components/KeywordForm.css
Normal file
169
src/components/KeywordForm.css
Normal file
@ -0,0 +1,169 @@
|
||||
.keyword-form {
|
||||
margin-bottom: 30px;
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background: white;
|
||||
padding: clamp(20px, 4vw, 40px);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.form-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 15px 50px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.form-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-section h2 {
|
||||
font-size: clamp(1.25rem, 3vw, 1.5rem);
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #667eea;
|
||||
}
|
||||
|
||||
.form-section h3 {
|
||||
font-size: clamp(1rem, 2.5vw, 1.1rem);
|
||||
color: #555;
|
||||
margin-bottom: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.competitors-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.form-group input:disabled,
|
||||
.form-group select:disabled {
|
||||
background: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
padding: 16px 24px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.submit-btn:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.submit-btn:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.submit-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-card {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 345px) {
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
font-size: 0.9rem;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
font-size: 1rem;
|
||||
padding: 14px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
212
src/components/KeywordForm.jsx
Normal file
212
src/components/KeywordForm.jsx
Normal file
@ -0,0 +1,212 @@
|
||||
import React, { useState } from 'react'
|
||||
import './KeywordForm.css'
|
||||
|
||||
const DATABASES = [
|
||||
{ value: 'us', label: 'United States' },
|
||||
{ value: 'uk', label: 'United Kingdom' },
|
||||
{ value: 'ca', label: 'Canada' },
|
||||
{ value: 'au', label: 'Australia' },
|
||||
{ value: 'de', label: 'Germany' },
|
||||
{ value: 'fr', label: 'France' },
|
||||
{ value: 'es', label: 'Spain' },
|
||||
{ value: 'it', label: 'Italy' },
|
||||
{ value: 'br', label: 'Brazil' },
|
||||
{ value: 'in', label: 'India' },
|
||||
]
|
||||
|
||||
function KeywordForm({ onSearch, loading }) {
|
||||
const [formData, setFormData] = useState({
|
||||
clientUrl: '',
|
||||
competitor1: '',
|
||||
competitor2: '',
|
||||
competitor3: '',
|
||||
competitor4: '',
|
||||
database: 'us',
|
||||
displayLimit: 10,
|
||||
apiKey: '61b5d6f509d57c3b7671d431ac5d7306'
|
||||
})
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!formData.clientUrl.trim()) {
|
||||
alert('Please enter your client website URL')
|
||||
return
|
||||
}
|
||||
|
||||
const competitors = [
|
||||
formData.competitor1,
|
||||
formData.competitor2,
|
||||
formData.competitor3,
|
||||
formData.competitor4
|
||||
].filter(c => c.trim())
|
||||
|
||||
if (competitors.length === 0) {
|
||||
alert('Please enter at least one competitor URL')
|
||||
return
|
||||
}
|
||||
|
||||
onSearch({
|
||||
...formData,
|
||||
competitors
|
||||
})
|
||||
}
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="keyword-form" onSubmit={handleSubmit}>
|
||||
<div className="form-card">
|
||||
<div className="form-section">
|
||||
<h2>Website Information</h2>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="clientUrl">
|
||||
Client Website URL *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="clientUrl"
|
||||
name="clientUrl"
|
||||
placeholder="evendigit.com"
|
||||
value={formData.clientUrl}
|
||||
onChange={handleChange}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="competitors-section">
|
||||
<h3>Competitor URLs (1-4)</h3>
|
||||
<div className="form-group">
|
||||
<label htmlFor="competitor1">Competitor 1 *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="competitor1"
|
||||
name="competitor1"
|
||||
placeholder="techmagnate.com"
|
||||
value={formData.competitor1}
|
||||
onChange={handleChange}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="competitor2">Competitor 2</label>
|
||||
<input
|
||||
type="text"
|
||||
id="competitor2"
|
||||
name="competitor2"
|
||||
placeholder="persuasionexperience.com"
|
||||
value={formData.competitor2}
|
||||
onChange={handleChange}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="competitor3">Competitor 3</label>
|
||||
<input
|
||||
type="text"
|
||||
id="competitor3"
|
||||
name="competitor3"
|
||||
placeholder="infidigit.com"
|
||||
value={formData.competitor3}
|
||||
onChange={handleChange}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="competitor4">Competitor 4</label>
|
||||
<input
|
||||
type="text"
|
||||
id="competitor4"
|
||||
name="competitor4"
|
||||
placeholder="ignitevisibility.com"
|
||||
value={formData.competitor4}
|
||||
onChange={handleChange}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
<h2>Search Parameters</h2>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="database">Target Location / Market *</label>
|
||||
<select
|
||||
id="database"
|
||||
name="database"
|
||||
value={formData.database}
|
||||
onChange={handleChange}
|
||||
disabled={loading}
|
||||
>
|
||||
{DATABASES.map(db => (
|
||||
<option key={db.value} value={db.value}>
|
||||
{db.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="displayLimit">Display Limit *</label>
|
||||
<input
|
||||
type="number"
|
||||
id="displayLimit"
|
||||
name="displayLimit"
|
||||
min="1"
|
||||
max="10000"
|
||||
value={formData.displayLimit}
|
||||
onChange={handleChange}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="apiKey">SEMrush API Key *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="apiKey"
|
||||
name="apiKey"
|
||||
placeholder="Your SEMrush API key"
|
||||
value={formData.apiKey}
|
||||
onChange={handleChange}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" className="submit-btn" disabled={loading}>
|
||||
{loading ? (
|
||||
<>
|
||||
<span className="spinner"></span>
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M8.5 14L4.5 10L5.91 8.59L8.5 11.17L14.59 5.08L16 6.5L8.5 14Z" fill="currentColor"/>
|
||||
</svg>
|
||||
Run Unified Analysis
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default KeywordForm
|
||||
|
||||
230
src/components/KeywordResults.css
Normal file
230
src/components/KeywordResults.css
Normal file
@ -0,0 +1,230 @@
|
||||
.keyword-results {
|
||||
background: white;
|
||||
padding: clamp(20px, 4vw, 40px);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
||||
animation: slideUp 0.5s ease-out;
|
||||
}
|
||||
|
||||
.results-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.results-title h2 {
|
||||
font-size: clamp(1.25rem, 3vw, 1.75rem);
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.results-count {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.export-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
background: white;
|
||||
color: #667eea;
|
||||
border: 2px solid #667eea;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.export-btn:hover {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.keywords-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 600px;
|
||||
}
|
||||
|
||||
.keywords-table thead {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.keywords-table th {
|
||||
padding: 16px 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.keywords-table th.sortable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.keywords-table th.sortable:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.sort-indicator {
|
||||
margin-left: 5px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.keywords-table tbody tr {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.keywords-table tbody tr:hover {
|
||||
background: #f8f9ff;
|
||||
}
|
||||
|
||||
.keywords-table tbody tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.keywords-table td {
|
||||
padding: 16px 12px;
|
||||
color: #555;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.keyword-phrase {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
max-width: 300px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.keyword-volume {
|
||||
font-weight: 500;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.difficulty-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.difficulty-badge {
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
text-align: center;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.difficulty-easy {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.difficulty-medium {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.difficulty-hard {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.difficulty-unknown {
|
||||
background: #e2e3e5;
|
||||
color: #383d41;
|
||||
}
|
||||
|
||||
.keyword-cpc {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.intent-badge {
|
||||
display: inline-block;
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
padding: 4px 12px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.results-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.keywords-table th,
|
||||
.keywords-table td {
|
||||
padding: 12px 8px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.keyword-phrase {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 345px) {
|
||||
.keywords-table {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.keywords-table th,
|
||||
.keywords-table td {
|
||||
padding: 10px 6px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.results-count {
|
||||
font-size: 0.75rem;
|
||||
padding: 3px 10px;
|
||||
}
|
||||
|
||||
.export-btn {
|
||||
padding: 8px 16px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
158
src/components/KeywordResults.jsx
Normal file
158
src/components/KeywordResults.jsx
Normal file
@ -0,0 +1,158 @@
|
||||
import React, { useState } from 'react'
|
||||
import './KeywordResults.css'
|
||||
|
||||
function KeywordResults({ keywords }) {
|
||||
const [sortBy, setSortBy] = useState('volume')
|
||||
const [sortOrder, setSortOrder] = useState('desc')
|
||||
|
||||
const sortedKeywords = [...keywords].sort((a, b) => {
|
||||
let aVal, bVal
|
||||
|
||||
switch (sortBy) {
|
||||
case 'volume':
|
||||
aVal = parseInt(a.volume) || 0
|
||||
bVal = parseInt(b.volume) || 0
|
||||
break
|
||||
case 'difficulty':
|
||||
aVal = parseInt(a.difficulty) || 0
|
||||
bVal = parseInt(b.difficulty) || 0
|
||||
break
|
||||
case 'cpc':
|
||||
aVal = parseFloat(a.cpc) || 0
|
||||
bVal = parseFloat(b.cpc) || 0
|
||||
break
|
||||
case 'phrase':
|
||||
return sortOrder === 'asc'
|
||||
? a.phrase.localeCompare(b.phrase)
|
||||
: b.phrase.localeCompare(a.phrase)
|
||||
default:
|
||||
aVal = 0
|
||||
bVal = 0
|
||||
}
|
||||
|
||||
return sortOrder === 'asc' ? aVal - bVal : bVal - aVal
|
||||
})
|
||||
|
||||
const handleSort = (field) => {
|
||||
if (sortBy === field) {
|
||||
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
|
||||
} else {
|
||||
setSortBy(field)
|
||||
setSortOrder('desc')
|
||||
}
|
||||
}
|
||||
|
||||
const exportToCSV = () => {
|
||||
const headers = ['Keyword', 'Search Volume', 'Keyword Difficulty', 'CPC', 'Competition', 'Intent']
|
||||
const rows = keywords.map(kw => [
|
||||
kw.phrase,
|
||||
kw.volume || 'N/A',
|
||||
kw.difficulty || 'N/A',
|
||||
kw.cpc || 'N/A',
|
||||
kw.competition || 'N/A',
|
||||
kw.intent || 'Informational'
|
||||
])
|
||||
|
||||
const csv = [
|
||||
headers.join(','),
|
||||
...rows.map(row => row.map(cell => `"${cell}"`).join(','))
|
||||
].join('\n')
|
||||
|
||||
const blob = new Blob([csv], { type: 'text/csv' })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `semrush-keywords-${new Date().toISOString().split('T')[0]}.csv`
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="keyword-results">
|
||||
<div className="results-header">
|
||||
<div className="results-title">
|
||||
<h2>Informational Keywords Found</h2>
|
||||
<span className="results-count">{keywords.length} keywords</span>
|
||||
</div>
|
||||
<button onClick={exportToCSV} className="export-btn">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M14 8L10 12L6 8H9V4H11V8H14ZM4 14V16H16V14H4Z" fill="currentColor"/>
|
||||
</svg>
|
||||
Export CSV
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="table-container">
|
||||
<table className="keywords-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th onClick={() => handleSort('phrase')} className="sortable">
|
||||
Keyword
|
||||
{sortBy === 'phrase' && (
|
||||
<span className="sort-indicator">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||
)}
|
||||
</th>
|
||||
<th onClick={() => handleSort('volume')} className="sortable">
|
||||
Search Volume
|
||||
{sortBy === 'volume' && (
|
||||
<span className="sort-indicator">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||
)}
|
||||
</th>
|
||||
<th onClick={() => handleSort('difficulty')} className="sortable">
|
||||
Difficulty
|
||||
{sortBy === 'difficulty' && (
|
||||
<span className="sort-indicator">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||
)}
|
||||
</th>
|
||||
<th onClick={() => handleSort('cpc')} className="sortable">
|
||||
CPC
|
||||
{sortBy === 'cpc' && (
|
||||
<span className="sort-indicator">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||
)}
|
||||
</th>
|
||||
<th>Competition</th>
|
||||
<th>Intent</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedKeywords.map((keyword, index) => (
|
||||
<tr key={index} className="keyword-row">
|
||||
<td className="keyword-phrase">{keyword.phrase}</td>
|
||||
<td className="keyword-volume">
|
||||
{keyword.volume ? parseInt(keyword.volume).toLocaleString() : 'N/A'}
|
||||
</td>
|
||||
<td className="keyword-difficulty">
|
||||
<div className="difficulty-cell">
|
||||
<span className={`difficulty-badge difficulty-${getDifficultyLevel(keyword.difficulty)}`}>
|
||||
{keyword.difficulty || 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="keyword-cpc">
|
||||
{keyword.cpc ? `${keyword.cpc}` : 'N/A'}
|
||||
</td>
|
||||
<td className="keyword-competition">
|
||||
{keyword.competition || 'N/A'}
|
||||
</td>
|
||||
<td className="keyword-intent">
|
||||
<span className="intent-badge">Informational</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function getDifficultyLevel(difficulty) {
|
||||
const diff = parseInt(difficulty)
|
||||
if (isNaN(diff)) return 'unknown'
|
||||
if (diff < 30) return 'easy'
|
||||
if (diff < 60) return 'medium'
|
||||
return 'hard'
|
||||
}
|
||||
|
||||
export default KeywordResults
|
||||
|
||||
881
src/components/SEOMaturityAnalysis.css
Normal file
881
src/components/SEOMaturityAnalysis.css
Normal file
@ -0,0 +1,881 @@
|
||||
.seo-maturity-analysis {
|
||||
margin: 2rem 0;
|
||||
padding: 2rem;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Metrics Cards */
|
||||
.metrics-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.metric-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.metric-card.authority {
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
|
||||
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3);
|
||||
}
|
||||
|
||||
.metric-card.keywords {
|
||||
background: linear-gradient(135deg, #4ecdc4 0%, #44a08d 100%);
|
||||
box-shadow: 0 4px 12px rgba(78, 205, 196, 0.3);
|
||||
}
|
||||
|
||||
.metric-card.traffic {
|
||||
background: linear-gradient(135deg, #45b7d1 0%, #96c93d 100%);
|
||||
box-shadow: 0 4px 12px rgba(69, 183, 209, 0.3);
|
||||
}
|
||||
|
||||
.metric-card.backlinks {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
box-shadow: 0 4px 12px rgba(240, 147, 251, 0.3);
|
||||
}
|
||||
|
||||
.metric-icon {
|
||||
font-size: 2.5rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.metric-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.metric-subtitle {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.metric-stage {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 12px;
|
||||
display: inline-block;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.analysis-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.analysis-header h2 {
|
||||
color: #2d3748;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.analysis-header p {
|
||||
color: #718096;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.summary-section {
|
||||
background: #f7fafc;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2rem;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.summary-section h3 {
|
||||
color: #2d3748;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.summary-content {
|
||||
color: #4a5568;
|
||||
line-height: 1.6;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.results-table-container {
|
||||
overflow-x: auto;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.seo-maturity-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.seo-maturity-table th {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 1.25rem 1rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.seo-maturity-table th:first-child {
|
||||
border-top-left-radius: 12px;
|
||||
}
|
||||
|
||||
.seo-maturity-table th:last-child {
|
||||
border-top-right-radius: 12px;
|
||||
}
|
||||
|
||||
.seo-maturity-table td {
|
||||
padding: 1.25rem 1rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
vertical-align: top;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.seo-maturity-table tr:hover {
|
||||
background: linear-gradient(90deg, #f7fafc 0%, #edf2f7 100%);
|
||||
}
|
||||
|
||||
.seo-maturity-table tr.error-row {
|
||||
background: linear-gradient(90deg, #fed7d7 0%, #feb2b2 100%);
|
||||
color: #c53030;
|
||||
}
|
||||
|
||||
.seo-maturity-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.domain-cell {
|
||||
font-weight: 600;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.client-badge {
|
||||
display: inline-block;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
margin-left: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.metric-cell {
|
||||
text-align: right;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.keywords-cell {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.top-keywords {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.keyword-tag {
|
||||
background: #e2e8f0;
|
||||
color: #4a5568;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.more-keywords {
|
||||
color: #718096;
|
||||
font-size: 0.8rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.categories-cell {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.top-categories {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
background: #f7fafc;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
color: #4a5568;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.more-categories {
|
||||
color: #718096;
|
||||
font-size: 0.8rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.stage-cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stage-badge {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.classification-legend {
|
||||
background: #f7fafc;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.classification-legend h4 {
|
||||
color: #2d3748;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.legend-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: #4a5568;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Actions Cell */
|
||||
.actions-cell {
|
||||
text-align: center;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.expand-button {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 4px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.expand-button:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 8px rgba(102, 126, 234, 0.5);
|
||||
}
|
||||
|
||||
/* Expanded Row */
|
||||
.expanded-row td {
|
||||
padding: 2rem !important;
|
||||
background: #f7fafc;
|
||||
border-top: 2px solid #667eea;
|
||||
border-bottom: 2px solid #667eea;
|
||||
}
|
||||
|
||||
/* Detailed Data Container */
|
||||
.detailed-data-container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* Detailed Tabs */
|
||||
.detailed-tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #718096;
|
||||
cursor: pointer;
|
||||
border-radius: 8px 8px 0 0;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-button:hover {
|
||||
background: #edf2f7;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Detailed Tables */
|
||||
.detailed-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.detailed-table thead {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.detailed-table th {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.detailed-table td {
|
||||
padding: 0.875rem 1rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
font-size: 0.9rem;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.detailed-table tbody tr:hover {
|
||||
background: #f7fafc;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.detailed-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Keywords Table */
|
||||
.keyword-cell {
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.position-cell {
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.volume-cell {
|
||||
text-align: right;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.cpc-cell {
|
||||
text-align: right;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #48bb78;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.competition-cell {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
background: #e2e8f0;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #48bb78 0%, #38a169 100%);
|
||||
transition: width 0.3s ease;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.difficulty-cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.difficulty-badge {
|
||||
display: inline-block;
|
||||
padding: 0.35rem 0.75rem;
|
||||
border-radius: 16px;
|
||||
font-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.difficulty-badge.difficulty-0 {
|
||||
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
|
||||
}
|
||||
|
||||
.difficulty-badge.difficulty-1 {
|
||||
background: linear-gradient(135deg, #ecc94b 0%, #d69e2e 100%);
|
||||
}
|
||||
|
||||
.difficulty-badge.difficulty-2 {
|
||||
background: linear-gradient(135deg, #ed8936 0%, #dd6b20 100%);
|
||||
}
|
||||
|
||||
.difficulty-badge.difficulty-3,
|
||||
.difficulty-badge.difficulty-4 {
|
||||
background: linear-gradient(135deg, #f56565 0%, #e53e3e 100%);
|
||||
}
|
||||
|
||||
/* Backlinks Summary */
|
||||
.backlinks-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1rem;
|
||||
background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.summary-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.8rem;
|
||||
color: #718096;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #2d3748;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* Backlinks Table */
|
||||
.ascore-cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ascore-badge {
|
||||
display: inline-block;
|
||||
padding: 0.35rem 0.75rem;
|
||||
border-radius: 16px;
|
||||
font-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.ascore-badge.ascore-0 {
|
||||
background: linear-gradient(135deg, #9e9e9e 0%, #757575 100%);
|
||||
}
|
||||
|
||||
.ascore-badge.ascore-1 {
|
||||
background: linear-gradient(135deg, #4ecdc4 0%, #44a08d 100%);
|
||||
}
|
||||
|
||||
.ascore-badge.ascore-2 {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.ascore-badge.ascore-3,
|
||||
.ascore-badge.ascore-4 {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
|
||||
.url-cell {
|
||||
max-width: 350px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.url-cell a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.url-cell a:hover {
|
||||
color: #764ba2;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.anchor-cell {
|
||||
font-style: italic;
|
||||
color: #718096;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
/* Top Pages Table */
|
||||
.traffic-cell {
|
||||
text-align: right;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
color: #48bb78;
|
||||
}
|
||||
|
||||
.count-cell {
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
/* No Data State */
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 2rem !important;
|
||||
color: #a0aec0;
|
||||
font-style: italic;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Scrollable Tables */
|
||||
.keywords-table-container,
|
||||
.backlinks-table-container,
|
||||
.pages-table-container {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
.keywords-table-container::-webkit-scrollbar,
|
||||
.backlinks-table-container::-webkit-scrollbar,
|
||||
.pages-table-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.keywords-table-container::-webkit-scrollbar-track,
|
||||
.backlinks-table-container::-webkit-scrollbar-track,
|
||||
.pages-table-container::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.keywords-table-container::-webkit-scrollbar-thumb,
|
||||
.backlinks-table-container::-webkit-scrollbar-thumb,
|
||||
.pages-table-container::-webkit-scrollbar-thumb {
|
||||
background: #667eea;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.keywords-table-container::-webkit-scrollbar-thumb:hover,
|
||||
.backlinks-table-container::-webkit-scrollbar-thumb:hover,
|
||||
.pages-table-container::-webkit-scrollbar-thumb:hover {
|
||||
background: #764ba2;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.seo-maturity-analysis {
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.metrics-cards {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.metric-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.seo-maturity-table {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.seo-maturity-table th,
|
||||
.seo-maturity-table td {
|
||||
padding: 0.75rem 0.5rem;
|
||||
}
|
||||
|
||||
.legend-items {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.detailed-tabs {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.detailed-data-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.detailed-table th,
|
||||
.detailed-table td {
|
||||
padding: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.backlinks-summary {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.url-cell {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.keyword-cell {
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.expanded-row td {
|
||||
padding: 1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.metrics-cards {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
padding: 0.75rem;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.metric-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.seo-maturity-table th,
|
||||
.seo-maturity-table td {
|
||||
padding: 0.5rem 0.25rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.stage-badge {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.keyword-tag {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.15rem 0.35rem;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.15rem 0.35rem;
|
||||
}
|
||||
|
||||
.detailed-tabs {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.detailed-data-container {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.detailed-table th,
|
||||
.detailed-table td {
|
||||
padding: 0.5rem 0.25rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.backlinks-summary {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.url-cell {
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.keyword-cell {
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.difficulty-badge,
|
||||
.ascore-badge {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.expand-button {
|
||||
padding: 0.35rem 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.expanded-row td {
|
||||
padding: 0.75rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Extra responsive for 345px minimum */
|
||||
@media (max-width: 345px) {
|
||||
.seo-maturity-analysis {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.detailed-data-container {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
padding: 0.4rem 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.detailed-table th,
|
||||
.detailed-table td {
|
||||
padding: 0.4rem 0.2rem;
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.url-cell {
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.keyword-cell {
|
||||
max-width: 80px;
|
||||
}
|
||||
}
|
||||
406
src/components/SEOMaturityAnalysis.jsx
Normal file
406
src/components/SEOMaturityAnalysis.jsx
Normal file
@ -0,0 +1,406 @@
|
||||
import React, { useState } from 'react'
|
||||
import './SEOMaturityAnalysis.css'
|
||||
|
||||
function SEOMaturityAnalysis({ analysisResults }) {
|
||||
const [expandedDomain, setExpandedDomain] = useState(null)
|
||||
const [activeTab, setActiveTab] = useState('keywords')
|
||||
|
||||
if (!analysisResults || analysisResults.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const clientData = analysisResults[0] // First domain is client
|
||||
const competitorsData = analysisResults.slice(1) // Rest are competitors
|
||||
|
||||
const generateSummary = () => {
|
||||
if (!clientData || clientData.error) return 'Unable to generate summary due to data errors.'
|
||||
|
||||
const clientStage = clientData.stage
|
||||
const avgCompetitorAS = competitorsData
|
||||
.filter(c => !c.error)
|
||||
.reduce((sum, c) => sum + c.authorityScore, 0) / competitorsData.filter(c => !c.error).length || 0
|
||||
|
||||
const avgCompetitorKW = competitorsData
|
||||
.filter(c => !c.error)
|
||||
.reduce((sum, c) => sum + c.organicKeywords, 0) / competitorsData.filter(c => !c.error).length || 0
|
||||
|
||||
const avgCompetitorRD = competitorsData
|
||||
.filter(c => !c.error)
|
||||
.reduce((sum, c) => sum + c.referringDomains, 0) / competitorsData.filter(c => !c.error).length || 0
|
||||
|
||||
let summary = `Your domain (${clientData.domain}) is classified as ${clientStage} with an Authority Score of ${clientData.authorityScore}, ${clientData.organicKeywords.toLocaleString()} organic keywords, and ${clientData.referringDomains.toLocaleString()} referring domains. `
|
||||
|
||||
if (competitorsData.length > 0) {
|
||||
summary += `Compared to your competitors (average: ${Math.round(avgCompetitorAS)} AS, ${Math.round(avgCompetitorKW).toLocaleString()} keywords, ${Math.round(avgCompetitorRD).toLocaleString()} referring domains), `
|
||||
|
||||
if (clientData.authorityScore > avgCompetitorAS) {
|
||||
summary += `you have a stronger domain authority. `
|
||||
} else {
|
||||
summary += `you have room to improve your domain authority. `
|
||||
}
|
||||
|
||||
if (clientData.organicKeywords > avgCompetitorKW) {
|
||||
summary += `You rank for more organic keywords than the average competitor. `
|
||||
} else {
|
||||
summary += `You rank for fewer organic keywords than the average competitor. `
|
||||
}
|
||||
|
||||
if (clientData.referringDomains > avgCompetitorRD) {
|
||||
summary += `Your backlink profile is stronger with more referring domains.`
|
||||
} else {
|
||||
summary += `Your backlink profile could be strengthened with more referring domains.`
|
||||
}
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
const getMetricCards = () => {
|
||||
if (!clientData || clientData.error) return null
|
||||
|
||||
return (
|
||||
<div className="metrics-cards">
|
||||
<div className="metric-card authority">
|
||||
<div className="metric-icon">🏆</div>
|
||||
<div className="metric-content">
|
||||
<div className="metric-value">{clientData.authorityScore}</div>
|
||||
<div className="metric-label">Authority Score</div>
|
||||
<div className="metric-stage">{clientData.stage}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="metric-card keywords">
|
||||
<div className="metric-icon">🔍</div>
|
||||
<div className="metric-content">
|
||||
<div className="metric-value">{clientData.organicKeywords.toLocaleString()}</div>
|
||||
<div className="metric-label">Organic Keywords</div>
|
||||
<div className="metric-subtitle">Ranking positions</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="metric-card traffic">
|
||||
<div className="metric-icon">📈</div>
|
||||
<div className="metric-content">
|
||||
<div className="metric-value">{clientData.organicTraffic.toLocaleString()}</div>
|
||||
<div className="metric-label">Organic Traffic</div>
|
||||
<div className="metric-subtitle">Monthly visits</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="metric-card backlinks">
|
||||
<div className="metric-icon">🔗</div>
|
||||
<div className="metric-content">
|
||||
<div className="metric-value">{clientData.referringDomains.toLocaleString()}</div>
|
||||
<div className="metric-label">Referring Domains</div>
|
||||
<div className="metric-subtitle">Backlink sources</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const getStageColor = (stage) => {
|
||||
switch (stage) {
|
||||
case 'NEW': return '#ff6b6b'
|
||||
case 'GROWING': return '#ffa726'
|
||||
case 'ESTABLISHED': return '#66bb6a'
|
||||
case 'ERROR': return '#9e9e9e'
|
||||
default: return '#9e9e9e'
|
||||
}
|
||||
}
|
||||
|
||||
const getStageIcon = (stage) => {
|
||||
switch (stage) {
|
||||
case 'NEW': return '🌱'
|
||||
case 'GROWING': return '📈'
|
||||
case 'ESTABLISHED': return '🏆'
|
||||
case 'ERROR': return '❌'
|
||||
default: return '❓'
|
||||
}
|
||||
}
|
||||
|
||||
const toggleDomainDetails = (domain) => {
|
||||
setExpandedDomain(expandedDomain === domain ? null : domain)
|
||||
}
|
||||
|
||||
const renderDetailedData = (data) => {
|
||||
if (!data || data.error) return null
|
||||
|
||||
return (
|
||||
<div className="detailed-data-container">
|
||||
<div className="detailed-tabs">
|
||||
<button
|
||||
className={`tab-button ${activeTab === 'keywords' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('keywords')}
|
||||
>
|
||||
🔍 Organic Keywords ({data.topKeywords?.length || 0})
|
||||
</button>
|
||||
<button
|
||||
className={`tab-button ${activeTab === 'backlinks' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('backlinks')}
|
||||
>
|
||||
🔗 Backlinks ({data.backlinksList?.length || 0})
|
||||
</button>
|
||||
<button
|
||||
className={`tab-button ${activeTab === 'pages' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('pages')}
|
||||
>
|
||||
📄 Top Pages ({data.topPages?.length || 0})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="tab-content">
|
||||
{activeTab === 'keywords' && (
|
||||
<div className="keywords-table-container">
|
||||
<table className="detailed-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Keyword</th>
|
||||
<th>Position</th>
|
||||
<th>Search Volume</th>
|
||||
<th>CPC</th>
|
||||
<th>Competition</th>
|
||||
<th>Keyword Difficulty</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.topKeywords?.map((kw, idx) => (
|
||||
<tr key={idx}>
|
||||
<td className="keyword-cell">{kw.phrase}</td>
|
||||
<td className="position-cell">{kw.position}</td>
|
||||
<td className="volume-cell">{kw.searchVolume?.toLocaleString()}</td>
|
||||
<td className="cpc-cell">${kw.cpc?.toFixed(2)}</td>
|
||||
<td className="competition-cell">
|
||||
<div className="progress-bar">
|
||||
<div
|
||||
className="progress-fill"
|
||||
style={{ width: `${(kw.competition * 100).toFixed(0)}%` }}
|
||||
></div>
|
||||
<span className="progress-text">{(kw.competition * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="difficulty-cell">
|
||||
<span className={`difficulty-badge difficulty-${Math.floor(kw.keywordDifficulty / 25)}`}>
|
||||
{kw.keywordDifficulty}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{(!data.topKeywords || data.topKeywords.length === 0) && (
|
||||
<tr>
|
||||
<td colSpan="6" className="no-data">No organic keywords data available</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'backlinks' && (
|
||||
<div className="backlinks-table-container">
|
||||
<div className="backlinks-summary">
|
||||
<div className="summary-stat">
|
||||
<span className="stat-label">Total Backlinks:</span>
|
||||
<span className="stat-value">{data.totalBacklinks?.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="summary-stat">
|
||||
<span className="stat-label">Referring Domains:</span>
|
||||
<span className="stat-value">{data.referringDomains?.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="summary-stat">
|
||||
<span className="stat-label">Referring URLs:</span>
|
||||
<span className="stat-value">{data.referringUrls?.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="summary-stat">
|
||||
<span className="stat-label">Referring IPs:</span>
|
||||
<span className="stat-value">{data.referringIps?.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
<table className="detailed-table backlinks-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Page Authority</th>
|
||||
<th>Source URL</th>
|
||||
<th>Target URL</th>
|
||||
<th>Anchor Text</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.backlinksList?.map((bl, idx) => (
|
||||
<tr key={idx}>
|
||||
<td className="ascore-cell">
|
||||
<span className={`ascore-badge ascore-${Math.floor(bl.pageAscore / 25)}`}>
|
||||
{bl.pageAscore}
|
||||
</span>
|
||||
</td>
|
||||
<td className="url-cell">
|
||||
<a href={bl.sourceUrl} target="_blank" rel="noopener noreferrer" title={bl.sourceUrl}>
|
||||
{bl.sourceUrl.length > 60 ? bl.sourceUrl.substring(0, 60) + '...' : bl.sourceUrl}
|
||||
</a>
|
||||
</td>
|
||||
<td className="url-cell">
|
||||
<a href={bl.targetUrl} target="_blank" rel="noopener noreferrer" title={bl.targetUrl}>
|
||||
{bl.targetUrl.length > 60 ? bl.targetUrl.substring(0, 60) + '...' : bl.targetUrl}
|
||||
</a>
|
||||
</td>
|
||||
<td className="anchor-cell">{bl.anchor || '(no anchor)'}</td>
|
||||
</tr>
|
||||
))}
|
||||
{(!data.backlinksList || data.backlinksList.length === 0) && (
|
||||
<tr>
|
||||
<td colSpan="4" className="no-data">No backlinks data available</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'pages' && (
|
||||
<div className="pages-table-container">
|
||||
<table className="detailed-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Page URL</th>
|
||||
<th>Traffic</th>
|
||||
<th>Keywords Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.topPages?.map((page, idx) => (
|
||||
<tr key={idx}>
|
||||
<td className="url-cell">
|
||||
<a href={page.url} target="_blank" rel="noopener noreferrer" title={page.url}>
|
||||
{page.url}
|
||||
</a>
|
||||
</td>
|
||||
<td className="traffic-cell">{page.traffic?.toLocaleString()}</td>
|
||||
<td className="count-cell">{page.positionCount || 1}</td>
|
||||
</tr>
|
||||
))}
|
||||
{(!data.topPages || data.topPages.length === 0) && (
|
||||
<tr>
|
||||
<td colSpan="3" className="no-data">No top pages data available</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="seo-maturity-analysis">
|
||||
<div className="analysis-header">
|
||||
<h2>SEO Maturity Analysis</h2>
|
||||
<p>Comprehensive domain-level metrics and classification</p>
|
||||
</div>
|
||||
|
||||
{/* Key Metrics Cards */}
|
||||
{getMetricCards()}
|
||||
|
||||
{/* Summary Section */}
|
||||
<div className="summary-section">
|
||||
<h3>Executive Summary</h3>
|
||||
<div className="summary-content">
|
||||
{generateSummary()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results Table */}
|
||||
<div className="results-table-container">
|
||||
<table className="seo-maturity-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Actions</th>
|
||||
<th>Domain</th>
|
||||
<th>Authority Score</th>
|
||||
<th>Organic Keywords</th>
|
||||
<th>Total Backlinks</th>
|
||||
<th>Referring Domains</th>
|
||||
<th>Monthly Traffic</th>
|
||||
<th>Stage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{analysisResults.map((result, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<tr className={result.error ? 'error-row' : ''}>
|
||||
<td className="actions-cell">
|
||||
{!result.error && (
|
||||
<button
|
||||
className="expand-button"
|
||||
onClick={() => toggleDomainDetails(result.domain)}
|
||||
title="View detailed data"
|
||||
>
|
||||
{expandedDomain === result.domain ? '▼' : '▶'}
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
<td className="domain-cell">
|
||||
<strong>{result.domain}</strong>
|
||||
{index === 0 && <span className="client-badge">Client</span>}
|
||||
</td>
|
||||
<td className="metric-cell">
|
||||
{result.error ? 'Error' : result.authorityScore.toLocaleString()}
|
||||
</td>
|
||||
<td className="metric-cell">
|
||||
{result.error ? 'Error' : result.organicKeywords.toLocaleString()}
|
||||
</td>
|
||||
<td className="metric-cell">
|
||||
{result.error ? 'Error' : result.totalBacklinks.toLocaleString()}
|
||||
</td>
|
||||
<td className="metric-cell">
|
||||
{result.error ? 'Error' : result.referringDomains.toLocaleString()}
|
||||
</td>
|
||||
<td className="metric-cell">
|
||||
{result.error ? 'Error' : result.monthlyTraffic.toLocaleString()}
|
||||
</td>
|
||||
<td className="stage-cell">
|
||||
<span
|
||||
className="stage-badge"
|
||||
style={{ backgroundColor: getStageColor(result.stage) }}
|
||||
>
|
||||
{getStageIcon(result.stage)} {result.stage}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{expandedDomain === result.domain && !result.error && (
|
||||
<tr className="expanded-row">
|
||||
<td colSpan="8">
|
||||
{renderDetailedData(result)}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Classification Legend */}
|
||||
<div className="classification-legend">
|
||||
<h4>Classification Criteria</h4>
|
||||
<div className="legend-items">
|
||||
<div className="legend-item">
|
||||
<span className="legend-color" style={{ backgroundColor: '#ff6b6b' }}></span>
|
||||
<strong>NEW:</strong> AS < 20 OR Organic KW < 500 OR < 10 Ref Domains
|
||||
</div>
|
||||
<div className="legend-item">
|
||||
<span className="legend-color" style={{ backgroundColor: '#ffa726' }}></span>
|
||||
<strong>GROWING:</strong> AS 20–40 OR Organic KW 500–3000
|
||||
</div>
|
||||
<div className="legend-item">
|
||||
<span className="legend-color" style={{ backgroundColor: '#66bb6a' }}></span>
|
||||
<strong>ESTABLISHED:</strong> AS > 40 OR Organic KW > 3000 OR > 100 Ref Domains
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SEOMaturityAnalysis
|
||||
1435
src/components/UnifiedAnalysis.css
Normal file
1435
src/components/UnifiedAnalysis.css
Normal file
File diff suppressed because it is too large
Load Diff
727
src/components/UnifiedAnalysis.jsx
Normal file
727
src/components/UnifiedAnalysis.jsx
Normal file
@ -0,0 +1,727 @@
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react'
|
||||
import './UnifiedAnalysis.css'
|
||||
import { sendToHookpilot } from '../services/semrushApi'
|
||||
|
||||
function UnifiedAnalysis({ analysisData }) {
|
||||
const [hookpilotStatus, setHookpilotStatus] = useState(null)
|
||||
const [sending, setSending] = useState(false)
|
||||
const [sortBy, setSortBy] = useState('volume')
|
||||
const [sortOrder, setSortOrder] = useState('desc')
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [itemsPerPage, setItemsPerPage] = useState(25)
|
||||
const [selectedDomain, setSelectedDomain] = useState(null) // Track which domain modal is open
|
||||
|
||||
if (!analysisData) return null
|
||||
|
||||
const {
|
||||
metrics,
|
||||
gapKeywords, // Simplified - just gap keywords (Intent=1 only)
|
||||
clientUrl,
|
||||
competitorUrls,
|
||||
clientStage,
|
||||
apiUsage,
|
||||
structuredResults
|
||||
} = analysisData
|
||||
|
||||
const clientData = metrics[0]
|
||||
const competitorsData = metrics.slice(1)
|
||||
|
||||
// Simplified - just show informational gap keywords
|
||||
const informationalGapKeywords = gapKeywords || []
|
||||
const hasKeywordOpportunities = informationalGapKeywords.length > 0
|
||||
|
||||
const handleSendToHookpilot = async () => {
|
||||
// Initial alert to inform user
|
||||
alert('⏳ Sending data to Hookpilot...\n\nYou will be notified once the operation completes or if any issues occur.')
|
||||
|
||||
setSending(true)
|
||||
setHookpilotStatus(null)
|
||||
|
||||
try {
|
||||
const result = await sendToHookpilot(analysisData)
|
||||
setHookpilotStatus(result)
|
||||
|
||||
// Success alert
|
||||
if (result.success) {
|
||||
alert('✅ Success!\n\nData has been successfully sent to Hookpilot.')
|
||||
} else {
|
||||
alert('⚠️ Warning!\n\nData was sent but Hookpilot returned an unexpected response. Please check the status below.')
|
||||
}
|
||||
} catch (error) {
|
||||
setHookpilotStatus({ success: false, error: error.message })
|
||||
|
||||
// Error alert
|
||||
alert(`❌ Error!\n\nFailed to send data to Hookpilot:\n\n${error.message}`)
|
||||
} finally {
|
||||
setSending(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Use gap keywords directly (no tabs needed)
|
||||
const currentKeywords = informationalGapKeywords
|
||||
|
||||
// Memoize sorted keywords to prevent re-sorting on every render
|
||||
const sortedKeywords = useMemo(() => {
|
||||
return [...currentKeywords].sort((a, b) => {
|
||||
let aVal, bVal
|
||||
|
||||
switch (sortBy) {
|
||||
case 'volume':
|
||||
aVal = a.volume || 0
|
||||
bVal = b.volume || 0
|
||||
break
|
||||
case 'difficulty':
|
||||
aVal = a.kd || 0
|
||||
bVal = b.kd || 0
|
||||
break
|
||||
case 'keyword':
|
||||
return sortOrder === 'asc'
|
||||
? a.keyword.localeCompare(b.keyword)
|
||||
: b.keyword.localeCompare(a.keyword)
|
||||
default:
|
||||
aVal = 0
|
||||
bVal = 0
|
||||
}
|
||||
|
||||
return sortOrder === 'asc' ? aVal - bVal : bVal - aVal
|
||||
})
|
||||
}, [currentKeywords, sortBy, sortOrder])
|
||||
|
||||
// Pagination calculations - memoized
|
||||
const { totalPages, startIndex, endIndex, paginatedKeywords } = useMemo(() => {
|
||||
const total = Math.ceil(sortedKeywords.length / itemsPerPage)
|
||||
const start = (currentPage - 1) * itemsPerPage
|
||||
const end = start + itemsPerPage
|
||||
const paginated = sortedKeywords.slice(start, end)
|
||||
|
||||
return {
|
||||
totalPages: total,
|
||||
startIndex: start,
|
||||
endIndex: end,
|
||||
paginatedKeywords: paginated
|
||||
}
|
||||
}, [sortedKeywords, itemsPerPage, currentPage])
|
||||
|
||||
// Reset to page 1 when sorting changes
|
||||
useEffect(() => {
|
||||
setCurrentPage(1)
|
||||
}, [sortBy, sortOrder])
|
||||
|
||||
// Handle page change
|
||||
const handlePageChange = useCallback((newPage) => {
|
||||
if (newPage >= 1 && newPage <= totalPages) {
|
||||
setCurrentPage(newPage)
|
||||
// Scroll to keyword section instead of top of page
|
||||
const keywordSection = document.querySelector('.keywords-section')
|
||||
if (keywordSection) {
|
||||
keywordSection.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
}
|
||||
}, [totalPages])
|
||||
|
||||
// Handle items per page change
|
||||
const handleItemsPerPageChange = useCallback((e) => {
|
||||
const newItemsPerPage = parseInt(e.target.value)
|
||||
setItemsPerPage(newItemsPerPage)
|
||||
setCurrentPage(1)
|
||||
}, [])
|
||||
|
||||
// CSV Export functions
|
||||
const exportToCSV = (data, filename, headers) => {
|
||||
const csvHeaders = headers.join(',')
|
||||
const csvRows = data.map(row => {
|
||||
return headers.map(header => {
|
||||
const value = row[header] || ''
|
||||
// Escape quotes and wrap in quotes if contains comma or quote
|
||||
if (typeof value === 'string' && (value.includes(',') || value.includes('"'))) {
|
||||
return `"${value.replace(/"/g, '""')}"`
|
||||
}
|
||||
return value
|
||||
}).join(',')
|
||||
})
|
||||
|
||||
const csv = [csvHeaders, ...csvRows].join('\n')
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
link.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
const exportMissingKeywords = () => {
|
||||
// Export informational gap keywords
|
||||
const dataForExport = informationalGapKeywords.map(kw => ({
|
||||
...kw,
|
||||
competitor_domains: Array.isArray(kw.competitor_domains)
|
||||
? kw.competitor_domains.join(', ')
|
||||
: kw.competitors_found_in || ''
|
||||
}))
|
||||
|
||||
exportToCSV(
|
||||
dataForExport,
|
||||
'informational_gap_keywords.csv',
|
||||
['keyword', 'volume', 'kd', 'competitor_domains']
|
||||
)
|
||||
}
|
||||
|
||||
const handleSort = useCallback((field) => {
|
||||
if (sortBy === field) {
|
||||
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
|
||||
} else {
|
||||
setSortBy(field)
|
||||
setSortOrder('desc')
|
||||
}
|
||||
}, [sortBy, sortOrder])
|
||||
|
||||
const getStageColor = (stage) => {
|
||||
switch (stage) {
|
||||
case 'NEW': return '#ff6b6b'
|
||||
case 'GROWING': return '#ffa726'
|
||||
case 'ESTABLISHED': return '#66bb6a'
|
||||
default: return '#9e9e9e'
|
||||
}
|
||||
}
|
||||
|
||||
const getStageIcon = (stage) => {
|
||||
switch (stage) {
|
||||
case 'NEW': return '🌱'
|
||||
case 'GROWING': return '📈'
|
||||
case 'ESTABLISHED': return '🏆'
|
||||
default: return '❓'
|
||||
}
|
||||
}
|
||||
|
||||
const formatNumber = (num) => {
|
||||
if (!num || num === 0) return '0'
|
||||
return parseInt(num).toLocaleString()
|
||||
}
|
||||
|
||||
// Open domain details modal
|
||||
const openDomainModal = useCallback((domainData) => {
|
||||
setSelectedDomain(domainData)
|
||||
}, [])
|
||||
|
||||
// Close modal
|
||||
const closeDomainModal = useCallback(() => {
|
||||
setSelectedDomain(null)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="unified-analysis">
|
||||
{/* Header */}
|
||||
<div className="analysis-header">
|
||||
<div className="header-content">
|
||||
<h2>🚀 SEO Analysis Results</h2>
|
||||
<p>Domain metrics, keyword gap analysis, and SEO maturity classification</p>
|
||||
</div>
|
||||
<div className="header-action-group">
|
||||
<button
|
||||
onClick={handleSendToHookpilot}
|
||||
className={`hookpilot-btn ${sending ? 'sending' : ''} ${hookpilotStatus?.success ? 'success' : ''}`}
|
||||
disabled={sending}
|
||||
>
|
||||
{sending ? (
|
||||
<>
|
||||
<span className="button-spinner"></span>
|
||||
<span>Sending to Hookpilot...</span>
|
||||
</>
|
||||
) : hookpilotStatus?.success ? (
|
||||
<>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M10 18a8 8 0 100-16 8 8 0 000 16z" fill="#10b981"/>
|
||||
<path d="M14.707 7.293a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L10 10.586l3.293-3.293a1 1 0 011.414 0z" fill="white"/>
|
||||
</svg>
|
||||
<span>Sent Successfully!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" fill="currentColor"/>
|
||||
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" fill="currentColor"/>
|
||||
</svg>
|
||||
<span>Send to Hookpilot</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{hookpilotStatus && !hookpilotStatus.success && (
|
||||
<div className="hookpilot-status error">
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" fill="currentColor"/>
|
||||
</svg>
|
||||
{hookpilotStatus.error || 'Failed to send'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* API Usage Report */}
|
||||
{apiUsage && (
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||
borderRadius: '12px',
|
||||
padding: '24px',
|
||||
marginBottom: '32px',
|
||||
color: 'white',
|
||||
boxShadow: '0 4px 6px rgba(0,0,0,0.1)'
|
||||
}}>
|
||||
<h3 style={{ margin: '0 0 16px 0', fontSize: '20px', fontWeight: '700' }}>
|
||||
📊 API Usage & Cost Report
|
||||
</h3>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', gap: '24px' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '36px', fontWeight: '800' }}>{apiUsage.totalUnits?.toLocaleString() || 0}</div>
|
||||
<div style={{ fontSize: '14px', opacity: 0.9 }}>Total Units</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: '36px', fontWeight: '800' }}>{apiUsage.duration || '0s'}</div>
|
||||
<div style={{ fontSize: '14px', opacity: 0.9 }}>Duration</div>
|
||||
</div>
|
||||
</div>
|
||||
{apiUsage.breakdown && apiUsage.breakdown.phrase_this && (
|
||||
<div style={{
|
||||
marginTop: '16px',
|
||||
padding: '12px',
|
||||
background: 'rgba(255,255,255,0.15)',
|
||||
borderRadius: '8px'
|
||||
}}>
|
||||
<strong>🔍 phrase_this Validation:</strong> {apiUsage.breakdown.phrase_this.calls || 0} API calls, {apiUsage.breakdown.phrase_this.units?.toLocaleString() || 0} units consumed
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Keyword Gap Analysis Section */}
|
||||
<div className="keywords-section">
|
||||
<div className="keywords-header">
|
||||
<h3>🎯 Informational Gap Keywords (Intent=1)</h3>
|
||||
<div className="sort-controls">
|
||||
<span>Sort by:</span>
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => handleSort(e.target.value)}
|
||||
className="sort-select"
|
||||
>
|
||||
<option value="volume">Volume</option>
|
||||
<option value="difficulty">KD</option>
|
||||
<option value="keyword">Keyword</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}
|
||||
className="sort-order-btn"
|
||||
>
|
||||
{sortOrder === 'asc' ? '↑ Asc' : '↓ Desc'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Warning message if no keyword opportunities */}
|
||||
{!hasKeywordOpportunities && (
|
||||
<div className="warning-message" style={{
|
||||
padding: '16px 20px',
|
||||
marginBottom: '24px',
|
||||
backgroundColor: '#fef3c7',
|
||||
border: '1px solid #fbbf24',
|
||||
borderRadius: '8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px'
|
||||
}}>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M10 18a8 8 0 100-16 8 8 0 000 16zM9 9a1 1 0 012 0v4a1 1 0 11-2 0V9zm1-4a1 1 0 100 2 1 1 0 000-2z" fill="#f59e0b"/>
|
||||
</svg>
|
||||
<span style={{ color: '#92400e', fontWeight: '500' }}>
|
||||
No keyword opportunities found. All competitors may have similar keyword profiles to your client.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Simplified Header - Just Informational Keywords */}
|
||||
{hasKeywordOpportunities && (
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
padding: '20px 24px',
|
||||
borderRadius: '12px',
|
||||
color: 'white',
|
||||
marginBottom: '20px',
|
||||
boxShadow: '0 4px 6px rgba(0,0,0,0.1)'
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '16px' }}>
|
||||
<div>
|
||||
<h3 style={{ margin: 0, fontSize: '20px', fontWeight: '700' }}>
|
||||
🎯 Informational Gap Keywords (Intent=1)
|
||||
</h3>
|
||||
<p style={{ margin: '6px 0 0 0', fontSize: '14px', opacity: 0.95 }}>
|
||||
Keywords competitors rank for, client doesn't - verified as Informational via phrase_this API
|
||||
</p>
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '48px',
|
||||
fontWeight: '800',
|
||||
textAlign: 'center',
|
||||
lineHeight: 1
|
||||
}}>
|
||||
{sortedKeywords.length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasKeywordOpportunities && (
|
||||
<>
|
||||
<div className="keywords-stats" style={{ display: 'flex', gap: '16px', flexWrap: 'wrap', marginBottom: '20px' }}>
|
||||
<button onClick={exportMissingKeywords} className="export-btn" style={{ marginLeft: 'auto' }}>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M14 8L10 12L6 8H9V4H11V8H14ZM4 14V16H16V14H4Z" fill="currentColor"/>
|
||||
</svg>
|
||||
Export CSV
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="table-container">
|
||||
<table className="keywords-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th onClick={() => handleSort('keyword')} className="sortable">
|
||||
Keyword
|
||||
{sortBy === 'keyword' && (
|
||||
<span className="sort-indicator">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||
)}
|
||||
</th>
|
||||
<th onClick={() => handleSort('volume')} className="sortable">
|
||||
Volume
|
||||
{sortBy === 'volume' && (
|
||||
<span className="sort-indicator">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||
)}
|
||||
</th>
|
||||
<th onClick={() => handleSort('difficulty')} className="sortable">
|
||||
KD
|
||||
{sortBy === 'difficulty' && (
|
||||
<span className="sort-indicator">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||
)}
|
||||
</th>
|
||||
<th>Competitors Found In</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paginatedKeywords.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} style={{ textAlign: 'center', padding: '40px' }}>
|
||||
<p style={{ color: '#6b7280', fontSize: '16px' }}>No keywords found in this category.</p>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
paginatedKeywords.map((keyword, index) => (
|
||||
<tr key={startIndex + index} className="keyword-row">
|
||||
<td className="keyword-phrase">{keyword.keyword}</td>
|
||||
<td className="keyword-volume">
|
||||
{keyword.volume ? formatNumber(keyword.volume) : 'N/A'}
|
||||
</td>
|
||||
<td className="keyword-difficulty">
|
||||
<div className="difficulty-cell">
|
||||
<span className={`difficulty-badge difficulty-${getDifficultyLevel(keyword.kd)}`}>
|
||||
{keyword.kd || 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="competitor-count">
|
||||
{keyword.competitors_found_in || (keyword.competitor_domains && keyword.competitor_domains.join(', ')) || 'N/A'}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination Controls */}
|
||||
{hasKeywordOpportunities && sortedKeywords.length > 0 && (
|
||||
<div className="pagination-container">
|
||||
<div className="pagination-info">
|
||||
<span>
|
||||
Showing {startIndex + 1} to {Math.min(endIndex, sortedKeywords.length)} of {sortedKeywords.length} keywords
|
||||
</span>
|
||||
<div className="pagination-items-per-page">
|
||||
<label htmlFor="itemsPerPage">Items per page:</label>
|
||||
<select
|
||||
id="itemsPerPage"
|
||||
value={itemsPerPage}
|
||||
onChange={handleItemsPerPageChange}
|
||||
className="items-per-page-select"
|
||||
>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pagination-controls">
|
||||
<button
|
||||
className="pagination-btn"
|
||||
onClick={() => handlePageChange(1)}
|
||||
disabled={currentPage === 1}
|
||||
title="First page"
|
||||
>
|
||||
««
|
||||
</button>
|
||||
<button
|
||||
className="pagination-btn"
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
title="Previous page"
|
||||
>
|
||||
‹ Prev
|
||||
</button>
|
||||
|
||||
<div className="pagination-pages">
|
||||
{Array.from({ length: totalPages }, (_, i) => i + 1)
|
||||
.filter(page => {
|
||||
// Show first page, last page, current page, and pages around current
|
||||
if (page === 1 || page === totalPages) return true
|
||||
if (Math.abs(page - currentPage) <= 1) return true
|
||||
return false
|
||||
})
|
||||
.map((page, index, array) => {
|
||||
const prevPage = array[index - 1]
|
||||
const showEllipsis = prevPage && page - prevPage > 1
|
||||
|
||||
return (
|
||||
<React.Fragment key={page}>
|
||||
{showEllipsis && (
|
||||
<span className="pagination-ellipsis">...</span>
|
||||
)}
|
||||
<button
|
||||
className={`pagination-page-btn ${currentPage === page ? 'active' : ''}`}
|
||||
onClick={() => handlePageChange(page)}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="pagination-btn"
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
title="Next page"
|
||||
>
|
||||
Next ›
|
||||
</button>
|
||||
<button
|
||||
className="pagination-btn"
|
||||
onClick={() => handlePageChange(totalPages)}
|
||||
disabled={currentPage === totalPages}
|
||||
title="Last page"
|
||||
>
|
||||
»»
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Domain Metrics Section */}
|
||||
<div className="metrics-section">
|
||||
<h3>📊 Domain Metrics</h3>
|
||||
<div className="metrics-grid">
|
||||
{/* Client Metrics */}
|
||||
<div className="metric-card client-card">
|
||||
<div
|
||||
className="card-header"
|
||||
onClick={() => openDomainModal(clientData)}
|
||||
style={{ cursor: 'pointer', userSelect: 'none' }}
|
||||
>
|
||||
<h4>{clientData.domain}</h4>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<span className="client-badge">Client</span>
|
||||
<span style={{ fontSize: '20px', opacity: 0.7 }}>👁️</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="metric-content">
|
||||
<div className="metric-item">
|
||||
<span className="metric-label">Rank:</span>
|
||||
<span className="metric-value">{formatNumber(clientData.rank)}</span>
|
||||
</div>
|
||||
<div className="metric-item">
|
||||
<span className="metric-label">Organic Traffic:</span>
|
||||
<span className="metric-value">{formatNumber(clientData.organicTraffic)}</span>
|
||||
</div>
|
||||
<div className="metric-item">
|
||||
<span className="metric-label">Organic Keywords:</span>
|
||||
<span className="metric-value">{formatNumber(clientData.organicKeywordsCount)}</span>
|
||||
</div>
|
||||
<div className="metric-item">
|
||||
<span className="metric-label">Authority Score:</span>
|
||||
<span className="metric-value">{formatNumber(clientData.authorityScore)}</span>
|
||||
</div>
|
||||
<div className="metric-item">
|
||||
<span className="metric-label">Total Backlinks:</span>
|
||||
<span className="metric-value">{formatNumber(clientData.totalBacklinks)}</span>
|
||||
</div>
|
||||
<div className="metric-item">
|
||||
<span className="metric-label">Referring Domains:</span>
|
||||
<span className="metric-value">{formatNumber(clientData.referringDomainsCount)}</span>
|
||||
</div>
|
||||
<div className="metric-item stage-item">
|
||||
<span className="metric-label">SEO Stage:</span>
|
||||
<span
|
||||
className="stage-badge"
|
||||
style={{ backgroundColor: getStageColor(clientData.stage) }}
|
||||
>
|
||||
{getStageIcon(clientData.stage)} {clientData.stage}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Competitors Metrics */}
|
||||
{competitorsData.map((competitor, index) => (
|
||||
<div key={index} className="metric-card competitor-card">
|
||||
<div
|
||||
className="card-header"
|
||||
onClick={() => openDomainModal(competitor)}
|
||||
style={{ cursor: 'pointer', userSelect: 'none' }}
|
||||
>
|
||||
<h4>{competitor.domain}</h4>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<span className="competitor-badge">Competitor {index + 1}</span>
|
||||
<span style={{ fontSize: '20px', opacity: 0.7 }}>👁️</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="metric-content">
|
||||
<div className="metric-item">
|
||||
<span className="metric-label">Rank:</span>
|
||||
<span className="metric-value">{formatNumber(competitor.rank)}</span>
|
||||
</div>
|
||||
<div className="metric-item">
|
||||
<span className="metric-label">Organic Traffic:</span>
|
||||
<span className="metric-value">{formatNumber(competitor.organicTraffic)}</span>
|
||||
</div>
|
||||
<div className="metric-item">
|
||||
<span className="metric-label">Organic Keywords:</span>
|
||||
<span className="metric-value">{formatNumber(competitor.organicKeywordsCount)}</span>
|
||||
</div>
|
||||
<div className="metric-item">
|
||||
<span className="metric-label">Authority Score:</span>
|
||||
<span className="metric-value">{formatNumber(competitor.authorityScore)}</span>
|
||||
</div>
|
||||
<div className="metric-item">
|
||||
<span className="metric-label">Total Backlinks:</span>
|
||||
<span className="metric-value">{formatNumber(competitor.totalBacklinks)}</span>
|
||||
</div>
|
||||
<div className="metric-item">
|
||||
<span className="metric-label">Referring Domains:</span>
|
||||
<span className="metric-value">{formatNumber(competitor.referringDomainsCount)}</span>
|
||||
</div>
|
||||
<div className="metric-item stage-item">
|
||||
<span className="metric-label">SEO Stage:</span>
|
||||
<span
|
||||
className="stage-badge"
|
||||
style={{ backgroundColor: getStageColor(competitor.stage) }}
|
||||
>
|
||||
{getStageIcon(competitor.stage)} {competitor.stage}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Domain Details Modal */}
|
||||
{selectedDomain && (
|
||||
<div
|
||||
className="modal-overlay"
|
||||
onClick={closeDomainModal}
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="modal-header">
|
||||
<h3>📊 {selectedDomain.domain} - Details</h3>
|
||||
<button className="modal-close" onClick={closeDomainModal}>✕</button>
|
||||
</div>
|
||||
|
||||
<div className="modal-body">
|
||||
{/* Top 10 Organic Keywords */}
|
||||
{selectedDomain.top10Keywords && selectedDomain.top10Keywords.length > 0 && (
|
||||
<div className="modal-section">
|
||||
<h4>
|
||||
📊 Top 10 Organic Keywords
|
||||
</h4>
|
||||
<div className="modal-table-wrapper">
|
||||
<table className="modal-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign: 'left' }}>Keyword</th>
|
||||
<th style={{ textAlign: 'right' }}>Search Volume</th>
|
||||
<th style={{ textAlign: 'right' }}>KD</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{selectedDomain.top10Keywords.map((kw, idx) => (
|
||||
<tr key={idx}>
|
||||
<td style={{ fontWeight: '500' }}>{kw.phrase}</td>
|
||||
<td style={{ textAlign: 'right' }}>{formatNumber(kw.searchVolume)}</td>
|
||||
<td style={{ textAlign: 'right' }}>{kw.keywordDifficulty}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Top 10 Referring Domains */}
|
||||
{selectedDomain.top10ReferringDomains && selectedDomain.top10ReferringDomains.length > 0 && (
|
||||
<div className="modal-section">
|
||||
<h4>
|
||||
🔗 Top 10 Referring Domains
|
||||
</h4>
|
||||
<div className="modal-table-wrapper">
|
||||
<table className="modal-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign: 'left' }}>Domain</th>
|
||||
<th style={{ textAlign: 'right' }}>Authority Score</th>
|
||||
<th style={{ textAlign: 'right' }}>Backlinks</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{selectedDomain.top10ReferringDomains.map((ref, idx) => (
|
||||
<tr key={idx}>
|
||||
<td style={{ fontWeight: '500' }}>{ref.domain}</td>
|
||||
<td style={{ textAlign: 'right' }}>{ref.domainAscore}</td>
|
||||
<td style={{ textAlign: 'right' }}>{formatNumber(ref.backlinksNum)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function getDifficultyLevel(difficulty) {
|
||||
const diff = parseInt(difficulty)
|
||||
if (isNaN(diff)) return 'unknown'
|
||||
if (diff < 30) return 'easy'
|
||||
if (diff < 60) return 'medium'
|
||||
return 'hard'
|
||||
}
|
||||
|
||||
export default UnifiedAnalysis
|
||||
22
src/index.css
Normal file
22
src/index.css
Normal file
@ -0,0 +1,22 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#root {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
11
src/main.jsx
Normal file
11
src/main.jsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
|
||||
1344
src/services/semrushApi.js
Normal file
1344
src/services/semrushApi.js
Normal file
File diff suppressed because it is too large
Load Diff
46
vite.config.js
Normal file
46
vite.config.js
Normal file
@ -0,0 +1,46 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api/semrush': {
|
||||
target: 'https://api.semrush.com',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api\/semrush/, ''),
|
||||
configure: (proxy, options) => {
|
||||
proxy.on('proxyReq', (proxyReq, req, res) => {
|
||||
// Add CORS headers to the request
|
||||
proxyReq.setHeader('Origin', 'https://api.semrush.com');
|
||||
});
|
||||
proxy.on('proxyRes', (proxyRes, req, res) => {
|
||||
// Add CORS headers to the response
|
||||
proxyRes.headers['Access-Control-Allow-Origin'] = '*';
|
||||
proxyRes.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS';
|
||||
proxyRes.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization';
|
||||
});
|
||||
}
|
||||
},
|
||||
'/api/analytics': {
|
||||
target: 'https://api.semrush.com',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api\/analytics/, '/analytics'),
|
||||
configure: (proxy, options) => {
|
||||
proxy.on('proxyReq', (proxyReq, req, res) => {
|
||||
// Add CORS headers to the request
|
||||
proxyReq.setHeader('Origin', 'https://api.semrush.com');
|
||||
});
|
||||
proxy.on('proxyRes', (proxyRes, req, res) => {
|
||||
// Add CORS headers to the response
|
||||
proxyRes.headers['Access-Control-Allow-Origin'] = '*';
|
||||
proxyRes.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS';
|
||||
proxyRes.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization';
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user