feat: sitemap validattion

This commit is contained in:
2026-02-21 14:26:06 +01:00
parent d651049659
commit bca3da1587
28 changed files with 1124 additions and 14 deletions

View File

@@ -17,6 +17,7 @@ import { ImportAnalysisView } from '../ImportAnalysisView';
import { MetadataDiffPanel } from '../MetadataDiffPanel';
import { GitDiffView } from '../GitDiffView/GitDiffView';
import { DocumentationView } from '../DocumentationView/DocumentationView';
import { SiteValidationView } from '../SiteValidationView';
import { AutoSaveManager, getContrastColor } from '../../utils';
import { InsertModal } from '../InsertModal';
import { AISuggestionsModal, AISuggestions } from '../AISuggestionsModal/AISuggestionsModal';
@@ -1735,6 +1736,7 @@ export const Editor: React.FC = () => {
const showMetadataDiff = activeTab?.type === 'metadata-diff';
const showGitDiff = activeTab?.type === 'git-diff';
const showDocumentation = activeTab?.type === 'documentation';
const showSiteValidation = activeTab?.type === 'site-validation';
useEffect(() => {
const activePostId = activeTab?.type === 'post' ? activeTab.id : null;
@@ -1873,6 +1875,16 @@ export const Editor: React.FC = () => {
);
}
if (showSiteValidation) {
return (
<div className="editor">
<SiteValidationView />
{renderErrorModal()}
{renderConfirmDeleteModal()}
</div>
);
}
// Show post editor if a post tab is active
if (showPost && activeTabId) {
return (

View File

@@ -0,0 +1,70 @@
.site-validation-view {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px;
height: 100%;
overflow: auto;
background: var(--vscode-editor-background);
color: var(--vscode-editor-foreground);
}
.site-validation-summary h2 {
margin: 0 0 8px 0;
font-size: 1.1rem;
}
.site-validation-summary p {
margin: 0;
color: var(--vscode-descriptionForeground);
}
.site-validation-section h3 {
margin: 0 0 8px 0;
font-size: 1rem;
}
.site-validation-list {
margin: 0;
padding-left: 20px;
font-family: var(--vscode-editor-font-family);
font-size: 12px;
}
.site-validation-list-missing {
color: var(--vscode-testing-iconPassed);
}
.site-validation-list-extra {
color: var(--vscode-notificationsErrorIcon-foreground);
}
.site-validation-empty,
.site-validation-status {
margin: 0;
color: var(--vscode-descriptionForeground);
}
.site-validation-actions {
margin-top: auto;
display: flex;
justify-content: flex-end;
}
.site-validation-apply {
background-color: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
}
.site-validation-apply:hover:not(:disabled) {
background-color: var(--vscode-button-hoverBackground);
}
.site-validation-apply:disabled {
opacity: 0.6;
cursor: not-allowed;
}

View File

@@ -0,0 +1,137 @@
import React, { useEffect, useMemo, useState } from 'react';
import { showToast } from '../Toast';
import { useI18n } from '../../i18n';
import './SiteValidationView.css';
type SiteValidationReport = {
sitemapPath: string;
sitemapChanged: boolean;
missingUrlPaths: string[];
extraUrlPaths: string[];
expectedUrlCount: number;
existingHtmlUrlCount: number;
};
type SiteValidationApplyResult = {
renderedUrlCount: number;
deletedUrlCount: number;
removedEmptyDirCount: number;
};
export const SiteValidationView: React.FC = () => {
const { t: tr } = useI18n();
const [isLoading, setIsLoading] = useState(true);
const [isApplying, setIsApplying] = useState(false);
const [report, setReport] = useState<SiteValidationReport | null>(null);
const loadReport = async () => {
setIsLoading(true);
try {
const result = await window.electronAPI.blog.validateSite();
setReport(result as SiteValidationReport);
} catch (error) {
console.error('Site validation failed:', error);
showToast.error(tr('siteValidation.error.validate'));
setReport(null);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
void loadReport();
}, []);
const canApply = useMemo(() => {
if (!report) return false;
return report.missingUrlPaths.length > 0 || report.extraUrlPaths.length > 0;
}, [report]);
const handleApply = async () => {
if (!report || !canApply) {
return;
}
setIsApplying(true);
try {
const result = await window.electronAPI.blog.applyValidation(report) as SiteValidationApplyResult;
showToast.success(tr('siteValidation.toast.applySuccess', {
rendered: result.renderedUrlCount,
deleted: result.deletedUrlCount,
}));
await loadReport();
} catch (error) {
console.error('Applying site validation failed:', error);
showToast.error(tr('siteValidation.error.apply'));
} finally {
setIsApplying(false);
}
};
if (isLoading) {
return (
<div className="site-validation-view">
<p className="site-validation-status">{tr('siteValidation.loading')}</p>
</div>
);
}
if (!report) {
return (
<div className="site-validation-view">
<p className="site-validation-status">{tr('siteValidation.error.validate')}</p>
</div>
);
}
return (
<div className="site-validation-view">
<div className="site-validation-summary">
<h2>{tr('siteValidation.title')}</h2>
<p>{tr('siteValidation.summary', {
expected: report.expectedUrlCount,
existing: report.existingHtmlUrlCount,
missing: report.missingUrlPaths.length,
extra: report.extraUrlPaths.length,
})}</p>
</div>
<section className="site-validation-section">
<h3>{tr('siteValidation.missingTitle')}</h3>
{report.missingUrlPaths.length === 0 ? (
<p className="site-validation-empty">{tr('siteValidation.noneMissing')}</p>
) : (
<ul className="site-validation-list site-validation-list-missing">
{report.missingUrlPaths.map((urlPath) => (
<li key={`missing:${urlPath}`}>{urlPath}</li>
))}
</ul>
)}
</section>
<section className="site-validation-section">
<h3>{tr('siteValidation.extraTitle')}</h3>
{report.extraUrlPaths.length === 0 ? (
<p className="site-validation-empty">{tr('siteValidation.noneExtra')}</p>
) : (
<ul className="site-validation-list site-validation-list-extra">
{report.extraUrlPaths.map((urlPath) => (
<li key={`extra:${urlPath}`}>{urlPath}</li>
))}
</ul>
)}
</section>
<div className="site-validation-actions">
<button
type="button"
className="site-validation-apply"
onClick={handleApply}
disabled={!canApply || isApplying}
>
{isApplying ? tr('siteValidation.applying') : tr('siteValidation.apply')}
</button>
</div>
</div>
);
};

View File

@@ -0,0 +1 @@
export { SiteValidationView } from './SiteValidationView';

View File

@@ -84,6 +84,10 @@ const getTabTitle = (
return tr('docs.title');
}
if (tab.type === 'site-validation') {
return tr('siteValidation.tabTitle');
}
return tr('tabBar.unknown');
};
@@ -150,6 +154,12 @@ const getTabIcon = (tab: Tab): React.ReactNode => {
<path d="M5 4h6v1H5V4zm0 2h6v1H5V6zm0 2h6v1H5V8zm0 2h4v1H5v-1z"/>
</svg>
);
case 'site-validation':
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 1.5a6.5 6.5 0 1 0 6.5 6.5A6.5 6.5 0 0 0 8 1.5zm0 1a5.5 5.5 0 0 1 4.39 8.82l-.88-.88a.5.5 0 0 0-.7.7l.8.8A5.5 5.5 0 1 1 8 2.5zm2.35 3.15L7 9 5.65 7.65a.5.5 0 1 0-.7.7l1.7 1.7a.5.5 0 0 0 .7 0l3.7-3.7a.5.5 0 1 0-.7-.7z"/>
</svg>
);
default:
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">

View File

@@ -25,3 +25,4 @@ export { ImportAnalysisView } from './ImportAnalysisView';
export { InsertModal } from './InsertModal';
export { WindowTitleBar } from './WindowTitleBar';
export { DocumentationView } from './DocumentationView/DocumentationView';
export { SiteValidationView } from './SiteValidationView';