25 KiB
High Opportunity Keywords for Add Keywords Page
Plan Date: January 14, 2026
Target Page: /setup/add-keywords (IndustriesSectorsKeywords.tsx)
Reference Implementation: Step 4 of Wizard (Step4AddKeywords.tsx)
Overview
Add a "High Opportunity Keywords" section to the top of the /setup/add-keywords page, similar to the wizard's Step 4 interface. This will allow users to quickly add curated keyword sets (Top 50 High Volume & Top 50 Low Difficulty) for each of their site's sectors.
Current State
Current Page Structure
- Page:
IndustriesSectorsKeywords.tsx(854 lines) - Route:
/setup/add-keywords - Primary Function: Browse and search global seed keywords with filters, sorting, and pagination
- Current Features:
- Search by keyword text
- Filter by country, difficulty, status (not added only)
- Sort by keyword, volume, difficulty, country
- Bulk selection and "Add Selected to Workflow"
- Individual "Add to Workflow" buttons
- Shows stats: X added / Y available
- Admin CSV import functionality
- Table-based display with pagination
- Requires active sector selection to add keywords
Wizard Step 4 Implementation
- Component:
Step4AddKeywords.tsx(738 lines) - Features to Replicate:
- Groups keywords by sector (e.g., "Physiotherapy & Rehabilitation", "Relaxation Devices", "Massage & Therapy")
- Two options per sector:
- Top 50 High Volume - Keywords sorted by highest volume
- Top 50 Low Difficulty - Keywords sorted by lowest difficulty (KD)
- Shows keyword count and sample keywords (first 3 with "+X more" badge)
- "Add All" button for each option
- Visual feedback when keywords are added (success badge, green styling)
- Loads seed keywords from API filtered by sector
Proposed Implementation
UI Design & Layout
┌─────────────────────────────────────────────────────────────────┐
│ Page Header: "Find Keywords" │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Back to options (link) │
│ │
│ High Opportunity Keywords (h2) │
│ Add top keywords for each of your sectors. Keywords will be │
│ added to your planner workflow. │
│ │
│ ┌─────────────┬─────────────┬─────────────┐ │
│ │ Sector 1 │ Sector 2 │ Sector 3 │ (3 columns) │
│ ├─────────────┼─────────────┼─────────────┤ │
│ │ ┌─────────┐ │ ┌─────────┐ │ ┌─────────┐ │ │
│ │ │Top 50 HV│ │ │Top 50 HV│ │ │Top 50 HV│ │ │
│ │ │X keywords│ │ │X keywords│ │ │X keywords│ │ │
│ │ │[kw] [kw]│ │ │[kw] [kw]│ │ │[kw] [kw]│ │ │
│ │ │+X more │ │ │+X more │ │ │+X more │ │ │
│ │ │[Add All]│ │ │[Add All]│ │ │[Add All]│ │ │
│ │ └─────────┘ │ └─────────┘ │ └─────────┘ │ │
│ │ ┌─────────┐ │ ┌─────────┐ │ ┌─────────┐ │ │
│ │ │Top 50 LD│ │ │Top 50 LD│ │ │Top 50 LD│ │ │
│ │ │X keywords│ │ │X keywords│ │ │X keywords│ │ │
│ │ │[kw] [kw]│ │ │[kw] [kw]│ │ │[kw] [kw]│ │ │
│ │ │+X more │ │ │+X more │ │ │+X more │ │ │
│ │ │[Add All]│ │ │[Add All]│ │ │[Add All]│ │ │
│ │ └─────────┘ │ └─────────┘ │ └─────────┘ │ │
│ └─────────────┴─────────────┴─────────────┘ │
│ │
│ ✓ X keywords added to your workflow │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Browse All Keywords (existing table interface) │
│ [Search] [Filters] [Table] [Pagination] │
└─────────────────────────────────────────────────────────────────┘
Component Architecture
1. New State Management
Add to IndustriesSectorsKeywords.tsx:
// High Opportunity Keywords state
const [showHighOpportunity, setShowHighOpportunity] = useState(true);
const [loadingOpportunityKeywords, setLoadingOpportunityKeywords] = useState(false);
const [sectorKeywordData, setSectorKeywordData] = useState<SectorKeywordData[]>([]);
const [addingOption, setAddingOption] = useState<string | null>(null);
interface SectorKeywordOption {
type: 'high-volume' | 'low-difficulty';
label: string;
keywords: SeedKeyword[];
added: boolean;
keywordCount: number;
}
interface SectorKeywordData {
sectorSlug: string;
sectorName: string;
sectorId: number;
options: SectorKeywordOption[];
}
2. Data Loading Function
Create loadHighOpportunityKeywords():
const loadHighOpportunityKeywords = async () => {
if (!activeSite || !activeSite.industry) {
setSectorKeywordData([]);
return;
}
setLoadingOpportunityKeywords(true);
try {
// 1. Get site sectors
const siteSectors = await fetchSiteSectors(activeSite.id);
// 2. Get industry data
const industriesResponse = await fetchIndustries();
const industry = industriesResponse.industries?.find(
i => i.id === activeSite.industry || i.slug === activeSite.industry_slug
);
if (!industry?.id) {
console.warn('Could not find industry information');
return;
}
// 3. Get already-attached keywords to mark as added
const attachedSeedKeywordIds = new Set<number>();
for (const sector of siteSectors) {
try {
const keywordsData = await fetchKeywords({
site_id: activeSite.id,
sector_id: sector.id,
page_size: 1000,
});
(keywordsData.results || []).forEach((k: any) => {
const seedKeywordId = k.seed_keyword_id || (k.seed_keyword && k.seed_keyword.id);
if (seedKeywordId) {
attachedSeedKeywordIds.add(Number(seedKeywordId));
}
});
} catch (err) {
console.warn(`Could not fetch attached keywords for sector ${sector.id}:`, err);
}
}
// 4. Build sector keyword data
const sectorData: SectorKeywordData[] = [];
for (const siteSector of siteSectors) {
if (!siteSector.is_active) continue;
// Fetch all keywords for this sector
const response = await fetchSeedKeywords({
industry: industry.id,
sector: siteSector.industry_sector,
page_size: 500,
});
const sectorKeywords = response.results;
// Top 50 by highest volume
const highVolumeKeywords = [...sectorKeywords]
.sort((a, b) => (b.volume || 0) - (a.volume || 0))
.slice(0, 50);
// Top 50 by lowest difficulty
const lowDifficultyKeywords = [...sectorKeywords]
.sort((a, b) => (a.difficulty || 100) - (b.difficulty || 100))
.slice(0, 50);
// Check if all keywords in each option are already added
const hvAdded = highVolumeKeywords.every(kw =>
attachedSeedKeywordIds.has(Number(kw.id))
);
const ldAdded = lowDifficultyKeywords.every(kw =>
attachedSeedKeywordIds.has(Number(kw.id))
);
sectorData.push({
sectorSlug: siteSector.slug,
sectorName: siteSector.name,
sectorId: siteSector.id,
options: [
{
type: 'high-volume',
label: 'Top 50 High Volume',
keywords: highVolumeKeywords,
added: hvAdded && highVolumeKeywords.length > 0,
keywordCount: highVolumeKeywords.length,
},
{
type: 'low-difficulty',
label: 'Top 50 Low Difficulty',
keywords: lowDifficultyKeywords,
added: ldAdded && lowDifficultyKeywords.length > 0,
keywordCount: lowDifficultyKeywords.length,
},
],
});
}
setSectorKeywordData(sectorData);
} catch (error: any) {
console.error('Failed to load high opportunity keywords:', error);
toast.error(`Failed to load high opportunity keywords: ${error.message}`);
} finally {
setLoadingOpportunityKeywords(false);
}
};
3. Add Keywords Function
Create handleAddSectorKeywords():
const handleAddSectorKeywords = async (
sectorSlug: string,
optionType: 'high-volume' | 'low-difficulty'
) => {
const sector = sectorKeywordData.find(s => s.sectorSlug === sectorSlug);
if (!sector || !activeSite) return;
const option = sector.options.find(o => o.type === optionType);
if (!option || option.added || option.keywords.length === 0) return;
const addingKey = `${sectorSlug}-${optionType}`;
setAddingOption(addingKey);
try {
const seedKeywordIds = option.keywords.map(kw => kw.id);
const result = await addSeedKeywordsToWorkflow(
seedKeywordIds,
activeSite.id,
sector.sectorId
);
if (result.success && result.created > 0) {
// Mark option as added
setSectorKeywordData(prev =>
prev.map(s =>
s.sectorSlug === sectorSlug
? {
...s,
options: s.options.map(o =>
o.type === optionType ? { ...o, added: true } : o
),
}
: s
)
);
let message = `Added ${result.created} keywords to ${sector.sectorName}`;
if (result.skipped && result.skipped > 0) {
message += ` (${result.skipped} already exist)`;
}
toast.success(message);
// Reload the main table to reflect changes
loadSeedKeywords();
} else if (result.errors && result.errors.length > 0) {
toast.error(result.errors[0]);
} else {
toast.warning('No keywords were added. They may already exist in your workflow.');
}
} catch (err: any) {
toast.error(err.message || 'Failed to add keywords to workflow');
} finally {
setAddingOption(null);
}
};
4. UI Component
Create HighOpportunityKeywordsSection component within the file:
const HighOpportunityKeywordsSection = () => {
if (!showHighOpportunity) return null;
if (!activeSite || sectorKeywordData.length === 0) return null;
const addedCount = sectorKeywordData.reduce(
(acc, s) =>
acc +
s.options
.filter(o => o.added)
.reduce((sum, o) => sum + o.keywordCount, 0),
0
);
return (
<div className="mx-6 mt-6 mb-6 p-6 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
{/* Header */}
<div className="mb-6">
<div className="flex items-center justify-between mb-2">
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
High Opportunity Keywords
</h2>
<button
onClick={() => setShowHighOpportunity(false)}
className="text-sm text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
>
Hide
</button>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400">
Add top keywords for each of your sectors. Keywords will be added to your planner workflow.
</p>
</div>
{/* Loading State */}
{loadingOpportunityKeywords ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-brand-500" />
</div>
) : (
<>
{/* Sector Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6 items-start">
{sectorKeywordData.map((sector) => (
<div key={sector.sectorSlug} className="flex flex-col gap-3">
{/* Sector Name */}
<h4 className="text-base font-semibold text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
{sector.sectorName}
</h4>
{/* Options Cards */}
{sector.options.map((option) => {
const addingKey = `${sector.sectorSlug}-${option.type}`;
const isAdding = addingOption === addingKey;
return (
<Card
key={option.type}
className={`p-4 transition-all flex flex-col ${
option.added
? 'border-success-300 dark:border-success-700 bg-success-50 dark:bg-success-900/20'
: 'hover:border-brand-300 dark:hover:border-brand-700'
}`}
>
{/* Option Header */}
<div className="flex items-center justify-between mb-3">
<div>
<h5 className="text-sm font-medium text-gray-900 dark:text-white">
{option.label}
</h5>
<p className="text-xs text-gray-500 dark:text-gray-400">
{option.keywordCount} keywords
</p>
</div>
{option.added ? (
<Badge tone="success" variant="soft" size="sm">
<CheckCircleIcon className="w-3 h-3 mr-1" />
Added
</Badge>
) : (
<Button
variant="primary"
size="xs"
onClick={() =>
handleAddSectorKeywords(sector.sectorSlug, option.type)
}
disabled={isAdding || !activeSector}
title={
!activeSector
? 'Please select a sector from the sidebar first'
: ''
}
>
{isAdding ? 'Adding...' : 'Add All'}
</Button>
)}
</div>
{/* Sample Keywords */}
<div className="flex flex-wrap gap-1.5 flex-1">
{option.keywords.slice(0, 3).map((kw) => (
<Badge
key={kw.id}
tone={option.added ? 'success' : 'neutral'}
variant="soft"
size="xs"
className="text-xs"
>
{kw.keyword}
</Badge>
))}
{option.keywordCount > 3 && (
<Badge
tone="neutral"
variant="outline"
size="xs"
className="text-xs"
>
+{option.keywordCount - 3} more
</Badge>
)}
</div>
</Card>
);
})}
</div>
))}
</div>
{/* Success Summary */}
{addedCount > 0 && (
<Card className="p-4 bg-success-50 dark:bg-success-900/20 border-success-200 dark:border-success-800">
<div className="flex items-center gap-3">
<CheckCircleIcon className="w-5 h-5 text-success-600 dark:text-success-400" />
<span className="text-sm text-success-700 dark:text-success-300">
{addedCount} keywords added to your workflow
</span>
</div>
</Card>
)}
</>
)}
</div>
);
};
5. Integration Points
In IndustriesSectorsKeywords.tsx:
-
Add imports:
import { CheckCircleIcon } from '../../icons'; import { fetchIndustries, fetchKeywords } from '../../services/api'; -
Add state variables (see #1 above)
-
Add useEffect to load opportunity keywords:
useEffect(() => { if (activeSite && activeSite.id && showHighOpportunity) { loadHighOpportunityKeywords(); } }, [activeSite?.id, showHighOpportunity]); -
Insert component before TablePageTemplate:
return ( <> <PageMeta ... /> <PageHeader ... /> {/* High Opportunity Keywords Section */} <HighOpportunityKeywordsSection /> {/* Existing sector banner */} {!activeSector && activeSite && ( ... )} <TablePageTemplate ... /> {/* Import Modal */} <Modal ... /> </> );
Visual Design Details
Colors & Styling
- Card default: White bg, gray-200 border, hover: brand-300 border
- Card added: success-50 bg, success-300 border, success badge
- Button: Primary variant, size xs for cards
- Badges: xs size for keywords, soft variant
- Grid: 1 column on mobile, 2 on md, 3 on lg
Responsive Behavior
- Mobile (< 768px): Single column, sectors stack vertically
- Tablet (768px - 1024px): 2 columns
- Desktop (> 1024px): 3 columns
User Interactions
-
"Add All" button:
- Disabled during adding (shows "Adding...")
- Disabled if no sector selected (with tooltip)
- Disappears after successful add (replaced with "Added" badge)
-
Success feedback:
- Toast notification with count
- Green success card appears at bottom when any keywords added
- Card styling changes to success state
- Badge changes to success badge with checkmark
-
Hide/Show:
- "Hide" button in header to collapse section
- Could add "Show High Opportunity Keywords" link when hidden
Error Handling
-
No active site:
- Don't render the section
- Show existing WorkflowGuide
-
No sectors:
- Don't render the section
- Show existing sector selection banner
-
API failures:
- Show toast error
- Log to console
- Don't crash the page
-
No keywords found:
- Show empty state or hide section
- Log warning to console
Performance Considerations
-
Lazy loading:
- Only load opportunity keywords when section is visible
- Use
showHighOpportunitystate flag
-
Caching:
- Store loaded keyword data in state
- Only reload on site/sector change
-
Batch API calls:
- Load all sector keywords in parallel if possible
- Use Promise.all() for concurrent requests
-
Pagination:
- Fetch 500 keywords max per sector
- This should cover top 50 for both options with margin
Testing Scenarios
-
Happy path:
- User has active site with 3 sectors
- Each sector has 50+ keywords
- Click "Add All" on each option
- Verify keywords added to workflow
- Verify success feedback shown
-
Edge cases:
- No active site: Section hidden
- No sectors: Section hidden
- No keywords for sector: Empty cards
- Already added keywords: Show "Added" badge
- API failure: Show error toast
-
Interaction:
- Add from high opportunity section
- Verify table below refreshes
- Verify count updates in header
- Verify keywords marked as "Added" in table
-
Responsive:
- Test on mobile, tablet, desktop
- Verify grid layout adapts
- Verify cards are readable
Implementation Steps
Phase 1: Core Functionality (Day 1)
- ✅ Create plan document (this file)
- Add state management and types
- Implement
loadHighOpportunityKeywords()function - Implement
handleAddSectorKeywords()function - Test data loading and API integration
Phase 2: UI Components (Day 1-2)
- Create
HighOpportunityKeywordsSectioncomponent - Build sector cards with options
- Add loading states
- Add success states and feedback
- Style according to design system
Phase 3: Integration (Day 2)
- Integrate with existing page
- Connect to existing state (activeSite, activeSector)
- Ensure table refreshes after keywords added
- Test hide/show functionality
Phase 4: Polish & Testing (Day 2-3)
- Add responsive styles
- Test on different screen sizes
- Handle edge cases and errors
- Add tooltips and help text
- Performance optimization
- Code review and cleanup
Dependencies
-
✅ Existing API functions in
services/api.ts -
✅
fetchSeedKeywords()- Fetch seed keywords -
✅
fetchSiteSectors()- Get sectors for site -
✅
fetchIndustries()- Get industry data -
✅
fetchKeywords()- Check already-attached keywords -
✅
addSeedKeywordsToWorkflow()- Bulk add keywords -
✅ Existing components
-
✅
Cardcomponent -
✅
Badgecomponent -
✅
Buttoncomponent -
✅
CheckCircleIconicon
Success Criteria
-
Functional:
- High opportunity keywords section displays at top of page
- Shows sectors with 2 options each (High Volume, Low Difficulty)
- "Add All" button adds keywords to workflow successfully
- Success feedback shown after adding
- Main table refreshes to show added keywords
- Hide/show functionality works
-
Visual:
- Matches wizard step 4 design
- Responsive on all screen sizes
- Success states clearly visible
- Loading states smooth
-
UX:
- Fast loading (< 2 seconds)
- Clear feedback on actions
- Tooltips help users understand
- Graceful error handling
- No blocking errors
Future Enhancements
- Expand/collapse sectors: Allow users to collapse individual sectors
- Keyword preview modal: Click on "+X more" to see full list
- Custom keyword counts: Let users choose how many keywords to add (25, 50, 100)
- Filter by metrics: Show only keywords above certain volume/below certain difficulty
- Save preferences: Remember hidden/shown state
- Analytics: Track which options users add most frequently
- Smart recommendations: Suggest sectors based on site content
- Batch operations: "Add all high volume" button to add from all sectors at once
Notes
- This feature improves onboarding by bringing wizard functionality into the main app
- Users can quickly populate their workflow without searching/filtering
- Reduces friction for new users who don't know which keywords to target
- Leverages existing curated seed keyword database
- No new API endpoints required - uses existing bulk add functionality
Related Files
Frontend:
/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx- Target file for implementation/frontend/src/components/onboarding/steps/Step4AddKeywords.tsx- Reference implementation/frontend/src/services/api.ts- API functions
Backend:
/backend/igny8_core/api/views/keywords.py- Keywords API/backend/igny8_core/api/views/seed_keywords.py- Seed keywords API/backend/igny8_core/business/seed_keywords.py- Bulk add logic
Docs:
/docs/40-WORKFLOWS/ADD_KEYWORDS.md- Add keywords workflow documentation/docs/10-MODULES/PLANNER.md- Planner module documentation