Blog SEO Analysis Modal - Updated with SEO Metadata Generator, Core Metadata Tab, and Metadata Display Components

This commit is contained in:
ajaysi
2025-09-23 16:21:09 +05:30
parent 12119d418b
commit a91677782e
16 changed files with 3433 additions and 89 deletions

View File

@@ -29,6 +29,7 @@ import { OutlineProgressModal } from './OutlineProgressModal';
import OutlineFeedbackForm from './OutlineFeedbackForm';
import { BlogEditor } from './WYSIWYG';
import { SEOAnalysisModal } from './SEOAnalysisModal';
import { SEOMetadataModal } from './SEOMetadataModal';
// Type assertion for CopilotKit action
const useCopilotActionTyped = useCopilotAction as any;
@@ -159,6 +160,7 @@ export const BlogWriter: React.FC = () => {
// SEO Analysis Modal state
const [isSEOAnalysisModalOpen, setIsSEOAnalysisModalOpen] = useState(false);
const [isSEOMetadataModalOpen, setIsSEOMetadataModalOpen] = useState(false);
useEffect(() => {
if ((mediumPolling.isPolling || rewritePolling.isPolling || isMediumGenerationStarting) && !showModal) {
@@ -268,6 +270,42 @@ export const BlogWriter: React.FC = () => {
}
});
// Generate SEO Metadata Action
useCopilotActionTyped({
name: "generateSEOMetadata",
description: "Generate comprehensive SEO metadata including titles, descriptions, Open Graph tags, Twitter cards, and structured data",
parameters: [
{
name: "title",
type: "string",
description: "Optional blog title to use for metadata generation",
required: false
}
],
handler: async ({ title }: { title?: string }) => {
console.log('🚀 Generate SEO Metadata Action Triggered!');
console.log('Title provided:', title);
console.log('Selected title:', selectedTitle);
console.log('Sections available:', !!sections && Object.keys(sections).length > 0);
console.log('Research data available:', !!research && !!research.keyword_analysis);
// Check if we have content to generate metadata for
if (!sections || Object.keys(sections).length === 0) {
return "Please generate blog content first before creating SEO metadata. Use the content generation features to create your blog post.";
}
if (!research || !research.keyword_analysis) {
return "Please complete research first to get keyword data for SEO metadata generation. Use the research features to gather keyword insights.";
}
// Open the SEO metadata modal
setIsSEOMetadataModalOpen(true);
console.log('SEO Metadata modal opened');
return "Opening SEO metadata generator! This will create optimized titles, descriptions, Open Graph tags, Twitter cards, and structured data for your blog post.";
}
});
@@ -373,10 +411,10 @@ export const BlogWriter: React.FC = () => {
<div>
{outlineConfirmed ? (
/* WYSIWYG Editor - Show when outline is confirmed */
<BlogEditor
<BlogEditor
outline={outline}
research={research}
initialTitle={selectedTitle}
initialTitle={selectedTitle || (typeof window !== 'undefined' ? localStorage.getItem('blog_selected_title') : '') || 'Your Amazing Blog Title'}
titleOptions={titleOptions}
researchTitles={researchTitles}
aiGeneratedTitles={aiGeneratedTitles}
@@ -586,12 +624,27 @@ Available tools:
isOpen={isSEOAnalysisModalOpen}
onClose={() => setIsSEOAnalysisModalOpen(false)}
blogContent={buildFullMarkdown()}
blogTitle={selectedTitle}
researchData={research}
onApplyRecommendations={(recommendations) => {
console.log('Applying SEO recommendations:', recommendations);
// TODO: Implement recommendation application logic
}}
/>
{/* SEO Metadata Modal */}
<SEOMetadataModal
isOpen={isSEOMetadataModalOpen}
onClose={() => setIsSEOMetadataModalOpen(false)}
blogContent={buildFullMarkdown()}
blogTitle={selectedTitle}
researchData={research}
onMetadataGenerated={(metadata) => {
console.log('SEO metadata generated:', metadata);
setSeoMetadata(metadata);
// TODO: Implement metadata application logic
}}
/>
</div>
);
};

View File

@@ -0,0 +1,394 @@
/**
* Core Metadata Tab Component
*
* Displays and allows editing of core SEO metadata including:
* - SEO Title
* - Meta Description
* - URL Slug
* - Blog Tags
* - Blog Categories
* - Social Hashtags
* - Reading Time
* - Focus Keyword
*/
import React from 'react';
import {
Box,
Typography,
TextField,
Chip,
Paper,
Grid,
IconButton,
Tooltip,
InputAdornment,
FormControl,
InputLabel,
Select,
MenuItem,
OutlinedInput,
Alert
} from '@mui/material';
import {
ContentCopy as CopyIcon,
Check as CheckIcon,
Search as SearchIcon,
Link as LinkIcon,
Tag as TagIcon,
Category as CategoryIcon,
Schedule as ScheduleIcon,
TrendingUp as TrendingUpIcon
} from '@mui/icons-material';
interface CoreMetadataTabProps {
metadata: any;
onMetadataEdit: (field: string, value: any) => void;
onCopyToClipboard: (text: string, itemId: string) => void;
copiedItems: Set<string>;
}
export const CoreMetadataTab: React.FC<CoreMetadataTabProps> = ({
metadata,
onMetadataEdit,
onCopyToClipboard,
copiedItems
}) => {
const handleTextFieldChange = (field: string) => (event: React.ChangeEvent<HTMLInputElement>) => {
onMetadataEdit(field, event.target.value);
};
const handleTagsChange = (field: string) => (event: any) => {
const value = typeof event.target.value === 'string' ? event.target.value.split(',') : event.target.value;
onMetadataEdit(field, value);
};
const getCharacterCountColor = (current: number, max: number) => {
if (current > max) return 'error';
if (current > max * 0.9) return 'warning';
return 'success';
};
const getCharacterCountText = (current: number, max: number) => {
if (current > max) return `${current}/${max} (Too long)`;
if (current > max * 0.9) return `${current}/${max} (Near limit)`;
return `${current}/${max}`;
};
return (
<Box>
<Typography variant="h6" sx={{ mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<SearchIcon sx={{ color: 'primary.main' }} />
Core SEO Metadata
</Typography>
<Grid container spacing={3}>
{/* SEO Title */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, display: 'flex', alignItems: 'center', gap: 1 }}>
<SearchIcon sx={{ fontSize: 20 }} />
SEO Title
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(metadata.seo_title || '', 'seo_title')}
>
{copiedItems.has('seo_title') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
multiline
rows={2}
value={metadata.seo_title || ''}
onChange={handleTextFieldChange('seo_title')}
placeholder="Enter SEO-optimized title (50-60 characters)"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Typography
variant="caption"
color={getCharacterCountColor((metadata.seo_title || '').length, 60)}
>
{getCharacterCountText((metadata.seo_title || '').length, 60)}
</Typography>
</InputAdornment>
)
}}
/>
<Alert severity="info" sx={{ mt: 1 }}>
Include your primary keyword and make it compelling for clicks
</Alert>
</Paper>
</Grid>
{/* Meta Description */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, display: 'flex', alignItems: 'center', gap: 1 }}>
<SearchIcon sx={{ fontSize: 20 }} />
Meta Description
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(metadata.meta_description || '', 'meta_description')}
>
{copiedItems.has('meta_description') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
multiline
rows={3}
value={metadata.meta_description || ''}
onChange={handleTextFieldChange('meta_description')}
placeholder="Enter compelling meta description (150-160 characters)"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Typography
variant="caption"
color={getCharacterCountColor((metadata.meta_description || '').length, 160)}
>
{getCharacterCountText((metadata.meta_description || '').length, 160)}
</Typography>
</InputAdornment>
)
}}
/>
<Alert severity="info" sx={{ mt: 1 }}>
Include a call-to-action and your primary keyword
</Alert>
</Paper>
</Grid>
{/* URL Slug */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, display: 'flex', alignItems: 'center', gap: 1 }}>
<LinkIcon sx={{ fontSize: 20 }} />
URL Slug
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(metadata.url_slug || '', 'url_slug')}
>
{copiedItems.has('url_slug') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={metadata.url_slug || ''}
onChange={handleTextFieldChange('url_slug')}
placeholder="seo-friendly-url-slug"
helperText="Use lowercase letters, numbers, and hyphens only"
/>
</Paper>
</Grid>
{/* Focus Keyword */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, display: 'flex', alignItems: 'center', gap: 1 }}>
<TrendingUpIcon sx={{ fontSize: 20 }} />
Focus Keyword
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(metadata.focus_keyword || '', 'focus_keyword')}
>
{copiedItems.has('focus_keyword') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={metadata.focus_keyword || ''}
onChange={handleTextFieldChange('focus_keyword')}
placeholder="primary-keyword"
helperText="Your main SEO keyword for this post"
/>
</Paper>
</Grid>
{/* Blog Tags */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, display: 'flex', alignItems: 'center', gap: 1 }}>
<TagIcon sx={{ fontSize: 20 }} />
Blog Tags
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard((metadata.blog_tags || []).join(', '), 'blog_tags')}
>
{copiedItems.has('blog_tags') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<FormControl fullWidth>
<InputLabel>Tags</InputLabel>
<Select
multiple
value={metadata.blog_tags || []}
onChange={handleTagsChange('blog_tags')}
input={<OutlinedInput label="Tags" />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value: string) => (
<Chip key={value} label={value} size="small" />
))}
</Box>
)}
>
{(metadata.blog_tags || []).map((tag: string) => (
<MenuItem key={tag} value={tag}>
{tag}
</MenuItem>
))}
</Select>
</FormControl>
<Alert severity="info" sx={{ mt: 1 }}>
Add relevant tags for better categorization and discoverability
</Alert>
</Paper>
</Grid>
{/* Blog Categories */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, display: 'flex', alignItems: 'center', gap: 1 }}>
<CategoryIcon sx={{ fontSize: 20 }} />
Blog Categories
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard((metadata.blog_categories || []).join(', '), 'blog_categories')}
>
{copiedItems.has('blog_categories') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<FormControl fullWidth>
<InputLabel>Categories</InputLabel>
<Select
multiple
value={metadata.blog_categories || []}
onChange={handleTagsChange('blog_categories')}
input={<OutlinedInput label="Categories" />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value: string) => (
<Chip key={value} label={value} size="small" color="primary" />
))}
</Box>
)}
>
{(metadata.blog_categories || []).map((category: string) => (
<MenuItem key={category} value={category}>
{category}
</MenuItem>
))}
</Select>
</FormControl>
<Alert severity="info" sx={{ mt: 1 }}>
Select 2-3 primary categories for your content
</Alert>
</Paper>
</Grid>
{/* Social Hashtags */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, display: 'flex', alignItems: 'center', gap: 1 }}>
<TagIcon sx={{ fontSize: 20 }} />
Social Hashtags
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard((metadata.social_hashtags || []).join(' '), 'social_hashtags')}
>
{copiedItems.has('social_hashtags') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<FormControl fullWidth>
<InputLabel>Hashtags</InputLabel>
<Select
multiple
value={metadata.social_hashtags || []}
onChange={handleTagsChange('social_hashtags')}
input={<OutlinedInput label="Hashtags" />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value: string) => (
<Chip key={value} label={value} size="small" color="secondary" />
))}
</Box>
)}
>
{(metadata.social_hashtags || []).map((hashtag: string) => (
<MenuItem key={hashtag} value={hashtag}>
{hashtag}
</MenuItem>
))}
</Select>
</FormControl>
<Alert severity="info" sx={{ mt: 1 }}>
Include # symbol for social media platforms
</Alert>
</Paper>
</Grid>
{/* Reading Time */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, display: 'flex', alignItems: 'center', gap: 1 }}>
<ScheduleIcon sx={{ fontSize: 20 }} />
Reading Time
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(`${metadata.reading_time || 0} minutes`, 'reading_time')}
>
{copiedItems.has('reading_time') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
type="number"
value={metadata.reading_time || 0}
onChange={handleTextFieldChange('reading_time')}
placeholder="5"
InputProps={{
endAdornment: <InputAdornment position="end">minutes</InputAdornment>
}}
helperText="Estimated reading time for your content"
/>
</Paper>
</Grid>
</Grid>
</Box>
);
};

View File

@@ -0,0 +1,388 @@
/**
* Preview Card Component
*
* Displays live previews of how the metadata will appear in:
* - Search engine results
* - Social media platforms
* - Rich snippets
*/
import React from 'react';
import {
Box,
Typography,
Paper,
Grid,
Card,
CardContent,
Chip,
Divider,
Alert,
IconButton,
Tooltip,
Button
} from '@mui/material';
import {
ContentCopy as CopyIcon,
Check as CheckIcon,
Search as SearchIcon,
Share as ShareIcon,
Code as CodeIcon,
Facebook as FacebookIcon,
Twitter as TwitterIcon,
LinkedIn as LinkedInIcon,
Google as GoogleIcon
} from '@mui/icons-material';
interface PreviewCardProps {
metadata: any;
blogTitle: string;
}
export const PreviewCard: React.FC<PreviewCardProps> = ({
metadata,
blogTitle
}) => {
const copyToClipboard = async (text: string, itemId: string) => {
try {
await navigator.clipboard.writeText(text);
// You could add a state to show "Copied!" feedback here
} catch (err) {
console.error('Failed to copy to clipboard:', err);
}
};
const getCurrentDate = () => {
return new Date().toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
};
const getCurrentTime = () => {
return new Date().toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit'
});
};
return (
<Box>
<Typography variant="h6" sx={{ mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<SearchIcon sx={{ color: 'primary.main' }} />
Live Preview
</Typography>
<Grid container spacing={3}>
{/* Google Search Results Preview */}
<Grid item xs={12}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<GoogleIcon sx={{ color: '#4285F4' }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Google Search Results
</Typography>
<Chip label="SERP Preview" size="small" color="primary" />
</Box>
<Card sx={{ border: '1px solid #e0e0e0', boxShadow: 'none' }}>
<CardContent sx={{ p: 2 }}>
{/* URL */}
<Typography variant="caption" sx={{ color: '#1a0dab', mb: 1, display: 'block' }}>
{metadata.canonical_url || 'https://yourwebsite.com/blog-post'}
</Typography>
{/* Title */}
<Typography
variant="h6"
sx={{
color: '#1a0dab',
fontWeight: 400,
fontSize: '1.1rem',
lineHeight: 1.3,
mb: 1,
cursor: 'pointer',
'&:hover': { textDecoration: 'underline' }
}}
>
{metadata.seo_title || blogTitle}
</Typography>
{/* Description */}
<Typography variant="body2" sx={{ color: '#4d5156', lineHeight: 1.4, mb: 1 }}>
{metadata.meta_description || 'Your meta description will appear here in Google search results...'}
</Typography>
{/* Additional Info */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1 }}>
<Typography variant="caption" sx={{ color: '#4d5156' }}>
{getCurrentDate()}
</Typography>
<Typography variant="caption" sx={{ color: '#4d5156' }}>
</Typography>
<Typography variant="caption" sx={{ color: '#4d5156' }}>
{metadata.reading_time || 5} min read
</Typography>
{metadata.blog_tags && metadata.blog_tags.length > 0 && (
<>
<Typography variant="caption" sx={{ color: '#4d5156' }}>
</Typography>
<Typography variant="caption" sx={{ color: '#4d5156' }}>
{metadata.blog_tags.slice(0, 2).join(', ')}
</Typography>
</>
)}
</Box>
</CardContent>
</Card>
<Alert severity="info" sx={{ mt: 2 }}>
This is how your blog post will appear in Google search results
</Alert>
</Paper>
</Grid>
{/* Social Media Previews */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<FacebookIcon sx={{ color: '#1877F2' }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Facebook Preview
</Typography>
<Chip label="Open Graph" size="small" color="primary" />
</Box>
<Card sx={{ border: '1px solid #e0e0e0', boxShadow: 'none', maxWidth: 400 }}>
<CardContent sx={{ p: 0 }}>
{/* Image placeholder */}
<Box sx={{
height: 200,
bgcolor: '#f5f5f5',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderBottom: '1px solid #e0e0e0'
}}>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
{metadata.open_graph?.image ? 'Image loaded' : 'No image set'}
</Typography>
</Box>
<Box sx={{ p: 2 }}>
{/* URL */}
<Typography variant="caption" sx={{ color: '#65676b', mb: 1, display: 'block' }}>
{metadata.canonical_url || 'yourwebsite.com'}
</Typography>
{/* Title */}
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 1, lineHeight: 1.3 }}>
{metadata.open_graph?.title || metadata.seo_title || blogTitle}
</Typography>
{/* Description */}
<Typography variant="body2" sx={{ color: '#65676b', lineHeight: 1.4 }}>
{metadata.open_graph?.description || metadata.meta_description || 'Your description will appear here...'}
</Typography>
</Box>
</CardContent>
</Card>
</Paper>
</Grid>
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<TwitterIcon sx={{ color: '#1DA1F2' }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Twitter Preview
</Typography>
<Chip label="Twitter Card" size="small" color="info" />
</Box>
<Card sx={{ border: '1px solid #e0e0e0', boxShadow: 'none', maxWidth: 400 }}>
<CardContent sx={{ p: 0 }}>
{/* Image placeholder */}
<Box sx={{
height: 200,
bgcolor: '#f5f5f5',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderBottom: '1px solid #e0e0e0'
}}>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
{metadata.twitter_card?.image ? 'Image loaded' : 'No image set'}
</Typography>
</Box>
<Box sx={{ p: 2 }}>
{/* URL */}
<Typography variant="caption" sx={{ color: '#536471', mb: 1, display: 'block' }}>
{metadata.canonical_url || 'yourwebsite.com'}
</Typography>
{/* Title */}
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 1, lineHeight: 1.3 }}>
{metadata.twitter_card?.title || metadata.seo_title || blogTitle}
</Typography>
{/* Description */}
<Typography variant="body2" sx={{ color: '#536471', lineHeight: 1.4 }}>
{metadata.twitter_card?.description || metadata.meta_description || 'Your description will appear here...'}
</Typography>
{/* Twitter handle */}
{metadata.twitter_card?.site && (
<Typography variant="caption" sx={{ color: '#536471', mt: 1, display: 'block' }}>
{metadata.twitter_card.site}
</Typography>
)}
</Box>
</CardContent>
</Card>
</Paper>
</Grid>
{/* Rich Snippets Preview */}
<Grid item xs={12}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<CodeIcon sx={{ color: '#34A853' }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Rich Snippets Preview
</Typography>
<Chip label="JSON-LD Schema" size="small" color="success" />
</Box>
<Card sx={{ border: '1px solid #e0e0e0', boxShadow: 'none' }}>
<CardContent sx={{ p: 2 }}>
{/* Article Schema Preview */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
{metadata.json_ld_schema?.headline || metadata.seo_title || blogTitle}
</Typography>
<Chip label="Article" size="small" color="success" />
</Box>
<Typography variant="body2" sx={{ color: '#4d5156', mb: 2 }}>
{metadata.json_ld_schema?.description || metadata.meta_description || 'Article description...'}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, flexWrap: 'wrap' }}>
{metadata.json_ld_schema?.author?.name && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Typography variant="caption" sx={{ color: '#4d5156' }}>
By {metadata.json_ld_schema.author.name}
</Typography>
</Box>
)}
{metadata.json_ld_schema?.datePublished && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Typography variant="caption" sx={{ color: '#4d5156' }}>
{new Date(metadata.json_ld_schema.datePublished).toLocaleDateString()}
</Typography>
</Box>
)}
{metadata.reading_time && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Typography variant="caption" sx={{ color: '#4d5156' }}>
{metadata.reading_time} min read
</Typography>
</Box>
)}
{metadata.json_ld_schema?.wordCount && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Typography variant="caption" sx={{ color: '#4d5156' }}>
{metadata.json_ld_schema.wordCount} words
</Typography>
</Box>
)}
</Box>
{metadata.json_ld_schema?.keywords && metadata.json_ld_schema.keywords.length > 0 && (
<Box sx={{ mt: 2 }}>
<Typography variant="caption" sx={{ color: '#4d5156', display: 'block', mb: 1 }}>
Keywords:
</Typography>
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
{metadata.json_ld_schema.keywords.slice(0, 5).map((keyword: string, index: number) => (
<Chip key={index} label={keyword} size="small" variant="outlined" />
))}
</Box>
</Box>
)}
</CardContent>
</Card>
<Alert severity="success" sx={{ mt: 2 }}>
Rich snippets help search engines understand your content and may display additional information in search results
</Alert>
</Paper>
</Grid>
{/* Metadata Summary */}
<Grid item xs={12}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<SearchIcon />
Metadata Summary
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} sm={6} md={3}>
<Box sx={{ textAlign: 'center', p: 2, bgcolor: 'rgba(76, 175, 80, 0.1)', borderRadius: 2 }}>
<Typography variant="h4" sx={{ fontWeight: 600, color: 'success.main' }}>
{metadata.optimization_score || 0}%
</Typography>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
Optimization Score
</Typography>
</Box>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Box sx={{ textAlign: 'center', p: 2, bgcolor: 'rgba(33, 150, 243, 0.1)', borderRadius: 2 }}>
<Typography variant="h4" sx={{ fontWeight: 600, color: 'primary.main' }}>
{metadata.reading_time || 0}
</Typography>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
Reading Time (min)
</Typography>
</Box>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Box sx={{ textAlign: 'center', p: 2, bgcolor: 'rgba(156, 39, 176, 0.1)', borderRadius: 2 }}>
<Typography variant="h4" sx={{ fontWeight: 600, color: 'secondary.main' }}>
{metadata.blog_tags?.length || 0}
</Typography>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
Tags
</Typography>
</Box>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Box sx={{ textAlign: 'center', p: 2, bgcolor: 'rgba(255, 152, 0, 0.1)', borderRadius: 2 }}>
<Typography variant="h4" sx={{ fontWeight: 600, color: 'warning.main' }}>
{metadata.blog_categories?.length || 0}
</Typography>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
Categories
</Typography>
</Box>
</Grid>
</Grid>
</Paper>
</Grid>
</Grid>
</Box>
);
};

View File

@@ -0,0 +1,444 @@
/**
* Social Media Tab Component
*
* Displays and allows editing of social media metadata including:
* - Open Graph tags (Facebook, LinkedIn)
* - Twitter Card tags
* - Social media previews
*/
import React from 'react';
import {
Box,
Typography,
TextField,
Paper,
Grid,
IconButton,
Tooltip,
InputAdornment,
Alert,
Card,
CardContent,
Divider,
Chip
} from '@mui/material';
import {
ContentCopy as CopyIcon,
Check as CheckIcon,
Share as ShareIcon,
Facebook as FacebookIcon,
Twitter as TwitterIcon,
LinkedIn as LinkedInIcon,
Image as ImageIcon,
Link as LinkIcon
} from '@mui/icons-material';
interface SocialMediaTabProps {
metadata: any;
onMetadataEdit: (field: string, value: any) => void;
onCopyToClipboard: (text: string, itemId: string) => void;
copiedItems: Set<string>;
}
export const SocialMediaTab: React.FC<SocialMediaTabProps> = ({
metadata,
onMetadataEdit,
onCopyToClipboard,
copiedItems
}) => {
const handleTextFieldChange = (field: string) => (event: React.ChangeEvent<HTMLInputElement>) => {
onMetadataEdit(field, event.target.value);
};
const handleNestedFieldChange = (parentField: string, childField: string) => (event: React.ChangeEvent<HTMLInputElement>) => {
const currentValue = metadata[parentField] || {};
onMetadataEdit(parentField, {
...currentValue,
[childField]: event.target.value
});
};
const getCharacterCountColor = (current: number, max: number) => {
if (current > max) return 'error';
if (current > max * 0.9) return 'warning';
return 'success';
};
const getCharacterCountText = (current: number, max: number) => {
if (current > max) return `${current}/${max} (Too long)`;
if (current > max * 0.9) return `${current}/${max} (Near limit)`;
return `${current}/${max}`;
};
const openGraph = metadata.open_graph || {};
const twitterCard = metadata.twitter_card || {};
return (
<Box>
<Typography variant="h6" sx={{ mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<ShareIcon sx={{ color: 'primary.main' }} />
Social Media Metadata
</Typography>
<Grid container spacing={3}>
{/* Open Graph Section */}
<Grid item xs={12}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<FacebookIcon sx={{ color: '#1877F2' }} />
<LinkedInIcon sx={{ color: '#0077B5' }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Open Graph Tags
</Typography>
<Chip label="Facebook & LinkedIn" size="small" color="primary" />
</Box>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
OG Title
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(openGraph.title || '', 'og_title')}
>
{copiedItems.has('og_title') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={openGraph.title || ''}
onChange={handleNestedFieldChange('open_graph', 'title')}
placeholder="Open Graph title (60 characters max)"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Typography
variant="caption"
color={getCharacterCountColor((openGraph.title || '').length, 60)}
>
{getCharacterCountText((openGraph.title || '').length, 60)}
</Typography>
</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
OG Description
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(openGraph.description || '', 'og_description')}
>
{copiedItems.has('og_description') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
multiline
rows={2}
value={openGraph.description || ''}
onChange={handleNestedFieldChange('open_graph', 'description')}
placeholder="Open Graph description (160 characters max)"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Typography
variant="caption"
color={getCharacterCountColor((openGraph.description || '').length, 160)}
>
{getCharacterCountText((openGraph.description || '').length, 160)}
</Typography>
</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
OG Image URL
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(openGraph.image || '', 'og_image')}
>
{copiedItems.has('og_image') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={openGraph.image || ''}
onChange={handleNestedFieldChange('open_graph', 'image')}
placeholder="https://example.com/image.jpg"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<ImageIcon />
</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
OG URL
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(openGraph.url || '', 'og_url')}
>
{copiedItems.has('og_url') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={openGraph.url || ''}
onChange={handleNestedFieldChange('open_graph', 'url')}
placeholder="https://example.com/blog-post"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<LinkIcon />
</InputAdornment>
)
}}
/>
</Grid>
</Grid>
<Alert severity="info" sx={{ mt: 2 }}>
Open Graph tags are used by Facebook, LinkedIn, and other social platforms to display rich previews
</Alert>
</Paper>
</Grid>
{/* Twitter Card Section */}
<Grid item xs={12}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<TwitterIcon sx={{ color: '#1DA1F2' }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Twitter Card Tags
</Typography>
<Chip label="Twitter & X" size="small" color="info" />
</Box>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Twitter Title
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(twitterCard.title || '', 'twitter_title')}
>
{copiedItems.has('twitter_title') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={twitterCard.title || ''}
onChange={handleNestedFieldChange('twitter_card', 'title')}
placeholder="Twitter card title (70 characters max)"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Typography
variant="caption"
color={getCharacterCountColor((twitterCard.title || '').length, 70)}
>
{getCharacterCountText((twitterCard.title || '').length, 70)}
</Typography>
</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Twitter Description
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(twitterCard.description || '', 'twitter_description')}
>
{copiedItems.has('twitter_description') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
multiline
rows={2}
value={twitterCard.description || ''}
onChange={handleNestedFieldChange('twitter_card', 'description')}
placeholder="Twitter card description (200 characters max)"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Typography
variant="caption"
color={getCharacterCountColor((twitterCard.description || '').length, 200)}
>
{getCharacterCountText((twitterCard.description || '').length, 200)}
</Typography>
</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Twitter Image URL
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(twitterCard.image || '', 'twitter_image')}
>
{copiedItems.has('twitter_image') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={twitterCard.image || ''}
onChange={handleNestedFieldChange('twitter_card', 'image')}
placeholder="https://example.com/twitter-image.jpg"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<ImageIcon />
</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Twitter Site Handle
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(twitterCard.site || '', 'twitter_site')}
>
{copiedItems.has('twitter_site') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={twitterCard.site || ''}
onChange={handleNestedFieldChange('twitter_card', 'site')}
placeholder="@yourwebsite"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<TwitterIcon />
</InputAdornment>
)
}}
/>
</Grid>
</Grid>
<Alert severity="info" sx={{ mt: 2 }}>
Twitter cards provide rich previews when your content is shared on Twitter/X
</Alert>
</Paper>
</Grid>
{/* Social Media Preview */}
<Grid item xs={12}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
<ShareIcon />
Social Media Preview
</Typography>
<Grid container spacing={2}>
{/* Facebook Preview */}
<Grid item xs={12} md={6}>
<Card sx={{ border: '1px solid #e0e0e0' }}>
<CardContent sx={{ p: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
<FacebookIcon sx={{ color: '#1877F2' }} />
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Facebook Preview
</Typography>
</Box>
<Box sx={{ border: '1px solid #e0e0e0', borderRadius: 1, p: 2, bgcolor: '#f5f5f5' }}>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
{openGraph.title || 'Your Blog Title'}
</Typography>
<Typography variant="caption" sx={{ color: 'text.secondary', mb: 1, display: 'block' }}>
{openGraph.url || 'yourwebsite.com'}
</Typography>
<Typography variant="body2" sx={{ fontSize: '0.875rem' }}>
{openGraph.description || 'Your meta description will appear here...'}
</Typography>
</Box>
</CardContent>
</Card>
</Grid>
{/* Twitter Preview */}
<Grid item xs={12} md={6}>
<Card sx={{ border: '1px solid #e0e0e0' }}>
<CardContent sx={{ p: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
<TwitterIcon sx={{ color: '#1DA1F2' }} />
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Twitter Preview
</Typography>
</Box>
<Box sx={{ border: '1px solid #e0e0e0', borderRadius: 1, p: 2, bgcolor: '#f5f5f5' }}>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
{twitterCard.title || 'Your Blog Title'}
</Typography>
<Typography variant="caption" sx={{ color: 'text.secondary', mb: 1, display: 'block' }}>
{twitterCard.site || '@yourwebsite'}
</Typography>
<Typography variant="body2" sx={{ fontSize: '0.875rem' }}>
{twitterCard.description || 'Your Twitter description will appear here...'}
</Typography>
</Box>
</CardContent>
</Card>
</Grid>
</Grid>
</Paper>
</Grid>
</Grid>
</Box>
);
};

View File

@@ -0,0 +1,509 @@
/**
* Structured Data Tab Component
*
* Displays and allows editing of JSON-LD structured data including:
* - Article schema
* - Author information
* - Publisher details
* - Publication dates
* - Keywords and categories
*/
import React, { useState } from 'react';
import {
Box,
Typography,
TextField,
Paper,
Grid,
IconButton,
Tooltip,
InputAdornment,
Alert,
Card,
CardContent,
Divider,
Chip,
Accordion,
AccordionSummary,
AccordionDetails,
Button
} from '@mui/material';
import {
ContentCopy as CopyIcon,
Check as CheckIcon,
Code as CodeIcon,
Person as PersonIcon,
Business as BusinessIcon,
CalendarToday as CalendarIcon,
ExpandMore as ExpandMoreIcon,
Visibility as VisibilityIcon,
Edit as EditIcon
} from '@mui/icons-material';
interface StructuredDataTabProps {
metadata: any;
onMetadataEdit: (field: string, value: any) => void;
onCopyToClipboard: (text: string, itemId: string) => void;
copiedItems: Set<string>;
}
export const StructuredDataTab: React.FC<StructuredDataTabProps> = ({
metadata,
onMetadataEdit,
onCopyToClipboard,
copiedItems
}) => {
const [showRawJson, setShowRawJson] = useState(false);
const handleTextFieldChange = (field: string) => (event: React.ChangeEvent<HTMLInputElement>) => {
onMetadataEdit(field, event.target.value);
};
const handleNestedFieldChange = (parentField: string, childField: string) => (event: React.ChangeEvent<HTMLInputElement>) => {
const currentValue = metadata[parentField] || {};
onMetadataEdit(parentField, {
...currentValue,
[childField]: event.target.value
});
};
const handleAuthorFieldChange = (field: string) => (event: React.ChangeEvent<HTMLInputElement>) => {
const currentSchema = metadata.json_ld_schema || {};
const currentAuthor = currentSchema.author || {};
onMetadataEdit('json_ld_schema', {
...currentSchema,
author: {
...currentAuthor,
[field]: event.target.value
}
});
};
const handlePublisherFieldChange = (field: string) => (event: React.ChangeEvent<HTMLInputElement>) => {
const currentSchema = metadata.json_ld_schema || {};
const currentPublisher = currentSchema.publisher || {};
onMetadataEdit('json_ld_schema', {
...currentSchema,
publisher: {
...currentPublisher,
[field]: event.target.value
}
});
};
const handleSchemaFieldChange = (field: string) => (event: React.ChangeEvent<HTMLInputElement>) => {
const currentSchema = metadata.json_ld_schema || {};
onMetadataEdit('json_ld_schema', {
...currentSchema,
[field]: event.target.value
});
};
const getJsonLdSchema = () => {
const schema = metadata.json_ld_schema || {};
return JSON.stringify(schema, null, 2);
};
const copyJsonLdSchema = () => {
onCopyToClipboard(getJsonLdSchema(), 'json_ld_schema');
};
const jsonLdSchema = metadata.json_ld_schema || {};
const author = jsonLdSchema.author || {};
const publisher = jsonLdSchema.publisher || {};
return (
<Box>
<Typography variant="h6" sx={{ mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<CodeIcon sx={{ color: 'primary.main' }} />
Structured Data (JSON-LD)
</Typography>
<Grid container spacing={3}>
{/* Article Information */}
<Grid item xs={12}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<CodeIcon />
Article Schema
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Headline
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(jsonLdSchema.headline || '', 'schema_headline')}
>
{copiedItems.has('schema_headline') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={jsonLdSchema.headline || ''}
onChange={handleSchemaFieldChange('headline')}
placeholder="Article headline"
/>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Description
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(jsonLdSchema.description || '', 'schema_description')}
>
{copiedItems.has('schema_description') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
multiline
rows={2}
value={jsonLdSchema.description || ''}
onChange={handleSchemaFieldChange('description')}
placeholder="Article description"
/>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Main Entity URL
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(jsonLdSchema.mainEntityOfPage || '', 'schema_url')}
>
{copiedItems.has('schema_url') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={jsonLdSchema.mainEntityOfPage || ''}
onChange={handleSchemaFieldChange('mainEntityOfPage')}
placeholder="https://example.com/blog-post"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<CodeIcon />
</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Word Count
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(jsonLdSchema.wordCount?.toString() || '', 'schema_wordcount')}
>
{copiedItems.has('schema_wordcount') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
type="number"
value={jsonLdSchema.wordCount || ''}
onChange={handleSchemaFieldChange('wordCount')}
placeholder="1500"
InputProps={{
endAdornment: <InputAdornment position="end">words</InputAdornment>
}}
/>
</Grid>
</Grid>
</Paper>
</Grid>
{/* Author Information */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<PersonIcon />
Author Information
</Typography>
<Grid container spacing={2}>
<Grid item xs={12}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Author Name
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(author.name || '', 'author_name')}
>
{copiedItems.has('author_name') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={author.name || ''}
onChange={handleAuthorFieldChange('name')}
placeholder="Author Name"
/>
</Grid>
<Grid item xs={12}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Author Type
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(author['@type'] || '', 'author_type')}
>
{copiedItems.has('author_type') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={author['@type'] || ''}
onChange={handleAuthorFieldChange('@type')}
placeholder="Person"
/>
</Grid>
</Grid>
</Paper>
</Grid>
{/* Publisher Information */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<BusinessIcon />
Publisher Information
</Typography>
<Grid container spacing={2}>
<Grid item xs={12}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Publisher Name
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(publisher.name || '', 'publisher_name')}
>
{copiedItems.has('publisher_name') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={publisher.name || ''}
onChange={handlePublisherFieldChange('name')}
placeholder="Publisher Name"
/>
</Grid>
<Grid item xs={12}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Publisher Logo
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(publisher.logo || '', 'publisher_logo')}
>
{copiedItems.has('publisher_logo') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={publisher.logo || ''}
onChange={handlePublisherFieldChange('logo')}
placeholder="https://example.com/logo.png"
/>
</Grid>
</Grid>
</Paper>
</Grid>
{/* Publication Dates */}
<Grid item xs={12}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<CalendarIcon />
Publication Dates
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Date Published
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(jsonLdSchema.datePublished || '', 'date_published')}
>
{copiedItems.has('date_published') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
type="datetime-local"
value={jsonLdSchema.datePublished || ''}
onChange={handleSchemaFieldChange('datePublished')}
InputLabelProps={{ shrink: true }}
/>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Date Modified
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(jsonLdSchema.dateModified || '', 'date_modified')}
>
{copiedItems.has('date_modified') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
type="datetime-local"
value={jsonLdSchema.dateModified || ''}
onChange={handleSchemaFieldChange('dateModified')}
InputLabelProps={{ shrink: true }}
/>
</Grid>
</Grid>
</Paper>
</Grid>
{/* Keywords */}
<Grid item xs={12}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<CodeIcon />
Keywords & Categories
</Typography>
<Grid container spacing={2}>
<Grid item xs={12}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Keywords
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard((jsonLdSchema.keywords || []).join(', '), 'schema_keywords')}
>
{copiedItems.has('schema_keywords') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
multiline
rows={2}
value={(jsonLdSchema.keywords || []).join(', ')}
onChange={(e) => {
const keywords = e.target.value.split(',').map(k => k.trim()).filter(k => k);
handleSchemaFieldChange('keywords')({ target: { value: keywords } } as any);
}}
placeholder="keyword1, keyword2, keyword3"
helperText="Separate keywords with commas"
/>
</Grid>
</Grid>
</Paper>
</Grid>
{/* Raw JSON View */}
<Grid item xs={12}>
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CodeIcon />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Raw JSON-LD Schema
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ position: 'relative' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Complete JSON-LD Schema
</Typography>
<Button
variant="outlined"
size="small"
startIcon={copiedItems.has('json_ld_schema') ? <CheckIcon /> : <CopyIcon />}
onClick={copyJsonLdSchema}
>
{copiedItems.has('json_ld_schema') ? 'Copied!' : 'Copy JSON'}
</Button>
</Box>
<TextField
fullWidth
multiline
rows={15}
value={getJsonLdSchema()}
InputProps={{
readOnly: true,
sx: {
fontFamily: 'monospace',
fontSize: '0.875rem'
}
}}
sx={{
'& .MuiInputBase-input': {
fontFamily: 'monospace',
fontSize: '0.875rem'
}
}}
/>
</Box>
</AccordionDetails>
</Accordion>
</Grid>
{/* Information Alert */}
<Grid item xs={12}>
<Alert severity="info">
<Typography variant="body2">
<strong>JSON-LD Structured Data:</strong> This schema helps search engines understand your content
and may enable rich snippets in search results. The data follows Schema.org Article guidelines.
</Typography>
</Alert>
</Grid>
</Grid>
</Box>
);
};

View File

@@ -11,7 +11,8 @@ import {
Typography,
Paper,
Grid,
Chip
Chip,
Tooltip
} from '@mui/material';
import {
BarChart
@@ -29,6 +30,15 @@ interface StructureAnalysisProps {
structure_score: number;
recommendations: string[];
};
content_quality?: {
word_count: number;
unique_words: number;
vocabulary_diversity: number;
transition_words_used: number;
content_depth_score: number;
flow_score: number;
recommendations: string[];
};
heading_structure?: {
h1_count: number;
h2_count: number;
@@ -43,11 +53,6 @@ interface StructureAnalysisProps {
}
export const StructureAnalysis: React.FC<StructureAnalysisProps> = ({ detailedAnalysis }) => {
// Debug logging
console.log('🏗️ StructureAnalysis received data:', detailedAnalysis);
console.log('📊 Content Structure:', detailedAnalysis?.content_structure);
console.log('📋 Heading Structure:', detailedAnalysis?.heading_structure);
return (
<Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
@@ -64,30 +69,113 @@ export const StructureAnalysis: React.FC<StructureAnalysisProps> = ({ detailedAn
Structure Overview
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Total Sections</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_structure?.total_sections || 'N/A'}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Total Paragraphs</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_structure?.total_paragraphs || 'N/A'}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Total Sentences</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_structure?.total_sentences || 'N/A'}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Structure Score</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_structure?.structure_score || 'N/A'}
</Typography>
</Box>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Total Sections
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Number of main content sections (H2 headings) in your blog post.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Optimal Range:</strong> 3-8 sections for most blog posts
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>Why it matters:</strong> Good sectioning improves readability and helps search engines understand your content structure.
</Typography>
</Box>
}
arrow
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'help' }}>
<Typography variant="body2">Total Sections</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_structure?.total_sections || 'N/A'}
</Typography>
</Box>
</Tooltip>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Total Paragraphs
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Number of paragraphs in your content (excluding headings).
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Optimal Range:</strong> 8-20 paragraphs for most blog posts
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>Why it matters:</strong> Appropriate paragraph count indicates good content depth and organization.
</Typography>
</Box>
}
arrow
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'help' }}>
<Typography variant="body2">Total Paragraphs</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_structure?.total_paragraphs || 'N/A'}
</Typography>
</Box>
</Tooltip>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Total Sentences
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Total number of sentences in your content.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Optimal Range:</strong> 40-100 sentences for most blog posts
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>Why it matters:</strong> Sentence count affects readability and content comprehensiveness.
</Typography>
</Box>
}
arrow
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'help' }}>
<Typography variant="body2">Total Sentences</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_structure?.total_sentences || 'N/A'}
</Typography>
</Box>
</Tooltip>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Structure Score
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Overall score (0-100) for your content's structural organization.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Scoring Factors:</strong> Section count, paragraph count, introduction/conclusion presence
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>Why it matters:</strong> Well-structured content ranks better and provides better user experience.
</Typography>
</Box>
}
arrow
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'help' }}>
<Typography variant="body2">Structure Score</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_structure?.structure_score || 'N/A'}
</Typography>
</Box>
</Tooltip>
</Box>
</Paper>
</Grid>
@@ -99,35 +187,296 @@ export const StructureAnalysis: React.FC<StructureAnalysisProps> = ({ detailedAn
Content Elements
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Has Introduction</Typography>
<Chip
label={detailedAnalysis?.content_structure?.has_introduction ? 'Yes' : 'No'}
color={detailedAnalysis?.content_structure?.has_introduction ? 'success' : 'error'}
size="small"
/>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Has Conclusion</Typography>
<Chip
label={detailedAnalysis?.content_structure?.has_conclusion ? 'Yes' : 'No'}
color={detailedAnalysis?.content_structure?.has_conclusion ? 'success' : 'error'}
size="small"
/>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Has Call to Action</Typography>
<Chip
label={detailedAnalysis?.content_structure?.has_call_to_action ? 'Yes' : 'No'}
color={detailedAnalysis?.content_structure?.has_call_to_action ? 'success' : 'error'}
size="small"
/>
</Box>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Introduction Section
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Whether your content has a clear introduction that sets context and expectations.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Why it matters:</strong> Introductions help readers understand what they'll learn and improve engagement.
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>SEO Impact:</strong> Clear introductions help search engines understand your content's purpose.
</Typography>
</Box>
}
arrow
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'help' }}>
<Typography variant="body2">Has Introduction</Typography>
<Chip
label={detailedAnalysis?.content_structure?.has_introduction ? 'Yes' : 'No'}
color={detailedAnalysis?.content_structure?.has_introduction ? 'success' : 'error'}
size="small"
/>
</Box>
</Tooltip>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Conclusion Section
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Whether your content has a clear conclusion that summarizes key points.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Why it matters:</strong> Conclusions help readers retain information and provide closure.
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>SEO Impact:</strong> Good conclusions can improve time on page and reduce bounce rate.
</Typography>
</Box>
}
arrow
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'help' }}>
<Typography variant="body2">Has Conclusion</Typography>
<Chip
label={detailedAnalysis?.content_structure?.has_conclusion ? 'Yes' : 'No'}
color={detailedAnalysis?.content_structure?.has_conclusion ? 'success' : 'error'}
size="small"
/>
</Box>
</Tooltip>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Call to Action
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Whether your content includes a clear call to action for readers.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Why it matters:</strong> CTAs guide readers to take desired actions and improve conversion rates.
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>SEO Impact:</strong> Strong CTAs can improve user engagement metrics.
</Typography>
</Box>
}
arrow
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'help' }}>
<Typography variant="body2">Has Call to Action</Typography>
<Chip
label={detailedAnalysis?.content_structure?.has_call_to_action ? 'Yes' : 'No'}
color={detailedAnalysis?.content_structure?.has_call_to_action ? 'success' : 'error'}
size="small"
/>
</Box>
</Tooltip>
</Box>
</Paper>
</Grid>
</Grid>
{/* Content Quality Metrics */}
<Grid container spacing={3} sx={{ mt: 2 }}>
<Grid item xs={12}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 2 }}>
Content Quality Metrics
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} md={4}>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Word Count
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Total number of words in your content.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Optimal Range:</strong> 800-2000 words for most blog posts
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>Why it matters:</strong> Longer content typically ranks better and provides more value to readers.
</Typography>
</Box>
}
arrow
>
<Box sx={{ p: 2, background: 'rgba(76, 175, 80, 0.1)', borderRadius: 2, border: '1px solid rgba(76, 175, 80, 0.3)', cursor: 'help' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'success.main', mb: 1 }}>
Word Count
</Typography>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
{detailedAnalysis?.content_quality?.word_count || 'N/A'}
</Typography>
</Box>
</Tooltip>
</Grid>
<Grid item xs={12} md={4}>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Vocabulary Diversity
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Ratio of unique words to total words, indicating content variety.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Optimal Range:</strong> 0.4-0.7 (40-70% unique words)
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>Why it matters:</strong> Higher diversity indicates richer, more engaging content.
</Typography>
</Box>
}
arrow
>
<Box sx={{ p: 2, background: 'rgba(33, 150, 243, 0.1)', borderRadius: 2, border: '1px solid rgba(33, 150, 243, 0.3)', cursor: 'help' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'primary.main', mb: 1 }}>
Vocabulary Diversity
</Typography>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
{detailedAnalysis?.content_quality?.vocabulary_diversity ?
(detailedAnalysis.content_quality.vocabulary_diversity * 100).toFixed(1) + '%' : 'N/A'}
</Typography>
</Box>
</Tooltip>
</Grid>
<Grid item xs={12} md={4}>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Content Depth Score
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Score (0-100) indicating how comprehensive and detailed your content is.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Scoring Factors:</strong> Word count, section depth, information density
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>Why it matters:</strong> Deeper content provides more value and ranks better in search results.
</Typography>
</Box>
}
arrow
>
<Box sx={{ p: 2, background: 'rgba(156, 39, 176, 0.1)', borderRadius: 2, border: '1px solid rgba(156, 39, 176, 0.3)', cursor: 'help' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'secondary.main', mb: 1 }}>
Content Depth Score
</Typography>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
{detailedAnalysis?.content_quality?.content_depth_score || 'N/A'}
</Typography>
</Box>
</Tooltip>
</Grid>
<Grid item xs={12} md={4}>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Flow Score
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Score (0-100) indicating how well your content flows from one idea to the next.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Scoring Factors:</strong> Transition words, sentence variety, logical progression
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>Why it matters:</strong> Good flow improves readability and keeps readers engaged.
</Typography>
</Box>
}
arrow
>
<Box sx={{ p: 2, background: 'rgba(255, 152, 0, 0.1)', borderRadius: 2, border: '1px solid rgba(255, 152, 0, 0.3)', cursor: 'help' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'warning.main', mb: 1 }}>
Flow Score
</Typography>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
{detailedAnalysis?.content_quality?.flow_score || 'N/A'}
</Typography>
</Box>
</Tooltip>
</Grid>
<Grid item xs={12} md={4}>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Transition Words
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Number of transition words used to connect ideas and improve flow.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Optimal Range:</strong> 5-15 transition words for most blog posts
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>Why it matters:</strong> Transition words improve readability and help readers follow your logic.
</Typography>
</Box>
}
arrow
>
<Box sx={{ p: 2, background: 'rgba(244, 67, 54, 0.1)', borderRadius: 2, border: '1px solid rgba(244, 67, 54, 0.3)', cursor: 'help' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'error.main', mb: 1 }}>
Transition Words
</Typography>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
{detailedAnalysis?.content_quality?.transition_words_used || 'N/A'}
</Typography>
</Box>
</Tooltip>
</Grid>
<Grid item xs={12} md={4}>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Unique Words
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Number of unique words used in your content.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Why it matters:</strong> More unique words indicate richer vocabulary and better content variety.
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>SEO Impact:</strong> Diverse vocabulary can help with semantic SEO and topic coverage.
</Typography>
</Box>
}
arrow
>
<Box sx={{ p: 2, background: 'rgba(0, 150, 136, 0.1)', borderRadius: 2, border: '1px solid rgba(0, 150, 136, 0.3)', cursor: 'help' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'info.main', mb: 1 }}>
Unique Words
</Typography>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
{detailedAnalysis?.content_quality?.unique_words || 'N/A'}
</Typography>
</Box>
</Tooltip>
</Grid>
</Grid>
</Paper>
</Grid>
</Grid>
{/* Heading Structure */}
<Grid container spacing={3} sx={{ mt: 2 }}>
<Grid item xs={12}>
@@ -184,10 +533,78 @@ export const StructureAnalysis: React.FC<StructureAnalysisProps> = ({ detailedAn
</Grid>
</Grid>
<Box sx={{ mt: 2, p: 2, background: 'rgba(0,0,0,0.02)', borderRadius: 2 }}>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
Heading Hierarchy Score: {detailedAnalysis?.heading_structure?.heading_hierarchy_score || 'N/A'}
</Typography>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Heading Hierarchy Score
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Score (0-100) indicating how well your heading structure follows SEO best practices.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Scoring Factors:</strong> H1 presence, logical hierarchy, keyword usage in headings
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>Why it matters:</strong> Good heading structure helps search engines understand your content and improves readability.
</Typography>
</Box>
}
arrow
>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1, cursor: 'help' }}>
Heading Hierarchy Score: {detailedAnalysis?.heading_structure?.heading_hierarchy_score || 'N/A'}
</Typography>
</Tooltip>
</Box>
{/* Structure Recommendations */}
{detailedAnalysis?.content_structure?.recommendations && detailedAnalysis.content_structure.recommendations.length > 0 && (
<Box sx={{ mt: 2, p: 2, background: 'rgba(255, 193, 7, 0.1)', borderRadius: 2, border: '1px solid rgba(255, 193, 7, 0.3)' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1, color: 'warning.main' }}>
Structure Recommendations
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
{detailedAnalysis.content_structure.recommendations.map((recommendation: string, index: number) => (
<Typography key={index} variant="caption" sx={{ display: 'block' }}>
• {recommendation}
</Typography>
))}
</Box>
</Box>
)}
{/* Heading Recommendations */}
{detailedAnalysis?.heading_structure?.recommendations && detailedAnalysis.heading_structure.recommendations.length > 0 && (
<Box sx={{ mt: 2, p: 2, background: 'rgba(33, 150, 243, 0.1)', borderRadius: 2, border: '1px solid rgba(33, 150, 243, 0.3)' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1, color: 'primary.main' }}>
Heading Recommendations
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
{detailedAnalysis.heading_structure.recommendations.map((recommendation: string, index: number) => (
<Typography key={index} variant="caption" sx={{ display: 'block' }}>
• {recommendation}
</Typography>
))}
</Box>
</Box>
)}
{/* Content Quality Recommendations */}
{detailedAnalysis?.content_quality?.recommendations && detailedAnalysis.content_quality.recommendations.length > 0 && (
<Box sx={{ mt: 2, p: 2, background: 'rgba(76, 175, 80, 0.1)', borderRadius: 2, border: '1px solid rgba(76, 175, 80, 0.3)' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1, color: 'success.main' }}>
Content Quality Recommendations
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
{detailedAnalysis.content_quality.recommendations.map((recommendation: string, index: number) => (
<Typography key={index} variant="caption" sx={{ display: 'block' }}>
{recommendation}
</Typography>
))}
</Box>
</Box>
)}
</Paper>
</Grid>
</Grid>

View File

@@ -141,6 +141,7 @@ interface SEOAnalysisModalProps {
isOpen: boolean;
onClose: () => void;
blogContent: string;
blogTitle?: string;
researchData: any;
onApplyRecommendations?: (recommendations: any[]) => void;
}
@@ -149,6 +150,7 @@ export const SEOAnalysisModal: React.FC<SEOAnalysisModalProps> = ({
isOpen,
onClose,
blogContent,
blogTitle,
researchData,
onApplyRecommendations
}) => {
@@ -192,6 +194,7 @@ export const SEOAnalysisModal: React.FC<SEOAnalysisModalProps> = ({
},
body: JSON.stringify({
blog_content: blogContent,
blog_title: blogTitle,
research_data: researchData
})
});
@@ -202,12 +205,6 @@ export const SEOAnalysisModal: React.FC<SEOAnalysisModalProps> = ({
const result = await response.json();
console.log('🔍 Backend SEO Analysis Response:', result);
console.log('📊 Category Scores:', result.category_scores);
console.log('💡 Recommendations:', result.actionable_recommendations);
console.log('🔍 Visualization Data:', result.visualization_data);
console.log('📝 Detailed Analysis:', result.detailed_analysis);
console.log('🏗️ Content Structure:', result.detailed_analysis?.content_structure);
console.log('📋 Heading Structure:', result.detailed_analysis?.heading_structure);
// Convert API response to frontend format - fail fast if data is missing
if (!result.success) {
@@ -610,9 +607,9 @@ export const SEOAnalysisModal: React.FC<SEOAnalysisModalProps> = ({
<Recommendations recommendations={analysisResult.actionable_recommendations} />
)}
{tabValue === 'keywords' && (
<KeywordAnalysis detailedAnalysis={analysisResult.detailed_analysis} />
)}
{tabValue === 'keywords' && (
<KeywordAnalysis detailedAnalysis={analysisResult.detailed_analysis} />
)}
{tabValue === 'readability' && (
<ReadabilityAnalysis
@@ -622,7 +619,15 @@ export const SEOAnalysisModal: React.FC<SEOAnalysisModalProps> = ({
)}
{tabValue === 'structure' && (
<StructureAnalysis detailedAnalysis={analysisResult.detailed_analysis} />
analysisResult ? (
<StructureAnalysis detailedAnalysis={analysisResult.detailed_analysis} />
) : (
<Box sx={{ p: 3, textAlign: 'center' }}>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
Loading structure analysis...
</Typography>
</Box>
)
)}
{tabValue === 'insights' && (

View File

@@ -0,0 +1,377 @@
/**
* SEO Metadata Modal Component
*
* Comprehensive SEO metadata generation and editing interface with:
* - Tabbed interface for different metadata types
* - Live preview of social media cards
* - Character counters and validation
* - Copy-to-clipboard functionality
* - Integration with backend metadata generation
*/
import React, { useState, useEffect } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Box,
Typography,
Tabs,
Tab,
Paper,
CircularProgress,
Alert,
IconButton,
Tooltip,
Chip,
Grid,
Card,
CardContent,
Divider,
TextField,
InputAdornment
} from '@mui/material';
import {
Close as CloseIcon,
ContentCopy as CopyIcon,
Check as CheckIcon,
Preview as PreviewIcon,
Search as SearchIcon,
Share as ShareIcon,
Code as CodeIcon,
Tag as TagIcon,
Refresh as RefreshIcon
} from '@mui/icons-material';
// Import metadata display components
import { CoreMetadataTab } from './SEO/MetadataDisplay/CoreMetadataTab';
import { SocialMediaTab } from './SEO/MetadataDisplay/SocialMediaTab';
import { StructuredDataTab } from './SEO/MetadataDisplay/StructuredDataTab';
import { PreviewCard } from './SEO/MetadataDisplay/PreviewCard';
interface SEOMetadataModalProps {
isOpen: boolean;
onClose: () => void;
blogContent: string;
blogTitle: string;
researchData: any;
onMetadataGenerated: (metadata: any) => void;
}
interface SEOMetadataResult {
success: boolean;
seo_title?: string;
meta_description?: string;
url_slug?: string;
blog_tags?: string[];
blog_categories?: string[];
social_hashtags?: string[];
open_graph?: any;
twitter_card?: any;
json_ld_schema?: any;
canonical_url?: string;
reading_time?: number;
focus_keyword?: string;
generated_at?: string;
optimization_score?: number;
error?: string;
}
export const SEOMetadataModal: React.FC<SEOMetadataModalProps> = ({
isOpen,
onClose,
blogContent,
blogTitle,
researchData,
onMetadataGenerated
}) => {
const [isGenerating, setIsGenerating] = useState(false);
const [metadataResult, setMetadataResult] = useState<SEOMetadataResult | null>(null);
const [error, setError] = useState<string | null>(null);
const [tabValue, setTabValue] = useState('core');
const [copiedItems, setCopiedItems] = useState<Set<string>>(new Set());
const [editableMetadata, setEditableMetadata] = useState<SEOMetadataResult | null>(null);
// Debug logging
useEffect(() => {
console.log('🔍 SEOMetadataModal render:', {
isOpen,
blogContent: blogContent?.length,
blogTitle,
researchData: !!researchData
});
}, [isOpen, blogContent, blogTitle, researchData]);
const generateMetadata = async () => {
try {
setIsGenerating(true);
setError(null);
setMetadataResult(null);
console.log('🚀 Starting SEO metadata generation...');
// Make API call to generate metadata
const response = await fetch('/api/blog/seo/metadata', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: blogContent,
title: blogTitle,
research_data: researchData
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
console.log('✅ SEO metadata generation response:', result);
if (!result.success) {
throw new Error(result.error || 'Metadata generation failed');
}
setMetadataResult(result);
setEditableMetadata(result);
console.log('📊 Metadata result set:', result);
} catch (err) {
console.error('❌ SEO metadata generation failed:', err);
setError(err instanceof Error ? err.message : 'Failed to generate SEO metadata');
} finally {
setIsGenerating(false);
}
};
const handleTabChange = (event: React.SyntheticEvent, newValue: string) => {
setTabValue(newValue);
};
const handleCopyToClipboard = async (text: string, itemId: string) => {
try {
await navigator.clipboard.writeText(text);
setCopiedItems(prev => new Set([...prev, itemId]));
setTimeout(() => {
setCopiedItems(prev => {
const newSet = new Set(prev);
newSet.delete(itemId);
return newSet;
});
}, 2000);
} catch (err) {
console.error('Failed to copy to clipboard:', err);
}
};
const handleMetadataEdit = (field: string, value: any) => {
if (editableMetadata) {
setEditableMetadata(prev => ({
...prev!,
[field]: value
}));
}
};
const handleApplyMetadata = () => {
if (editableMetadata) {
onMetadataGenerated(editableMetadata);
onClose();
}
};
const getTabIcon = (tabValue: string) => {
switch (tabValue) {
case 'core': return <SearchIcon />;
case 'social': return <ShareIcon />;
case 'structured': return <CodeIcon />;
case 'preview': return <PreviewIcon />;
default: return <TagIcon />;
}
};
const getTabLabel = (tabValue: string) => {
switch (tabValue) {
case 'core': return 'Core SEO';
case 'social': return 'Social Media';
case 'structured': return 'Structured Data';
case 'preview': return 'Preview';
default: return 'Metadata';
}
};
return (
<Dialog
open={isOpen}
onClose={onClose}
maxWidth="lg"
fullWidth
PaperProps={{
sx: {
background: 'rgba(255, 255, 255, 0.98)',
backdropFilter: 'blur(10px)',
borderRadius: 3,
minHeight: '80vh'
}
}}
>
<DialogTitle sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
pb: 1,
borderBottom: '1px solid rgba(0,0,0,0.1)'
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<TagIcon sx={{ color: 'primary.main' }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
SEO Metadata Generator
</Typography>
{metadataResult && (
<Chip
label={`${metadataResult.optimization_score || 0}% Optimized`}
color={metadataResult.optimization_score && metadataResult.optimization_score >= 80 ? 'success' :
metadataResult.optimization_score && metadataResult.optimization_score >= 60 ? 'warning' : 'error'}
size="small"
/>
)}
</Box>
<IconButton onClick={onClose} size="small">
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent sx={{ p: 0 }}>
{!metadataResult && !isGenerating && (
<Box sx={{ p: 4, textAlign: 'center' }}>
<Typography variant="h6" sx={{ mb: 2 }}>
Generate Comprehensive SEO Metadata
</Typography>
<Typography variant="body2" sx={{ mb: 3, color: 'text.secondary' }}>
Create optimized titles, descriptions, Open Graph tags, Twitter cards, and structured data for your blog post.
</Typography>
<Button
variant="contained"
size="large"
onClick={generateMetadata}
startIcon={<RefreshIcon />}
sx={{ px: 4 }}
>
Generate SEO Metadata
</Button>
</Box>
)}
{isGenerating && (
<Box sx={{ p: 4, textAlign: 'center' }}>
<CircularProgress size={60} sx={{ mb: 2 }} />
<Typography variant="h6" sx={{ mb: 1 }}>
Generating SEO Metadata...
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
Creating optimized titles, descriptions, and social media tags
</Typography>
</Box>
)}
{error && (
<Box sx={{ p: 3 }}>
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
<Button
variant="outlined"
onClick={generateMetadata}
startIcon={<RefreshIcon />}
>
Try Again
</Button>
</Box>
)}
{metadataResult && (
<Box>
{/* Tabs */}
<Box sx={{ borderBottom: 1, borderColor: 'divider', px: 3 }}>
<Tabs
value={tabValue}
onChange={handleTabChange}
variant="scrollable"
scrollButtons="auto"
sx={{ minHeight: 48 }}
>
{['core', 'social', 'structured', 'preview'].map((tab) => (
<Tab
key={tab}
value={tab}
label={getTabLabel(tab)}
icon={getTabIcon(tab)}
iconPosition="start"
sx={{ minHeight: 48, textTransform: 'none' }}
/>
))}
</Tabs>
</Box>
{/* Tab Content */}
<Box sx={{ p: 3 }}>
{tabValue === 'core' && (
<CoreMetadataTab
metadata={editableMetadata || metadataResult}
onMetadataEdit={handleMetadataEdit}
onCopyToClipboard={handleCopyToClipboard}
copiedItems={copiedItems}
/>
)}
{tabValue === 'social' && (
<SocialMediaTab
metadata={editableMetadata || metadataResult}
onMetadataEdit={handleMetadataEdit}
onCopyToClipboard={handleCopyToClipboard}
copiedItems={copiedItems}
/>
)}
{tabValue === 'structured' && (
<StructuredDataTab
metadata={editableMetadata || metadataResult}
onMetadataEdit={handleMetadataEdit}
onCopyToClipboard={handleCopyToClipboard}
copiedItems={copiedItems}
/>
)}
{tabValue === 'preview' && (
<PreviewCard
metadata={editableMetadata || metadataResult}
blogTitle={blogTitle}
/>
)}
</Box>
</Box>
)}
</DialogContent>
{metadataResult && (
<DialogActions sx={{ p: 3, borderTop: '1px solid rgba(0,0,0,0.1)' }}>
<Button onClick={onClose} color="inherit">
Cancel
</Button>
<Button
variant="contained"
onClick={handleApplyMetadata}
startIcon={<CheckIcon />}
sx={{ px: 3 }}
>
Apply Metadata
</Button>
</DialogActions>
)}
</Dialog>
);
};

View File

@@ -114,9 +114,9 @@ const BlogEditor: React.FC<BlogEditorProps> = ({
<h1
className="flex-1 text-2xl md:text-4xl font-bold font-serif text-gray-900 leading-tight cursor-pointer hover:bg-gray-50 p-2 rounded-md transition-colors duration-200"
style={{
wordBreak: 'break-word',
overflowWrap: 'break-word',
whiteSpace: 'normal',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
lineHeight: '1.3'
}}
onClick={() => {