mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-10-23 03:27:27 +09:00
## Summary This update adds new MCP and CLI capabilities for project auditing, structured data, project-specific agent instructions, focused project search, safer bulk editing, image descriptions, font discovery, visual verification, and external registry inspection. ## New Capabilities ### Audit projects - Run a single `audit` command across accessibility, SEO, security, assets, styles, and performance. - Limit an audit to selected scopes, pages, or severity levels. - Receive stable findings with locations and suggested fixes. - Run rendered audits across desktop, mobile, and Builder breakpoint widths. - Detect horizontal overflow, broken images, oversized images, eager below-fold images, render-blocking resources, and legacy font formats. - Identify unused assets, tokens, CSS variables, local styles, and breakpoints. - Identify duplicate token declarations. - Detect accessibility issues involving names, labels, headings, landmarks, IDs, keyboard focus, ARIA, iframes, and autoplaying media. - Detect SEO issues involving titles, descriptions, language, social images, and structured data. - Detect unsafe new-tab links and write resources exposed during page rendering. ### Search an entire project - Search project text, labels, props, resources, assets, and styles through `search-project`. - Find relevant records without downloading and reparsing the full project snapshot. - Scope searches to the relevant project-data areas. ### Add validated JSON-LD - Add structured data with a dedicated JSON-LD component. - Place JSON-LD inside the page Head Slot. - Edit JSON in a purpose-built code editor. - Prevent structurally invalid JSON-LD from being saved. - Validate Schema.org types, properties, and values. - Find invalid, missing, unknown, deprecated, or incompatible structured data through the SEO audit. - Let agents discover the correct JSON-LD workflow without using page custom metadata or a generic HTML embed. ### Configure project-specific agent instructions - Add AI-agent instructions from the new **Agents** section in Project Settings. - Synchronize those instructions into a managed root `AGENTS.md`. - Update or remove the managed file automatically during project sync. - Protect existing user-owned `AGENTS.md` files from being overwritten. - Share the instructions with authenticated CLI and MCP agents. - Keep agent instructions out of publicly readable production bundles. ### Generate and manage image descriptions - Audit image assets for missing descriptions. - Let a vision-capable agent inspect each image in its rendered context. - Save generated descriptions to multiple image assets in one operation. - Mark images as intentionally decorative. - Reuse the saved asset description wherever the image is used. - Re-run the audit to verify that missing-description findings are resolved. ### Perform bounded bulk replacements - Replace matching literal text across selected pages with `replace-text`. - Replace matching fixed prop text with `replace-prop-text`. - Replace matching resource names or URLs with `replace-resource-text`. - Restrict replacements by page, instance, prop, resource, field, match mode, and maximum change count. - Leave dynamic expressions unchanged. ### Inspect element ancestry - Return an element’s direct parent in instance listings. - Request the complete ancestor chain when inspecting an element. - Understand tree placement without rebuilding the instance hierarchy from raw project data. ### Discover available fonts - List uploaded font families through `list-fonts`. - Discover available system font stacks. - Inspect font family, style, weight, format, and associated asset information before applying typography. ### Inspect external component registries - Inspect a local or remote shadcn-style registry item. - Read its description, files, dependencies, targets, and documentation. - Receive an explicit Webstudio compatibility report. - Inspect registry metadata without installing or executing external code. ### Capture generated project routes directly - Run `webstudio screenshot --path /route` against the configured project. - Generate a temporary production-style preview automatically. - Capture the requested route. - Stop the temporary preview after capture. - Avoid manually building and starting a preview for a single screenshot. ### Assert visual changes - Add expected text to screenshot comparisons. - Set maximum pixel mismatch limits. - Set minimum or maximum changed-region counts. - Assert whether overall brightness or a color channel moved in the expected direction. - Use these assertions as pass/fail evidence for visual changes. ### Require compatible CLI versions - Send the project-bundle contract version with CLI synchronization requests. - Stop incompatible CLI versions before they receive incomplete project data. - Tell users exactly how to update the CLI or run the latest version once. - Prevent older clients from silently dropping project settings. ## Upgrade Note This branch introduces a new synchronized project-bundle contract. Older CLI versions must update before synchronizing against the new API. ```sh npm install -g webstudio@latest ``` Or run the latest version once: ```sh npx webstudio@latest <command> ``` --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
304 lines
9.2 KiB
TypeScript
304 lines
9.2 KiB
TypeScript
import { useId } from "react";
|
|
import {
|
|
Box,
|
|
Checkbox,
|
|
Grid,
|
|
InputErrorsTooltip,
|
|
InputField,
|
|
Label,
|
|
Text,
|
|
TextArea,
|
|
theme,
|
|
} from "@webstudio-is/design-system";
|
|
import { isLiteralExpression } from "@webstudio-is/sdk";
|
|
import {
|
|
BindingControl,
|
|
BindingPopover,
|
|
} from "~/builder/shared/binding-popover";
|
|
import { computeExpression } from "@webstudio-is/project-build/runtime/data";
|
|
import type {
|
|
PageSettingsErrors,
|
|
PageSettingsValues,
|
|
} from "@webstudio-is/project-build/runtime/pages";
|
|
import { useStore } from "@nanostores/react";
|
|
import { $assets, $projectSettings } from "~/shared/sync/data-stores";
|
|
import { $pageRootScope } from "../page-utils";
|
|
import { SearchPreview } from "../search-preview";
|
|
import { usePageUrl, type OnChange } from "./shared";
|
|
|
|
const LanguageField = ({
|
|
errors,
|
|
value,
|
|
disabled,
|
|
showBindingControls = true,
|
|
onChange,
|
|
}: {
|
|
errors?: string[];
|
|
value: string;
|
|
disabled?: boolean;
|
|
showBindingControls?: boolean;
|
|
onChange: (value: string) => void;
|
|
}) => {
|
|
const id = useId();
|
|
const { variableValues, scope, aliases } = useStore($pageRootScope);
|
|
return (
|
|
<Grid gap={1}>
|
|
<Label htmlFor={id}>Language</Label>
|
|
<BindingControl>
|
|
{showBindingControls && (
|
|
<BindingPopover
|
|
scope={scope}
|
|
aliases={aliases}
|
|
variant={isLiteralExpression(value) ? "default" : "bound"}
|
|
value={value}
|
|
onChange={onChange}
|
|
onRemove={(evaluatedValue) =>
|
|
onChange(JSON.stringify(evaluatedValue ?? ""))
|
|
}
|
|
/>
|
|
)}
|
|
<InputErrorsTooltip errors={errors}>
|
|
<InputField
|
|
color={errors && "error"}
|
|
id={id}
|
|
placeholder="en-US"
|
|
disabled={disabled || isLiteralExpression(value) === false}
|
|
value={String(computeExpression(value, variableValues))}
|
|
onChange={(event) => onChange(JSON.stringify(event.target.value))}
|
|
/>
|
|
</InputErrorsTooltip>
|
|
</BindingControl>
|
|
</Grid>
|
|
);
|
|
};
|
|
|
|
export const SearchSection = ({
|
|
values,
|
|
errors,
|
|
canEditTitle = true,
|
|
canEditDescription = true,
|
|
canEditExcludePageFromSearch = true,
|
|
canEditLanguage = true,
|
|
showBindingControls = true,
|
|
onChange,
|
|
}: {
|
|
values: PageSettingsValues;
|
|
errors: PageSettingsErrors;
|
|
canEditTitle?: boolean;
|
|
canEditDescription?: boolean;
|
|
canEditExcludePageFromSearch?: boolean;
|
|
canEditLanguage?: boolean;
|
|
showBindingControls?: boolean;
|
|
onChange: OnChange;
|
|
}) => {
|
|
const titleId = useId();
|
|
const descriptionId = useId();
|
|
const excludePageFromSearchId = useId();
|
|
const { variableValues, scope, aliases } = useStore($pageRootScope);
|
|
const assets = useStore($assets);
|
|
const projectSettings = useStore($projectSettings);
|
|
const pageUrl = usePageUrl(values);
|
|
const faviconAsset = assets.get(projectSettings?.meta.faviconAssetId ?? "");
|
|
const faviconUrl = faviconAsset?.type === "image" ? faviconAsset.name : "";
|
|
const title = String(computeExpression(values.title, variableValues));
|
|
const description = String(
|
|
computeExpression(values.description, variableValues)
|
|
);
|
|
const excludePageFromSearch = Boolean(
|
|
computeExpression(values.excludePageFromSearch, variableValues)
|
|
);
|
|
return (
|
|
<Grid gap={2}>
|
|
<Grid gap={2}>
|
|
<Text color="subtle">
|
|
Optimize the way this page appears in search engine results pages.
|
|
</Text>
|
|
<Grid gap={1}>
|
|
<Label>Search result preview</Label>
|
|
<Box
|
|
css={{
|
|
padding: theme.spacing[5],
|
|
background: theme.colors.white,
|
|
borderRadius: theme.borderRadius[4],
|
|
border: `1px solid ${theme.colors.borderMain}`,
|
|
}}
|
|
>
|
|
<Box
|
|
css={{
|
|
transformOrigin: "top left",
|
|
transform: "scale(0.667)",
|
|
width: 600,
|
|
height: 80,
|
|
}}
|
|
>
|
|
<SearchPreview
|
|
siteName={projectSettings?.meta.siteName ?? ""}
|
|
faviconUrl={faviconUrl}
|
|
pageUrl={pageUrl}
|
|
titleLink={title}
|
|
snippet={description}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
<Grid gap={1}>
|
|
<Label htmlFor={titleId}>Title</Label>
|
|
<BindingControl>
|
|
{showBindingControls && (
|
|
<BindingPopover
|
|
scope={scope}
|
|
aliases={aliases}
|
|
variant={isLiteralExpression(values.title) ? "default" : "bound"}
|
|
value={values.title}
|
|
onChange={(value) => {
|
|
onChange({
|
|
field: "title",
|
|
value,
|
|
});
|
|
}}
|
|
onRemove={(evaluatedValue) => {
|
|
onChange({
|
|
field: "title",
|
|
value: JSON.stringify(evaluatedValue ?? ""),
|
|
});
|
|
}}
|
|
/>
|
|
)}
|
|
<InputErrorsTooltip errors={errors.title}>
|
|
<InputField
|
|
color={errors.title && "error"}
|
|
id={titleId}
|
|
name="title"
|
|
placeholder="My awesome project - About"
|
|
disabled={
|
|
canEditTitle === false ||
|
|
isLiteralExpression(values.title) === false
|
|
}
|
|
value={title}
|
|
onChange={(event) => {
|
|
onChange({
|
|
field: "title",
|
|
value: JSON.stringify(event.target.value),
|
|
});
|
|
}}
|
|
/>
|
|
</InputErrorsTooltip>
|
|
</BindingControl>
|
|
</Grid>
|
|
|
|
<Grid gap={1}>
|
|
<Label htmlFor={descriptionId}>Description</Label>
|
|
<BindingControl>
|
|
{showBindingControls && (
|
|
<BindingPopover
|
|
scope={scope}
|
|
aliases={aliases}
|
|
variant={
|
|
isLiteralExpression(values.description) ? "default" : "bound"
|
|
}
|
|
value={values.description}
|
|
onChange={(value) => {
|
|
onChange({
|
|
field: "description",
|
|
value,
|
|
});
|
|
}}
|
|
onRemove={(evaluatedValue) => {
|
|
onChange({
|
|
field: "description",
|
|
value: JSON.stringify(evaluatedValue ?? ""),
|
|
});
|
|
}}
|
|
/>
|
|
)}
|
|
<InputErrorsTooltip errors={errors.description}>
|
|
<TextArea
|
|
color={errors.description ? "error" : undefined}
|
|
id={descriptionId}
|
|
name="description"
|
|
disabled={
|
|
canEditDescription === false ||
|
|
isLiteralExpression(values.description) === false
|
|
}
|
|
value={description}
|
|
onChange={(value) => {
|
|
onChange({
|
|
field: "description",
|
|
value: JSON.stringify(value),
|
|
});
|
|
}}
|
|
autoGrow
|
|
maxRows={10}
|
|
/>
|
|
</InputErrorsTooltip>
|
|
</BindingControl>
|
|
<BindingControl>
|
|
<Grid
|
|
flow={"column"}
|
|
gap={1}
|
|
justify={"start"}
|
|
align={"center"}
|
|
css={{ py: theme.spacing[2] }}
|
|
>
|
|
{showBindingControls && (
|
|
<BindingPopover
|
|
scope={scope}
|
|
aliases={aliases}
|
|
variant={
|
|
isLiteralExpression(values.excludePageFromSearch)
|
|
? "default"
|
|
: "bound"
|
|
}
|
|
value={values.excludePageFromSearch}
|
|
onChange={(value) => {
|
|
onChange({
|
|
field: "excludePageFromSearch",
|
|
value,
|
|
});
|
|
}}
|
|
onRemove={(evaluatedValue) => {
|
|
onChange({
|
|
field: "excludePageFromSearch",
|
|
value: JSON.stringify(evaluatedValue ?? ""),
|
|
});
|
|
}}
|
|
/>
|
|
)}
|
|
<Checkbox
|
|
id={excludePageFromSearchId}
|
|
disabled={
|
|
canEditExcludePageFromSearch === false ||
|
|
isLiteralExpression(values.excludePageFromSearch) === false
|
|
}
|
|
checked={excludePageFromSearch}
|
|
onCheckedChange={() => {
|
|
const newValue = !excludePageFromSearch;
|
|
onChange({
|
|
field: "excludePageFromSearch",
|
|
value: newValue.toString(),
|
|
});
|
|
}}
|
|
/>
|
|
|
|
<InputErrorsTooltip errors={errors.excludePageFromSearch}>
|
|
<Label htmlFor={excludePageFromSearchId}>
|
|
Exclude this page from search results
|
|
</Label>
|
|
</InputErrorsTooltip>
|
|
</Grid>
|
|
</BindingControl>
|
|
</Grid>
|
|
|
|
<LanguageField
|
|
errors={errors.language}
|
|
value={values.language}
|
|
disabled={canEditLanguage === false}
|
|
showBindingControls={showBindingControls}
|
|
onChange={(value) => onChange({ field: "language", value })}
|
|
/>
|
|
</Grid>
|
|
);
|
|
};
|