Merge branch 'beta' into copilot/fix-937

This commit is contained in:
Elio Struyf
2025-09-10 13:56:43 +02:00
17 changed files with 160 additions and 43 deletions
+12
View File
@@ -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
+2 -2
View File
@@ -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: <a href="" title="open extension in VS Code" data-vscode="vscode:extension/eliostruyf.vscode-front-matter">open extension in VS Code</a>
> **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: <a href="" title="open extension in VS Code" data-vscode="vscode:extension/eliostruyf.vscode-front-matter-beta">open extension in VS Code</a>
> **Info**: The BETA docs can be found on [beta.frontmatter.codes](https://beta.frontmatter.codes).
+2 -2
View File
@@ -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: <a href="" title="open extension in VS Code" data-vscode="vscode:extension/eliostruyf.vscode-front-matter">open extension in VS Code</a>
> **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: <a href="" title="open extension in VS Code" data-vscode="vscode:extension/eliostruyf.vscode-front-matter-beta">open extension in VS Code</a>
> **Info**: The BETA docs can be found on [beta.frontmatter.codes](https://beta.frontmatter.codes).
+2 -1
View File
@@ -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",
@@ -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<INumberFieldProps> = ({
name,
value,
placeholder,
description,
icon,
autoFocus,
disabled,
onChange,
onReset
}: React.PropsWithChildren<INumberFieldProps>) => {
return (
<>
<div className="relative flex justify-center">
{
icon && (
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
{icon}
</div>
)
}
<input
type="number"
name={name}
className={`block w-full py-2 ${icon ? "pl-10" : "pl-2"} pr-2 sm:text-sm appearance-none disabled:opacity-50 rounded bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] placeholder-[var(--vscode-input-placeholderForeground)] border-[var(--frontmatter-border)] focus:border-[var(--vscode-focusBorder)] focus:outline-0`}
style={{
boxShadow: "none"
}}
placeholder={placeholder || ""}
value={value}
autoFocus={!!autoFocus}
onChange={(e) => onChange && onChange(e.target.value)}
disabled={!!disabled}
/>
{(value && onReset) && (
<button onClick={onReset} className="absolute inset-y-0 right-0 pr-3 flex items-center text-[var(--vscode-input-foreground)] hover:text-[var(--vscode-textLink-activeForeground)]">
<XCircleIcon className={`h-5 w-5`} aria-hidden="true" />
</button>
)}
</div>
{
description && (
<p className="text-xs text-[var(--vscode--settings-headerForeground)] opacity-75 mt-2 mx-2">
{description}
</p>
)
}
</>
);
};
@@ -146,10 +146,10 @@ export const Overview: React.FunctionComponent<IOverviewProps> = ({
/>
{settings && settings?.contentFolders?.length > 0 ? (
<p className={`text-xl font-medium`}>{localize(LocalizationKey.dashboardContentsOverviewNoMarkdown)}</p>
) : (
<p className={`text-lg font-medium`}>{localize(LocalizationKey.dashboardContentsOverviewNoFolders)}</p>
)}
</div>
</div>
@@ -202,7 +202,7 @@ export const Overview: React.FunctionComponent<IOverviewProps> = ({
<h1 className='text-xl flex space-x-2 items-center mb-4'>
<PinIcon className={`-rotate-45`} />
<span>{localize(LocalizationKey.dashboardContentsOverviewPinned)}</span>
</h1>
<List>
{pinnedPages.map((page, idx) => (
@@ -20,7 +20,6 @@ interface FolderNode {
export const StructureView: React.FunctionComponent<IStructureViewProps> = ({
pages
}: React.PropsWithChildren<IStructureViewProps>) => {
const folderTree = useMemo(() => {
const root: FolderNode = {
name: '',
@@ -125,15 +124,6 @@ export const StructureView: React.FunctionComponent<IStructureViewProps> = ({
}
}
// 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<IStructureViewProps> = ({
if (isRoot) {
// For root node, render children and pages directly
return (
<div>
<div className='space-y-4'>
{/* Root level folders */}
{node.children.map(child => renderFolderNode(child, depth + 1))}
{/* Root level pages */}
{node.pages.length > 0 && (
<div className="mb-6">
<div>
<h3 className="text-lg font-medium mb-3 text-[var(--vscode-editor-foreground)]">
Root Files
</h3>
@@ -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<IPaginationProps> = ({
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<IPaginationProps> = ({
if (i >= 0 && i <= totalPagesNr) {
buttons.push(
<button
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}
</button>
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}
</button>
);
}
}
@@ -58,6 +60,10 @@ export const Pagination: React.FunctionComponent<IPaginationProps> = ({
setPage(0);
}, []);
if (view === DashboardViewType.Structure) {
return null;
}
return (
<div className="flex justify-between items-center sm:justify-end space-x-2 text-sm">
<PaginationButton
@@ -3,6 +3,7 @@ import { ChevronDownIcon } from '@heroicons/react/24/outline';
import { Choice, SnippetField, SnippetInfoField } from '../../../models';
import { useEffect } from 'react';
import { TextField } from '../Common/TextField';
import { NumberField } from '../Common/NumberField';
export interface ISnippetInputFieldProps {
field: SnippetField;
@@ -78,6 +79,17 @@ export const SnippetInputField: React.FunctionComponent<ISnippetInputFieldProps>
);
}
if (field.type === 'number') {
return (
<NumberField
name={field.name}
value={field.value as string || ''}
description={field.description}
onChange={(e) => onValueChange(field, e)}
/>
);
}
return (
<TextField
name={field.name}
@@ -21,9 +21,7 @@ import { DEFAULT_DASHBOARD_FEATURE_FLAGS } from '../../../constants/DefaultFeatu
export interface ISnippetsProps { }
export const Snippets: React.FunctionComponent<ISnippetsProps> = (
_: React.PropsWithChildren<ISnippetsProps>
) => {
export const Snippets: React.FunctionComponent<ISnippetsProps> = () => {
const settings = useRecoilValue(SettingsSelector);
const viewData = useRecoilValue(ViewDataSelector);
const mode = useRecoilValue(ModeAtom);
+19 -2
View File
@@ -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
};
}
+5 -1
View File
@@ -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%
*/
@@ -11,6 +11,7 @@ export interface IArticleDetailsProps {
internalLinks: number;
externalLinks: number;
images: number;
firstParagraph?: string;
};
}
@@ -40,11 +40,13 @@ export const DateTimeField: React.FunctionComponent<IDateTimeFieldProps> = ({
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;
@@ -16,6 +16,7 @@ export interface ISeoKeywordInfoProps {
content: string;
wordCount?: number;
headings?: string[];
firstParagraph?: string;
}
const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({
@@ -26,7 +27,8 @@ const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({
slug,
content,
wordCount,
headings
headings,
firstParagraph
}: React.PropsWithChildren<ISeoKeywordInfoProps>) => {
const density = () => {
@@ -90,9 +92,10 @@ const SeoKeywordInfo: React.FunctionComponent<ISeoKeywordInfoProps> = ({
(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<ISeoKeywordInfoProps> = ({
<span className='inline-flex items-center gap-1'><ValidInfo isValid={checks.description} /> {localize(LocalizationKey.commonDescription)}</span><br />
<span className='inline-flex items-center gap-1'><ValidInfo isValid={checks.slug} /> {localize(LocalizationKey.commonSlug)}</span><br />
<span className='inline-flex items-center gap-1'><ValidInfo isValid={checks.content} /> {localize(LocalizationKey.panelSeoKeywordInfoValidInfoContent)}</span><br />
<span className='inline-flex items-center gap-1'><ValidInfo isValid={!!checks.heading} /> {localize(LocalizationKey.panelSeoKeywordInfoValidInfoLabel)}</span>
<span className='inline-flex items-center gap-1'><ValidInfo isValid={!!checks.heading} /> {localize(LocalizationKey.panelSeoKeywordInfoValidInfoLabel)}</span><br />
<span className='inline-flex items-center gap-1'><ValidInfo isValid={checks.firstParagraph} /> {localize(LocalizationKey.panelSeoKeywordInfoValidInfoFirstParagraph)}</span>
</>
)
}, [checks]);
@@ -14,6 +14,7 @@ export interface ISeoKeywordsProps {
content: string;
headings?: string[];
wordCount?: number;
firstParagraph?: string;
}
const SeoKeywords: React.FunctionComponent<ISeoKeywordsProps> = ({
@@ -96,6 +96,7 @@ const SeoStatus: React.FunctionComponent<ISeoStatusProps> = ({
headings={metadata?.articleDetails?.headingsText}
wordCount={metadata?.articleDetails?.wordCount}
content={metadata?.articleDetails?.content}
firstParagraph={metadata?.articleDetails?.firstParagraph}
/>
<FieldBoundary fieldName={`Keywords`}>