feat: remove canonical URL field and add URL validation
All checks were successful
Deploy to Production / deploy (push) Successful in 2m1s

- Removed canonical URL field from MetadataPanel component and related interfaces
- Added URL validation function to ensure feature image and canonical URLs are valid HTTP/HTTPS
- Updated Ghost API to skip invalid URLs when publishing posts
- Simplified metadata state management by removing canonicalUrl from initial state and update handlers
This commit is contained in:
Ender 2025-10-28 15:13:12 +01:00
parent 3e3b314407
commit b8f0da4644
4 changed files with 16 additions and 9 deletions

View File

@ -3,7 +3,6 @@ import { Box, Stack, TextField, Typography, IconButton } from '@mui/material';
export type Metadata = { export type Metadata = {
title: string; title: string;
tagsText: string; // comma-separated tagsText: string; // comma-separated
canonicalUrl: string;
featureImage?: string; featureImage?: string;
}; };
@ -20,7 +19,6 @@ export default function MetadataPanel({ value, onChange }: {
<Stack spacing={1}> <Stack spacing={1}>
<TextField label="Title" value={value.title} onChange={(e) => set({ title: e.target.value })} fullWidth /> <TextField label="Title" value={value.title} onChange={(e) => set({ title: e.target.value })} fullWidth />
<TextField label="Tags (comma-separated)" value={value.tagsText} onChange={(e) => set({ tagsText: e.target.value })} fullWidth /> <TextField label="Tags (comma-separated)" value={value.tagsText} onChange={(e) => set({ tagsText: e.target.value })} fullWidth />
<TextField label="Canonical URL" value={value.canonicalUrl} onChange={(e) => set({ canonicalUrl: e.target.value })} fullWidth />
<TextField label="Feature Image URL" value={value.featureImage || ''} onChange={(e) => set({ featureImage: e.target.value })} fullWidth /> <TextField label="Feature Image URL" value={value.featureImage || ''} onChange={(e) => set({ featureImage: e.target.value })} fullWidth />
{value.featureImage && ( {value.featureImage && (
<Box sx={{ position: 'relative', border: '1px solid #eee', borderRadius: 1, overflow: 'hidden', height: 180, background: '#fafafa' }}> <Box sx={{ position: 'relative', border: '1px solid #eee', borderRadius: 1, overflow: 'hidden', height: 180, background: '#fafafa' }}>

View File

@ -41,7 +41,6 @@ export default function StepMetadata({
const newMetadata: Metadata = { const newMetadata: Metadata = {
title: metadata.title, title: metadata.title,
tagsText: metadata.tags, tagsText: metadata.tags,
canonicalUrl: metadata.canonicalUrl,
featureImage: value.featureImage, // Keep existing feature image featureImage: value.featureImage, // Keep existing feature image
}; };

View File

@ -6,7 +6,7 @@ import type { Metadata } from '../components/MetadataPanel';
export function usePostEditor(initialPostId?: string | null) { export function usePostEditor(initialPostId?: string | null) {
const [postId, setPostId] = useState<string | null>(null); const [postId, setPostId] = useState<string | null>(null);
const [draft, setDraft] = useState<string>(''); const [draft, setDraft] = useState<string>('');
const [meta, setMeta] = useState<Metadata>({ title: '', tagsText: '', canonicalUrl: '', featureImage: '' }); const [meta, setMeta] = useState<Metadata>({ title: '', tagsText: '', featureImage: '' });
const [postClips, setPostClips] = useState<Array<{ id: string; bucket: string; key: string; mime: string; transcript?: string; createdAt: string }>>([]); const [postClips, setPostClips] = useState<Array<{ id: string; bucket: string; key: string; mime: string; transcript?: string; createdAt: string }>>([]);
const [postStatus, setPostStatus] = useState<'inbox' | 'editing' | 'ready_for_publish' | 'published' | 'archived'>('editing'); const [postStatus, setPostStatus] = useState<'inbox' | 'editing' | 'ready_for_publish' | 'published' | 'archived'>('editing');
const [promptText, setPromptText] = useState<string>(''); const [promptText, setPromptText] = useState<string>('');
@ -46,7 +46,6 @@ export function usePostEditor(initialPostId?: string | null) {
...m, ...m,
title: data.title || '', title: data.title || '',
tagsText: data.tagsText || '', tagsText: data.tagsText || '',
canonicalUrl: data.canonicalUrl || '',
featureImage: data.featureImage || '' featureImage: data.featureImage || ''
})); }));
if (data.status) setPostStatus(data.status); if (data.status) setPostStatus(data.status);
@ -69,7 +68,6 @@ export function usePostEditor(initialPostId?: string | null) {
contentHtml: draft, contentHtml: draft,
tags: meta.tagsText ? meta.tagsText.split(',').map(t => t.trim()).filter(Boolean) : undefined, tags: meta.tagsText ? meta.tagsText.split(',').map(t => t.trim()).filter(Boolean) : undefined,
featureImage: meta.featureImage || undefined, featureImage: meta.featureImage || undefined,
canonicalUrl: meta.canonicalUrl || undefined,
status: postStatus, status: postStatus,
prompt: promptText || undefined, prompt: promptText || undefined,
selectedImageKeys: genImageKeys.length > 0 ? genImageKeys : undefined, selectedImageKeys: genImageKeys.length > 0 ? genImageKeys : undefined,
@ -102,7 +100,6 @@ export function usePostEditor(initialPostId?: string | null) {
title: meta.title || 'Untitled', title: meta.title || 'Untitled',
html: draft, html: draft,
feature_image: meta.featureImage || null, feature_image: meta.featureImage || null,
canonical_url: meta.canonicalUrl || null,
tags, tags,
status, status,
authors: selectedAuthor ? [selectedAuthor] : undefined, authors: selectedAuthor ? [selectedAuthor] : undefined,

View File

@ -146,6 +146,19 @@ router.post('/post', async (req, res) => {
} }
} catch {} } catch {}
function isValidHttpUrl(u?: string | null): boolean {
if (!u || typeof u !== 'string') return false;
try {
const parsed = new URL(u);
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
} catch {
return false;
}
}
const safeCanonical = isValidHttpUrl(canonical_url) ? canonical_url : undefined;
const safeFeatureImage = isValidHttpUrl(rewrittenFeatureImage) ? rewrittenFeatureImage : undefined;
const payload = { const payload = {
posts: [ posts: [
{ {
@ -154,8 +167,8 @@ router.post('/post', async (req, res) => {
html: rewrittenHtml, html: rewrittenHtml,
status: status || 'draft', status: status || 'draft',
tags: tags && Array.isArray(tags) ? tags : [], tags: tags && Array.isArray(tags) ? tags : [],
...(rewrittenFeatureImage ? { feature_image: rewrittenFeatureImage } : {}), ...(safeFeatureImage ? { feature_image: safeFeatureImage } : {}),
...(canonical_url ? { canonical_url } : {}), ...(safeCanonical ? { canonical_url: safeCanonical } : {}),
...(authors && Array.isArray(authors) && authors.length > 0 ? { authors } : {}), ...(authors && Array.isArray(authors) && authors.length > 0 ? { authors } : {}),
}, },
], ],