Files
insta-recipe/docs/plans/RefactorSharePageAndEnhanceThumbnails.md
Giancarmine Salucci 7e4d82de8d feat(share): refactor page and enhance thumbnail extraction
- Extract 8 reusable components from monolithic share page
- Add LLM health indicator with 30s polling
- Implement stealth thumbnail extraction with 4-method cascade
- Integrate real-time thumbnail preview component
- Reduce share page from 306 to ~140 lines
- Add comprehensive outcome documentation

Components:
- UrlInputSection: URL input and extraction trigger
- ProgressIndicator: Loading state display
- ExtractedTextViewer: Collapsible text preview
- RecipeCard: Recipe display with Tandoor integration
- ErrorState: Error handling UI
- LogViewer: System logs with color coding
- LlmHealthIndicator: LLM status with polling
- ThumbnailPreview: Real-time thumbnail display

Thumbnail Methods:
1. Meta tag extraction (og:image, twitter:image)
2. Video poster attribute
3. Instagram embedded JSON data
4. Screenshot fallback

Stories Completed:
- Story 1: Component extraction and refactoring
- Story 2: LLM health status indicator
- Story 3: Enhanced stealth thumbnail extraction
- Story 4: Thumbnail preview integration

Closes: RefactorSharePageAndEnhanceThumbnails
2025-12-21 04:18:38 +01:00

27 KiB

Execution Plan: Refactor Share Page and Enhance Thumbnails

Outcome Name: RefactorSharePageAndEnhanceThumbnails
Created: 2025-12-21
Status: Ready for Implementation


Overview

This plan addresses three key improvements to the InstaChef PWA:

  1. Component Modularization: Split the monolithic 306-line share page into focused, reusable components
  2. LLM Health Monitoring: Add visual health status indicator for the LLM service
  3. Stealthy Thumbnail Extraction: Enhance thumbnail extraction with Instagram-friendly stealth techniques

Problem Statement

Current Issues

  1. Share Page Complexity: The +page.svelte file contains 306 lines with mixed concerns (state management, UI rendering, business logic), making it difficult to maintain and test
  2. No LLM Visibility: Users have no way to know if the LLM service is healthy before attempting extraction
  3. Basic Thumbnail Extraction: Current screenshot-based approach is detectable and may trigger Instagram's anti-bot measures

User Impact

  • Difficult to maintain and extend the share page functionality
  • Poor user experience when LLM service is down (only discover during extraction)
  • Risk of Instagram blocking due to detectable automation patterns

Technical Context

Current Architecture

Frontend:

  • Svelte 5.43.8 with modern runes ($state, $derived, $effect)
  • TailwindCSS 4.1.17 for styling
  • Share page uses snippets for UI modularity

Backend:

  • Playwright 1.56.1 for browser automation
  • Existing /api/llm-health endpoint for service monitoring
  • extractThumbnail() function uses screenshot-based approach

Hexagonal Architecture Alignment

  • Domain: extraction.ts contains business logic for thumbnail extraction
  • Adapters:
    • Primary (Driving): Svelte components, API routes
    • Secondary (Driven): Playwright Page interface
  • Ports: Clear interfaces between components and domain logic

Stories

Story 1: Refactor Share Page into Modular Components

Priority: High
Complexity: Medium
Estimated Effort: 4 hours

Description

Extract the current snippets from +page.svelte into standalone, reusable Svelte components. This improves maintainability, testability, and follows single responsibility principle.

Acceptance Criteria

  • Create src/routes/share/components/ directory
  • Extract 6 components from current snippets:
    1. UrlInputSection.svelte - URL input and extraction trigger
    2. ProgressIndicator.svelte - Loading state display
    3. ExtractedTextViewer.svelte - Collapsible text preview
    4. RecipeCard.svelte - Recipe display with Tandoor integration
    5. ErrorState.svelte - Error handling UI
    6. LogViewer.svelte - System logs display
  • Parent +page.svelte orchestrates state and passes props to components
  • Reduced +page.svelte from 306 to ~100 lines
  • All components use Svelte 5 runes ($state, $props)
  • Maintain existing functionality with no regressions
  • TailwindCSS styling preserved

Technical Specifications

Component Interfaces:

// UrlInputSection.svelte
interface Props {
    targetUrl: string | null;
    sharedText: string;
    sharedUrl: string;
    status: string;
    onProcess: () => void;
}

// ProgressIndicator.svelte
interface Props {
    status: string;
}

// ExtractedTextViewer.svelte
interface Props {
    bodyText: string;
}

// RecipeCard.svelte
interface Props {
    recipe: Recipe | null;
    tandoorEnabled: boolean;
    tandoorImporting: boolean;
    tandoorError: string | null;
    onRetry: () => void;
    onImportToTandoor: () => void;
}

// ErrorState.svelte
interface Props {
    status: string;
    bodyText: string;
    onRetry: () => void;
}

// LogViewer.svelte
interface Props {
    logs: string[];
    currentMethod: string;
    status: string;
}

Implementation Steps

  1. Create src/routes/share/components/ directory
  2. For each component:
    • Create new .svelte file
    • Extract relevant snippet code
    • Define props interface using let { prop1, prop2 } = $props()
    • Convert callbacks to prop functions
    • Preserve TailwindCSS classes
  3. Update +page.svelte:
    • Import all components
    • Remove snippet definitions
    • Replace {@render snippet()} with <Component />
    • Pass state and callbacks as props

Testing Strategy

  • Visual regression testing (manual verification)
  • Test each component in isolation
  • Verify state flow from parent to children
  • Verify callbacks work correctly
  • Test with real Instagram URL extraction

Files Modified

  • src/routes/share/+page.svelte

Files Created

  • src/routes/share/components/UrlInputSection.svelte
  • src/routes/share/components/ProgressIndicator.svelte
  • src/routes/share/components/ExtractedTextViewer.svelte
  • src/routes/share/components/RecipeCard.svelte
  • src/routes/share/components/ErrorState.svelte
  • src/routes/share/components/LogViewer.svelte
  • src/routes/share/components/ThumbnailPreview.svelte (Story 4)
  • src/routes/share/components/LlmHealthIndicator.svelte (Story 2)

Story 2: Add LLM Health Status Component

Priority: Medium
Complexity: Low
Estimated Effort: 2 hours

Description

Create a component that monitors the LLM service health using the existing /api/llm-health endpoint and displays a visual indicator to users.

Acceptance Criteria

  • Create LlmHealthIndicator.svelte component
  • Component polls /api/llm-health every 30 seconds
  • Visual indicator shows service status:
    • 🟢 Green: healthy
    • 🟡 Yellow: checking/loading
    • 🔴 Red: unhealthy/error
  • Tooltip/hover shows detailed status message
  • Polling starts on mount and cleans up on unmount
  • Component is non-blocking (doesn't prevent extraction)
  • Integrated into share page header area

Technical Specifications

API Contract:

// GET /api/llm-health response
{
    status: 'healthy' | 'unhealthy' | 'error';
    message: string;
}

Component Interface:

// LlmHealthIndicator.svelte
interface Props {
    pollInterval?: number; // default: 30000ms
}

interface HealthState {
    status: 'checking' | 'healthy' | 'unhealthy' | 'error';
    message: string;
    lastChecked: Date | null;
}

Implementation Pattern:

<script lang="ts">
    let { pollInterval = 30000 } = $props();
    
    let health = $state<HealthState>({
        status: 'checking',
        message: '',
        lastChecked: null
    });

    async function checkHealth() {
        try {
            const res = await fetch('/api/llm-health');
            const data = await res.json();
            health = {
                status: data.status === 'healthy' ? 'healthy' : 'unhealthy',
                message: data.message,
                lastChecked: new Date()
            };
        } catch (e) {
            health = {
                status: 'error',
                message: e instanceof Error ? e.message : 'Network error',
                lastChecked: new Date()
            };
        }
    }

    $effect(() => {
        checkHealth(); // Initial check
        const interval = setInterval(checkHealth, pollInterval);
        return () => clearInterval(interval);
    });
</script>

<div class="flex items-center gap-2 text-sm">
    <div class="flex items-center gap-1">
        {#if health.status === 'checking'}
            🟡 <span>Checking LLM...</span>
        {:else if health.status === 'healthy'}
            🟢 <span class="text-green-600">LLM Ready</span>
        {:else if health.status === 'unhealthy'}
            🔴 <span class="text-red-600">LLM Unavailable</span>
        {:else}
            🔴 <span class="text-red-600">LLM Error</span>
        {/if}
    </div>
    <div class="text-xs text-gray-500" title={health.message}>
        {health.lastChecked ? `Last: ${health.lastChecked.toLocaleTimeString()}` : ''}
    </div>
</div>

Implementation Steps

  1. Create src/routes/share/components/LlmHealthIndicator.svelte
  2. Implement health checking logic with polling
  3. Add visual status indicator with appropriate colors
  4. Implement cleanup in $effect return
  5. Add component to share page header
  6. Test polling behavior and visual states

Testing Strategy

  • Test all health states (checking, healthy, unhealthy, error)
  • Verify polling interval works correctly
  • Verify cleanup on component unmount
  • Test network error handling
  • Manual testing with LM Studio running/stopped

Files Created

  • src/routes/share/components/LlmHealthIndicator.svelte

Files Modified

  • src/routes/share/+page.svelte (add health indicator to header)

Story 3: Enhance Thumbnail Extraction with Stealth Techniques

Priority: High
Complexity: High
Estimated Effort: 6 hours

Description

Replace the basic screenshot-based thumbnail extraction with a multi-layered stealth approach that tries less detectable methods first, falling back to screenshots only when necessary.

Acceptance Criteria

  • Implement extractThumbnailStealth() function in extraction.ts
  • Try 4 extraction methods in order:
    1. Meta tags (og:image, twitter:image)
    2. Video poster attribute
    3. Instagram window data structures
    4. Screenshot fallback (improved)
  • Each method logged for debugging
  • Return base64 data URI for consistency
  • No new dependencies added
  • Backward compatible with existing code
  • Handle all edge cases (missing elements, CORS, etc.)
  • Add 'thumbnail' to ProgressEventType union
  • Emit progress event when thumbnail is extracted
  • Frontend receives thumbnail data in real-time via SSE

Technical Specifications

Research Findings:

From web research, Instagram thumbnails can be extracted using:

  1. Meta Tags (Most Stealthy):

    • og:image - OpenGraph thumbnail
    • twitter:image - Twitter card thumbnail
    • No detection risk, reads HTML only
  2. Video Poster Attribute:

    • <video poster="..."> attribute
    • Direct thumbnail URL
    • Low detection risk
  3. Instagram Data Structures:

    • window.__additionalDataLoaded object
    • GraphQL data in page
    • Medium detection risk
  4. Screenshot Fallback:

    • Existing method as last resort
    • High detection risk but guaranteed to work

Progress Event Integration:

Update the ProgressEventType to include thumbnail events:

export type ProgressEventType = 'status' | 'method' | 'retry' | 'error' | 'thumbnail' | 'complete';

Emit progress event when thumbnail is extracted:

// After successful thumbnail extraction
if (progressCallback) {
    progressCallback({
        type: 'thumbnail',
        message: 'Thumbnail extracted successfully',
        data: { thumbnail: thumbnailDataUri },
        timestamp: new Date().toISOString()
    });
}

Implementation:

/**
 * Extract thumbnail from Instagram post using stealth techniques
 * Tries multiple methods in order of stealth:
 * 1. Meta tags (og:image)
 * 2. Video poster attribute
 * 3. Instagram window data
 * 4. Screenshot fallback
 */
async function extractThumbnailStealth(page: Page, progressCallback?: ProgressCallback): Promise<string | null> {
    console.log('[Thumbnail] Starting stealth extraction');

    // Method 1: Try meta tags (most stealthy)
    try {
        const ogImage = await page.getAttribute('meta[property="og:image"]', 'content');
        if (ogImage) {
            console.log('[Thumbnail] Found og:image meta tag');
            const imageBuffer = await fetchImageAsBase64(ogImage);
            if (imageBuffer) {
                return imageBuffer;
            }
        }

        const twitterImage = await page.getAttribute('meta[name="twitter:image"]', 'content');
        if (twitterImage) {
            console.log('[Thumbnail] Found twitter:image meta tag');
            const imageBuffer = await fetchImageAsBase64(twitterImage);
            if (imageBuffer) {
                return imageBuffer;
            }
        }
    } catch (e) {
        console.log('[Thumbnail] Meta tag method failed:', e);
    }

    // Method 2: Try video poster attribute
    try {
        const poster = await page.getAttribute('video', 'poster');
        if (poster) {
            console.log('[Thumbnail] Found video poster attribute');
            const imageBuffer = await fetchImageAsBase64(poster);
            if (imageBuffer) {
                return imageBuffer;
            }
        }
    } catch (e) {
        console.log('[Thumbnail] Video poster method failed:', e);
    }

    // Method 3: Try Instagram window data structures
    try {
        const thumbnailUrl = await page.evaluate(() => {
            // Check for Instagram's internal data structures
            const data = (window as any).__additionalDataLoaded;
            if (data) {
                // Navigate through Instagram's data structure
                for (const key in data) {
                    const item = data[key];
                    if (item?.graphql?.shortcode_media?.display_url) {
                        return item.graphql.shortcode_media.display_url;
                    }
                    if (item?.graphql?.shortcode_media?.thumbnail_src) {
                        return item.graphql.shortcode_media.thumbnail_src;
                    }
                }
            }
            return null;
        });

        if (thumbnailUrl) {
            console.log('[Thumbnail] Found thumbnail in Instagram data structures');
            const imageBuffer = await fetchImageAsBase64(thumbnailUrl);
            if (imageBuffer) {
                return imageBuffer;
            }
        }
    } catch (e) {
        console.log('[Thumbnail] Instagram data method failed:', e);
    }

    // Method 4: Screenshot fallback (existing method)
    console.log('[Thumbnail] Falling back to screenshot method');
    return extractThumbnailScreenshot(page);
}

/**
 * Helper: Fetch image from URL and convert to base64 data URI
 */
async function fetchImageAsBase64(imageUrl: string): Promise<string | null> {
    try {
        const response = await fetch(imageUrl);
        if (!response.ok) return null;

        const arrayBuffer = await response.arrayBuffer();
        const buffer = Buffer.from(arrayBuffer);
        const contentType = response.headers.get('content-type') || 'image/jpeg';

        return `data:${contentType};base64,${buffer.toString('base64')}`;
    } catch (e) {
        console.error('[Thumbnail] Failed to fetch image:', e);
        return null;
    }
}

/**
 * Screenshot-based thumbnail extraction (existing method, renamed)
 */
async function extractThumbnailScreenshot(page: Page): Promise<string | null> {
    const videoBounds = await page.evaluate(() => {
        const video = document.querySelector('video');
        if (!video) return null;
        const rect = video.getBoundingClientRect();
        return {
            x: Math.max(0, rect.left),
            y: Math.max(0, rect.top),
            width: Math.min(rect.width, window.innerWidth),
            height: Math.min(rect.height, window.innerHeight)
        };
    });

    let screenshotBuffer: Buffer;

    if (videoBounds && videoBounds.width > 0 && videoBounds.height > 0) {
        screenshotBuffer = await page.screenshot({
            type: 'jpeg',
            quality: 85,
            clip: videoBounds
        });
    } else {
        console.warn('[Thumbnail] Video element not found, taking full page screenshot');
        screenshotBuffer = await page.screenshot({ type: 'jpeg', quality: 85 });
    }

    return `data:image/jpeg;base64,${screenshotBuffer.toString('base64')}`;
}

Implementation Steps

  1. Add 'thumbnail' to ProgressEventType union in extraction.ts
  2. Rename existing extractThumbnail() to extractThumbnailScreenshot()
  3. Create new extractThumbnailStealth() function with optional progressCallback parameter
  4. Implement meta tag extraction (Method 1)
  5. Implement video poster extraction (Method 2)
  6. Implement Instagram data structure extraction (Method 3)
  7. Implement fetchImageAsBase64() helper
  8. Emit progress event after successful thumbnail extraction
  9. Add comprehensive logging for debugging
  10. Update all extraction method calls to pass progressCallback
  11. Test with various Instagram posts (reels, videos, carousels)

Edge Cases & Error Handling

  • CORS Issues: Image URLs might have CORS restrictions - handle gracefully
  • Missing Elements: Not all posts have all meta tags - try multiple
  • Invalid URLs: Validate URLs before fetching
  • Network Errors: Timeout and retry logic
  • Instagram Format Changes: Fallback ensures functionality
  • Private/Deleted Posts: Handle gracefully with null return

Testing Strategy

  • Test with multiple Instagram post types:
    • Reels
    • Video posts
    • Carousel posts
    • Story highlights
  • Test each extraction method independently
  • Verify fallback chain works correctly
  • Test with network failures
  • Monitor Instagram's detection (no blocks)

Files Modified

  • src/lib/server/extraction.ts

Migration Notes

Replace all occurrences of:

const thumbnail = await extractThumbnail(page);

With:

const thumbnail = await extractThumbnailStealth(page);

The function signature remains the same, ensuring backward compatibility.


Story 4: Create Thumbnail Preview Component

Priority: Medium
Complexity: Low
Estimated Effort: 2 hours

Description

Create a dedicated component to display the extracted thumbnail in real-time as soon as it's available, separate from the recipe display. This provides immediate visual feedback to users during the extraction process.

Acceptance Criteria

  • Create ThumbnailPreview.svelte component
  • Component displays thumbnail image when available
  • Shows loading skeleton while thumbnail is being extracted
  • Shows error state if thumbnail extraction fails
  • Responsive design with proper aspect ratio
  • Integrates into share page between progress indicator and logs
  • Updates in real-time when thumbnail event is received
  • Maintains aspect ratio and prevents layout shift

Technical Specifications

Component Interface:

// ThumbnailPreview.svelte
interface Props {
    thumbnail: string | null;
    status: 'idle' | 'extracting' | 'success' | 'error';
}

Implementation Pattern:

<script lang="ts">
    let { thumbnail = null, status = 'idle' } = $props();
</script>

{#if status === 'extracting'}
    <div class="border rounded-lg p-4 bg-gray-50">
        <div class="flex items-center gap-3 mb-2">
            <div class="animate-spin text-blue-600">🎨</div>
            <span class="text-sm font-medium text-gray-700">Extracting thumbnail...</span>
        </div>
        <!-- Loading skeleton -->
        <div class="w-full aspect-square bg-gray-200 animate-pulse rounded"></div>
    </div>
{:else if status === 'success' && thumbnail}
    <div class="border rounded-lg p-4 bg-white shadow-sm">
        <div class="flex items-center gap-2 mb-2">
            <span class="text-green-600"></span>
            <span class="text-sm font-medium text-gray-700">Thumbnail extracted</span>
        </div>
        <img 
            src={thumbnail} 
            alt="Post thumbnail" 
            class="w-full aspect-square object-cover rounded"
        />
    </div>
{:else if status === 'error'}
    <div class="border border-red-200 rounded-lg p-4 bg-red-50">
        <div class="flex items-center gap-2">
            <span class="text-red-600"></span>
            <span class="text-sm font-medium text-red-700">Thumbnail extraction failed</span>
        </div>
    </div>
{/if}

Integration with Share Page:

Update the share page to:

  1. Add thumbnail state: let thumbnail = $state<string | null>(null)
  2. Add thumbnail status: let thumbnailStatus = $state<'idle' | 'extracting' | 'success' | 'error'>('idle')
  3. Listen for thumbnail progress events in SSE handler
  4. Update thumbnail state when event is received
// In the SSE event handler (+page.svelte)
if (event.type === 'thumbnail') {
    thumbnail = event.data?.thumbnail || null;
    thumbnailStatus = thumbnail ? 'success' : 'error';
    logs = [...logs, `🎨 ${event.message}`];
}

Implementation Steps

  1. Create src/routes/share/components/ThumbnailPreview.svelte
  2. Implement loading skeleton state
  3. Implement success state with image display
  4. Implement error state
  5. Add responsive styling with TailwindCSS
  6. Update +page.svelte to add thumbnail state variables
  7. Add thumbnail event handler to SSE processing
  8. Integrate component into share page layout
  9. Set thumbnailStatus to 'extracting' when extraction starts
  10. Test with real Instagram URLs

Layout Integration

The component should be placed in the share page layout as:

<div class="p-8 max-w-lg mx-auto space-y-4">
    <div class="flex items-center justify-between">
        <h1 class="text-2xl font-bold">InstaChef PWA</h1>
        <LlmHealthIndicator />
    </div>
    
    <UrlInputSection {targetUrl} {sharedText} {sharedUrl} {status} {onProcess} />
    <ProgressIndicator {status} />
    
    <!-- Thumbnail Preview - NEW -->
    <ThumbnailPreview {thumbnail} status={thumbnailStatus} />
    
    <ExtractedTextViewer {bodyText} />
    <RecipeCard {recipe} {tandoorEnabled} {tandoorImporting} {tandoorError} {onRetry} {onImportToTandoor} />
    <ErrorState {status} {bodyText} {onRetry} />
    <LogViewer {logs} {currentMethod} {status} />
</div>

Testing Strategy

  • Test loading state appears immediately when extraction starts
  • Test thumbnail appears when event is received
  • Test error state when thumbnail extraction fails
  • Verify no layout shift when thumbnail loads
  • Test responsive behavior on mobile devices
  • Verify aspect ratio is maintained
  • Test with various image sizes and formats

Files Created

  • src/routes/share/components/ThumbnailPreview.svelte

Files Modified

  • src/routes/share/+page.svelte (add thumbnail state and event handling)

User Experience Benefits

  • Immediate Feedback: Users see the thumbnail as soon as it's extracted, not waiting for full recipe
  • Visual Confirmation: Confirms correct post is being processed
  • Progressive Loading: Shows extraction progress step-by-step
  • Error Visibility: Clear indication if thumbnail extraction fails

Implementation Order

  1. Story 1: Component Refactoring (Foundation)

    • Establishes clean component structure
    • Makes future changes easier
  2. Story 2: LLM Health Indicator (Quick Win)

    • Independent of other stories
    • Provides immediate user value
  3. Story 3: Thumbnail Enhancement (Complex)

    • Implements stealth extraction methods
    • Adds progress event emission
    • Requires thorough testing
  4. Story 4: Thumbnail Preview Component (Integration)

    • Depends on Story 1 (component structure) and Story 3 (thumbnail events)
    • Displays extracted thumbnails in real-time
    • Enhances user experience

Dependencies & Prerequisites

External Dependencies

  • None (uses existing tech stack)

Internal Dependencies

  • Story 2 is independent and can be done anytime
  • Story 1 should be completed first (provides component structure)
  • Story 3 should be completed before Story 4 (provides thumbnail events)
  • Story 4 depends on Story 1 and Story 3

Environment Requirements

  • LM Studio running for Story 2 testing
  • Valid Instagram URLs for Story 3 testing

Risk Assessment

High Risk

  • Instagram Format Changes: Instagram may change their data structures
    • Mitigation: Multi-method approach with screenshot fallback

Medium Risk

  • Component Refactoring Errors: Breaking existing functionality during split
    • Mitigation: Thorough manual testing, incremental migration

Low Risk

  • LLM Health Polling Performance: Frequent polling might impact performance
    • Mitigation: Configurable interval, can be disabled if needed

Success Metrics

Code Quality

  • Share page reduced from 306 to ~100 lines
  • Component cohesion: each component < 80 lines
  • Test coverage: manual verification of all user flows

User Experience

  • LLM status visible before extraction attempt
  • Thumbnail extraction success rate > 95%
  • No Instagram blocking/detection

Performance

  • Page load time unchanged
  • Health polling doesn't impact extraction speed
  • Thumbnail extraction time reduced (meta tags are faster)

Documentation Updates

Code Documentation

  • JSDoc comments for all new functions
  • Component prop interfaces documented
  • Extraction methods logged for debugging

User Documentation

  • No user-facing docs needed (internal feature)

Rollback Plan

Story 1 Rollback

  • Revert +page.svelte to previous version
  • Delete component files

Story 2 Rollback

  • Remove LlmHealthIndicator import from share page
  • Delete component file

Story 3 Rollback

  • Revert extraction.ts changes
  • Restore original extractThumbnail() function
  • Remove 'thumbnail' from ProgressEventType

Story 4 Rollback

  • Remove ThumbnailPreview import from share page
  • Remove thumbnail state variables from +page.svelte
  • Remove thumbnail event handler from SSE processing
  • Delete ThumbnailPreview.svelte component file

Testing Checklist

Story 1: Component Refactoring

  • All components render correctly
  • State flows from parent to children
  • Callbacks trigger parent functions
  • Styling matches original
  • No console errors
  • Extraction flow works end-to-end

Story 2: LLM Health Indicator

  • Shows yellow "checking" on load
  • Shows green when LLM is healthy
  • Shows red when LLM is down
  • Polling works (check network tab)
  • Cleanup on unmount (no memory leaks)
  • Tooltip shows status message

Story 3: Thumbnail Enhancement

  • Meta tag extraction works
  • Video poster extraction works
  • Instagram data extraction works
  • Screenshot fallback works
  • All methods logged correctly
  • No Instagram blocking detected
  • Thumbnail progress event is emitted
  • Event includes thumbnail data

Story 4: Thumbnail Preview Component

  • Loading skeleton shows when extraction starts
  • Thumbnail displays when event received
  • Error state shows on extraction failure
  • No layout shift when thumbnail loads
  • Responsive on mobile devices
  • Aspect ratio maintained correctly
  • Component receives real-time updates

Estimated Total Effort

  • Story 1: 4 hours
  • Story 2: 2 hours
  • Story 3: 6 hours
  • Story 4: 2 hours
  • Testing & Integration: 2 hours

Total: ~16 hours


Next Steps

Once this plan is approved:

  1. Create feature branch: feature/refactor-share-page-thumbnails
  2. Implement stories in order (1 → 2 → 3)
  3. Test each story before moving to next
  4. Create pull request with comprehensive testing results
  5. Deploy to production after approval

References

External Documentation

Internal Documentation


Plan Status: Ready for Implementation
Developer Command: @dev RefactorSharePageAndEnhanceThumbnails