Blog SEO Analysis Modal - Updated with SEO Metadata Generator, Core Metadata Tab, and Metadata Display Components
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user