Blog SEO Analysis Modal - Updated with SEO Metadata Generator, Core Metadata Tab, and Metadata Display Components
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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' && (
|
||||
|
||||
377
frontend/src/components/BlogWriter/SEOMetadataModal.tsx
Normal file
377
frontend/src/components/BlogWriter/SEOMetadataModal.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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={() => {
|
||||
|
||||
Reference in New Issue
Block a user