diff --git a/CHANGELOG.md b/CHANGELOG.md index 021100b6..18ffe3b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Change Log +## [10.10.0] - 2025-xx-xx + +### 🎨 Enhancements + +- [#937](https://github.com/estruyf/vscode-front-matter/issues/937): Dashboard "Structure" view for documentation sites +- [#965](https://github.com/estruyf/vscode-front-matter/issues/965): Added SEO support for the keyword in the first paragraph +- [#973](https://github.com/estruyf/vscode-front-matter/issues/973): Support for number fields in the snippets + +### 🐞 Fixes + +- [#969](https://github.com/estruyf/vscode-front-matter/issues/969): Fix typo on welcome screen + ## [10.9.0] - 2025-07-01 - [Release notes](https://beta.frontmatter.codes/updates/v10.9.0) ### 🎨 Enhancements diff --git a/README.beta.md b/README.beta.md index d81c6404..e6d6a43d 100644 --- a/README.beta.md +++ b/README.beta.md @@ -117,7 +117,7 @@ In version v2 we released the re-designed sidebar panel with improved SEO suppor You can get the extension via: - The VS Code marketplace: [VS Code Marketplace - Front Matter](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter). -- The extension CLI: `ext install eliostruyf.vscode-front-matter` +- The extension CLI: `code --install-extension eliostruyf.vscode-front-matter` - Or by clicking on the following link: open extension in VS Code > **Info**: The docs can be found on [frontmatter.codes](https://frontmatter.codes). @@ -129,7 +129,7 @@ If you have the courage to test out the beta features, we made available a beta - Uninstall the main Front Matter version - Install the beta version - VS Code marketplace: [VS Code Marketplace - Front Matter BETA](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter-beta). - - The extension CLI: `ext install eliostruyf.vscode-front-matter-beta` + - The extension CLI: `code --install-extension eliostruyf.vscode-front-matter-beta` - Or by clicking on the following link: open extension in VS Code > **Info**: The BETA docs can be found on [beta.frontmatter.codes](https://beta.frontmatter.codes). diff --git a/README.md b/README.md index 45fc04ad..7407286d 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ In version v2 we released the re-designed sidebar panel with improved SEO suppor You can get the extension via: - The VS Code marketplace: [VS Code Marketplace - Front Matter](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter). -- The extension CLI: `ext install eliostruyf.vscode-front-matter` +- The extension CLI: `code --install-extension eliostruyf.vscode-front-matter` - Or by clicking on the following link: open extension in VS Code > **Info**: The docs can be found on [frontmatter.codes](https://frontmatter.codes). @@ -127,7 +127,7 @@ If you have the courage to test out the beta features, we made available a beta - Uninstall the main Front Matter version - Install the beta version - VS Code marketplace: [VS Code Marketplace - Front Matter BETA](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter-beta). - - The extension CLI: `ext install eliostruyf.vscode-front-matter-beta` + - The extension CLI: `code --install-extension eliostruyf.vscode-front-matter-beta` - Or by clicking on the following link: open extension in VS Code > **Info**: The BETA docs can be found on [beta.frontmatter.codes](https://beta.frontmatter.codes). diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index b16ad718..90499486 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -333,7 +333,7 @@ "dashboard.steps.stepsToGetStarted.tags.name": "Import all tags and categories (optional)", "dashboard.steps.stepsToGetStarted.tags.description": "Now that Front Matter knows all the content folders. Would you like to import all tags and categories from the available content?", "dashboard.steps.stepsToGetStarted.git.name": "Do you want to enable Git synchronization?", - "dashboard.steps.stepsToGetStarted.git.description": "Enable Git synchronization to eaily sync your changes with your repository.", + "dashboard.steps.stepsToGetStarted.git.description": "Enable Git synchronization to easily sync your changes with your repository.", "dashboard.steps.stepsToGetStarted.showDashboard.name": "Show the dashboard", "dashboard.steps.stepsToGetStarted.showDashboard.description": "Once all actions are completed, the dashboard can be loaded.", "dashboard.steps.stepsToGetStarted.template.name": "Use a configuration template", @@ -505,6 +505,7 @@ "panel.seoKeywords.density": "Keyword density", "panel.seoKeywordInfo.validInfo.label": "Heading(s)", "panel.seoKeywordInfo.validInfo.content": "Content", + "panel.seoKeywordInfo.validInfo.firstParagraph": "First paragraph", "panel.seoKeywordInfo.density.tooltip": "Recommended frequency: 0.75% - 1.5%", "panel.seoKeywords.title": "Keywords", diff --git a/src/dashboardWebView/components/Common/NumberField.tsx b/src/dashboardWebView/components/Common/NumberField.tsx new file mode 100644 index 00000000..f3714237 --- /dev/null +++ b/src/dashboardWebView/components/Common/NumberField.tsx @@ -0,0 +1,68 @@ +import { XCircleIcon } from '@heroicons/react/24/solid'; +import * as React from 'react'; + +export interface INumberFieldProps { + name: string; + value?: string; + placeholder?: string; + description?: string; + icon?: JSX.Element; + disabled?: boolean; + autoFocus?: boolean; + onChange?: (value: string) => void; + onReset?: () => void; +} + +export const NumberField: React.FunctionComponent = ({ + name, + value, + placeholder, + description, + icon, + autoFocus, + disabled, + onChange, + onReset +}: React.PropsWithChildren) => { + return ( + <> +
+ { + icon && ( +
+ {icon} +
+ ) + } + + onChange && onChange(e.target.value)} + disabled={!!disabled} + /> + + {(value && onReset) && ( + + )} +
+ + { + description && ( +

+ {description} +

+ ) + } + + ); +}; diff --git a/src/dashboardWebView/components/Contents/Overview.tsx b/src/dashboardWebView/components/Contents/Overview.tsx index 45dbedd1..7e7d4c2f 100644 --- a/src/dashboardWebView/components/Contents/Overview.tsx +++ b/src/dashboardWebView/components/Contents/Overview.tsx @@ -146,10 +146,10 @@ export const Overview: React.FunctionComponent = ({ /> {settings && settings?.contentFolders?.length > 0 ? (

{localize(LocalizationKey.dashboardContentsOverviewNoMarkdown)}

- + ) : (

{localize(LocalizationKey.dashboardContentsOverviewNoFolders)}

- + )} @@ -202,7 +202,7 @@ export const Overview: React.FunctionComponent = ({

{localize(LocalizationKey.dashboardContentsOverviewPinned)} - +

{pinnedPages.map((page, idx) => ( diff --git a/src/dashboardWebView/components/Contents/StructureView.tsx b/src/dashboardWebView/components/Contents/StructureView.tsx index 30b9c083..b73cc15c 100644 --- a/src/dashboardWebView/components/Contents/StructureView.tsx +++ b/src/dashboardWebView/components/Contents/StructureView.tsx @@ -20,7 +20,6 @@ interface FolderNode { export const StructureView: React.FunctionComponent = ({ pages }: React.PropsWithChildren) => { - const folderTree = useMemo(() => { const root: FolderNode = { name: '', @@ -125,15 +124,6 @@ export const StructureView: React.FunctionComponent = ({ } } - // Sort folders and pages - const sortNode = (node: FolderNode) => { - node.children.sort((a, b) => a.name.localeCompare(b.name)); - node.pages.sort((a, b) => a.title.localeCompare(b.title)); - node.children.forEach(sortNode); - }; - - sortNode(root); - return root; }, [pages]); @@ -150,13 +140,13 @@ export const StructureView: React.FunctionComponent = ({ if (isRoot) { // For root node, render children and pages directly return ( -
+
{/* Root level folders */} {node.children.map(child => renderFolderNode(child, depth + 1))} {/* Root level pages */} {node.pages.length > 0 && ( -
+

Root Files

diff --git a/src/dashboardWebView/components/Header/Pagination.tsx b/src/dashboardWebView/components/Header/Pagination.tsx index 5a68be1a..d044f11d 100644 --- a/src/dashboardWebView/components/Header/Pagination.tsx +++ b/src/dashboardWebView/components/Header/Pagination.tsx @@ -2,10 +2,11 @@ import * as React from 'react'; import { useCallback, useEffect, useMemo } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; import usePagination from '../../hooks/usePagination'; -import { MediaTotalSelector, PageAtom, SettingsAtom } from '../../state'; +import { MediaTotalSelector, PageAtom, SettingsAtom, ViewSelector } from '../../state'; import { PaginationButton } from './PaginationButton'; import * as l10n from '@vscode/l10n'; import { LocalizationKey } from '../../../localization'; +import { DashboardViewType } from '../../models'; export interface IPaginationProps { totalPages?: number; @@ -17,6 +18,7 @@ export const Pagination: React.FunctionComponent = ({ const [page, setPage] = useRecoilState(PageAtom); const totalMedia = useRecoilValue(MediaTotalSelector); const settings = useRecoilValue(SettingsAtom); + const view = useRecoilValue(ViewSelector); const { pageSetNr, totalPagesNr } = usePagination( settings?.dashboardState.contents.pagination, totalPages, @@ -33,17 +35,17 @@ export const Pagination: React.FunctionComponent = ({ if (i >= 0 && i <= totalPagesNr) { buttons.push( + key={i} + disabled={i === page} + onClick={() => { + setPage(i); + }} + className={`max-h-8 rounded ${page === i + ? `px-2 bg-[var(--vscode-list-activeSelectionBackground)] text-[var(--vscode-list-activeSelectionForeground)]` + : `text-[var(--vscode-editor-foreground)] hover:text-[var(--vscode-list-activeSelectionForeground)]`}`} + > + {i + 1} + ); } } @@ -58,6 +60,10 @@ export const Pagination: React.FunctionComponent = ({ setPage(0); }, []); + if (view === DashboardViewType.Structure) { + return null; + } + return (
); } + if (field.type === 'number') { + return ( + onValueChange(field, e)} + /> + ); + } + return ( = ( - _: React.PropsWithChildren -) => { +export const Snippets: React.FunctionComponent = () => { const settings = useRecoilValue(SettingsSelector); const viewData = useRecoilValue(ViewDataSelector); const mode = useRecoilValue(ModeAtom); diff --git a/src/helpers/ArticleHelper.ts b/src/helpers/ArticleHelper.ts index 9f96db7a..bb592b8d 100644 --- a/src/helpers/ArticleHelper.ts +++ b/src/helpers/ArticleHelper.ts @@ -807,7 +807,8 @@ export class ArticleHelper { const elms: Parent[] | Link[] = this.getAllElms(mdTree); const headings = elms.filter((node) => node.type === 'heading'); - const paragraphs = elms.filter((node) => node.type === 'paragraph').length; + const paragraphNodes = elms.filter((node) => node.type === 'paragraph'); + const paragraphs = paragraphNodes.length; const images = elms.filter((node) => node.type === 'image').length; const links: string[] = elms .filter((node) => node.type === 'link') @@ -836,6 +837,21 @@ export class ArticleHelper { } } + // Extract first paragraph text for SEO keyword checking + let firstParagraph = ''; + if (paragraphNodes.length > 0) { + const firstParagraphNode = paragraphNodes[0]; + const extractTextFromNode = (node: any): string => { + if (node.type === 'text') { + return node.value || ''; + } else if (node.children && Array.isArray(node.children)) { + return node.children.map(extractTextFromNode).join(''); + } + return ''; + }; + firstParagraph = extractTextFromNode(firstParagraphNode); + } + const wordCount = this.wordCount(0, mdTree); return { @@ -846,7 +862,8 @@ export class ArticleHelper { internalLinks, externalLinks: externalLinks.length, wordCount, - content: article.content + content: article.content, + firstParagraph }; } diff --git a/src/localization/localization.enum.ts b/src/localization/localization.enum.ts index 80ab834f..312eda56 100644 --- a/src/localization/localization.enum.ts +++ b/src/localization/localization.enum.ts @@ -1112,7 +1112,7 @@ export enum LocalizationKey { */ dashboardStepsStepsToGetStartedGitName = 'dashboard.steps.stepsToGetStarted.git.name', /** - * Enable Git synchronization to eaily sync your changes with your repository. + * Enable Git synchronization to easily sync your changes with your repository. */ dashboardStepsStepsToGetStartedGitDescription = 'dashboard.steps.stepsToGetStarted.git.description', /** @@ -1636,6 +1636,10 @@ export enum LocalizationKey { * Content */ panelSeoKeywordInfoValidInfoContent = 'panel.seoKeywordInfo.validInfo.content', + /** + * First paragraph + */ + panelSeoKeywordInfoValidInfoFirstParagraph = 'panel.seoKeywordInfo.validInfo.firstParagraph', /** * Recommended frequency: 0.75% - 1.5% */ diff --git a/src/panelWebView/components/ArticleDetails.tsx b/src/panelWebView/components/ArticleDetails.tsx index 747d100a..8fd7ddb2 100644 --- a/src/panelWebView/components/ArticleDetails.tsx +++ b/src/panelWebView/components/ArticleDetails.tsx @@ -11,6 +11,7 @@ export interface IArticleDetailsProps { internalLinks: number; externalLinks: number; images: number; + firstParagraph?: string; }; } diff --git a/src/panelWebView/components/Fields/DateTimeField.tsx b/src/panelWebView/components/Fields/DateTimeField.tsx index 9d6e06af..c9c0f152 100644 --- a/src/panelWebView/components/Fields/DateTimeField.tsx +++ b/src/panelWebView/components/Fields/DateTimeField.tsx @@ -40,11 +40,13 @@ export const DateTimeField: React.FunctionComponent = ({ const onDateChange = React.useCallback((date: Date) => { setDateValue(date); if (format) { + // Always use DateHelper.formatInTimezone when a format is provided onChange(DateHelper.formatInTimezone(date, format, timezone) || ""); } else { + // Only fallback to ISO string if no format is provided onChange(date.toISOString()); } - }, [format, onChange]); + }, [format, timezone, onChange]); const showRequiredState = useMemo(() => { return required && !dateValue; diff --git a/src/panelWebView/components/SeoKeywordInfo.tsx b/src/panelWebView/components/SeoKeywordInfo.tsx index bb3b78d1..094fc442 100644 --- a/src/panelWebView/components/SeoKeywordInfo.tsx +++ b/src/panelWebView/components/SeoKeywordInfo.tsx @@ -16,6 +16,7 @@ export interface ISeoKeywordInfoProps { content: string; wordCount?: number; headings?: string[]; + firstParagraph?: string; } const SeoKeywordInfo: React.FunctionComponent = ({ @@ -26,7 +27,8 @@ const SeoKeywordInfo: React.FunctionComponent = ({ slug, content, wordCount, - headings + headings, + firstParagraph }: React.PropsWithChildren) => { const density = () => { @@ -90,9 +92,10 @@ const SeoKeywordInfo: React.FunctionComponent = ({ (slug.toLowerCase().includes(keyword.toLowerCase()) || slug.toLowerCase().includes(keyword.replace(/ /g, '-').toLowerCase())), content: !!content && content.toLowerCase().includes(keyword.toLowerCase()), - heading: checkHeadings() + heading: checkHeadings(), + firstParagraph: !!firstParagraph && firstParagraph.toLowerCase().includes(keyword.toLowerCase()) }; - }, [title, description, slug, content, headings, wordCount]); + }, [title, description, slug, content, headings, wordCount, firstParagraph]); const tooltipContent = React.useMemo(() => { return ( @@ -102,7 +105,8 @@ const SeoKeywordInfo: React.FunctionComponent = ({ {localize(LocalizationKey.commonDescription)}
{localize(LocalizationKey.commonSlug)}
{localize(LocalizationKey.panelSeoKeywordInfoValidInfoContent)}
- {localize(LocalizationKey.panelSeoKeywordInfoValidInfoLabel)} + {localize(LocalizationKey.panelSeoKeywordInfoValidInfoLabel)}
+ {localize(LocalizationKey.panelSeoKeywordInfoValidInfoFirstParagraph)} ) }, [checks]); diff --git a/src/panelWebView/components/SeoKeywords.tsx b/src/panelWebView/components/SeoKeywords.tsx index 15fb3ad7..fc459ab6 100644 --- a/src/panelWebView/components/SeoKeywords.tsx +++ b/src/panelWebView/components/SeoKeywords.tsx @@ -14,6 +14,7 @@ export interface ISeoKeywordsProps { content: string; headings?: string[]; wordCount?: number; + firstParagraph?: string; } const SeoKeywords: React.FunctionComponent = ({ diff --git a/src/panelWebView/components/SeoStatus.tsx b/src/panelWebView/components/SeoStatus.tsx index af28b788..aeb3cf27 100644 --- a/src/panelWebView/components/SeoStatus.tsx +++ b/src/panelWebView/components/SeoStatus.tsx @@ -96,6 +96,7 @@ const SeoStatus: React.FunctionComponent = ({ headings={metadata?.articleDetails?.headingsText} wordCount={metadata?.articleDetails?.wordCount} content={metadata?.articleDetails?.content} + firstParagraph={metadata?.articleDetails?.firstParagraph} />