Files
igny8/docs/plans/HIGH_OPPORTUNITY_KEYWORDS_ADD_KEYWORDS_PAGE.md
IGNY8 VPS (Salman) 5c4593359e KW_DB Udpates
2026-01-14 16:03:35 +00:00

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:

  1. Add imports:

    import { CheckCircleIcon } from '../../icons';
    import { fetchIndustries, fetchKeywords } from '../../services/api';
    
  2. Add state variables (see #1 above)

  3. Add useEffect to load opportunity keywords:

    useEffect(() => {
      if (activeSite && activeSite.id && showHighOpportunity) {
        loadHighOpportunityKeywords();
      }
    }, [activeSite?.id, showHighOpportunity]);
    
  4. 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

  1. "Add All" button:

    • Disabled during adding (shows "Adding...")
    • Disabled if no sector selected (with tooltip)
    • Disappears after successful add (replaced with "Added" badge)
  2. 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
  3. Hide/Show:

    • "Hide" button in header to collapse section
    • Could add "Show High Opportunity Keywords" link when hidden

Error Handling

  1. No active site:

    • Don't render the section
    • Show existing WorkflowGuide
  2. No sectors:

    • Don't render the section
    • Show existing sector selection banner
  3. API failures:

    • Show toast error
    • Log to console
    • Don't crash the page
  4. No keywords found:

    • Show empty state or hide section
    • Log warning to console

Performance Considerations

  1. Lazy loading:

    • Only load opportunity keywords when section is visible
    • Use showHighOpportunity state flag
  2. Caching:

    • Store loaded keyword data in state
    • Only reload on site/sector change
  3. Batch API calls:

    • Load all sector keywords in parallel if possible
    • Use Promise.all() for concurrent requests
  4. Pagination:

    • Fetch 500 keywords max per sector
    • This should cover top 50 for both options with margin

Testing Scenarios

  1. 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
  2. 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
  3. Interaction:

    • Add from high opportunity section
    • Verify table below refreshes
    • Verify count updates in header
    • Verify keywords marked as "Added" in table
  4. Responsive:

    • Test on mobile, tablet, desktop
    • Verify grid layout adapts
    • Verify cards are readable

Implementation Steps

Phase 1: Core Functionality (Day 1)

  1. Create plan document (this file)
  2. Add state management and types
  3. Implement loadHighOpportunityKeywords() function
  4. Implement handleAddSectorKeywords() function
  5. Test data loading and API integration

Phase 2: UI Components (Day 1-2)

  1. Create HighOpportunityKeywordsSection component
  2. Build sector cards with options
  3. Add loading states
  4. Add success states and feedback
  5. Style according to design system

Phase 3: Integration (Day 2)

  1. Integrate with existing page
  2. Connect to existing state (activeSite, activeSector)
  3. Ensure table refreshes after keywords added
  4. Test hide/show functionality

Phase 4: Polish & Testing (Day 2-3)

  1. Add responsive styles
  2. Test on different screen sizes
  3. Handle edge cases and errors
  4. Add tooltips and help text
  5. Performance optimization
  6. 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

  • Card component

  • Badge component

  • Button component

  • CheckCircleIcon icon

Success Criteria

  1. 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
  2. Visual:

    • Matches wizard step 4 design
    • Responsive on all screen sizes
    • Success states clearly visible
    • Loading states smooth
  3. UX:

    • Fast loading (< 2 seconds)
    • Clear feedback on actions
    • Tooltips help users understand
    • Graceful error handling
    • No blocking errors

Future Enhancements

  1. Expand/collapse sectors: Allow users to collapse individual sectors
  2. Keyword preview modal: Click on "+X more" to see full list
  3. Custom keyword counts: Let users choose how many keywords to add (25, 50, 100)
  4. Filter by metrics: Show only keywords above certain volume/below certain difficulty
  5. Save preferences: Remember hidden/shown state
  6. Analytics: Track which options users add most frequently
  7. Smart recommendations: Suggest sectors based on site content
  8. 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

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