website: user-centered copy rewrite, fix image layout shift, replace HTML entities

- Rewrite landing page copy to focus on user needs: agent controlling
  your browser, low context usage, unrestricted capabilities, collaboration
- Add Collaboration section (captchas, consent walls, getting agent unstuck)
- Replace HTML entities (— ' –) with JS string expressions
- Add width/height to screenshot image to prevent layout shift
- Add no-layout-shift requirement to AGENTS.md
This commit is contained in:
Tommy D. Rossi
2026-02-20 23:25:48 +01:00
parent 5c1ef5d332
commit 8871a3f36f
2 changed files with 110 additions and 72 deletions
+10
View File
@@ -49,6 +49,16 @@ If you create a new CSS file that needs `@variant dark`, add it as a CSS `@impor
For files with many dark mode selectors (like prism syntax colors), define CSS variables in `:root { @variant dark { ... } }` and reference them in selectors. This avoids repeating `@variant dark` in every rule. For files with many dark mode selectors (like prism syntax colors), define CSS variables in `:root { @variant dark { ... } }` and reference them in selectors. This avoids repeating `@variant dark` in every rule.
## Images must not cause layout shift
Every `<img>` must have explicit `width` and `height` attributes matching the intrinsic pixel dimensions of the source file. Add `style={{ height: "auto" }}` to keep it responsive. This lets the browser reserve the correct aspect ratio space before the image loads, preventing layout shift.
```tsx
<img src="/photo.png" width={1280} height={800} style={{ maxWidth: "100%", height: "auto" }} />
```
Use `sips -g pixelWidth -g pixelHeight <file>` to get dimensions on macOS.
## No localStorage dark mode toggle ## No localStorage dark mode toggle
There is no JS-based `.dark` class toggle. Do not add inline scripts to toggle `.dark` on `<html>`. The site follows the OS preference only via `prefers-color-scheme`. There is no JS-based `.dark` class toggle. Do not add inline scripts to toggle `.dark` on `<html>`. The site follows the OS preference only via `prefers-color-scheme`.
+100 -72
View File
@@ -45,6 +45,7 @@ export const meta: MetaFunction = () => {
const tocItems = [ const tocItems = [
{ label: "Getting started", href: "#getting-started" }, { label: "Getting started", href: "#getting-started" },
{ label: "How it works", href: "#how-it-works" }, { label: "How it works", href: "#how-it-works" },
{ label: "Collaboration", href: "#collaboration" },
{ label: "Snapshots", href: "#snapshots" }, { label: "Snapshots", href: "#snapshots" },
{ label: "Visual labels", href: "#visual-labels" }, { label: "Visual labels", href: "#visual-labels" },
{ label: "Sessions", href: "#sessions" }, { label: "Sessions", href: "#sessions" },
@@ -61,9 +62,10 @@ export default function IndexPage() {
<EditorialPage toc={tocItems} logo="playwriter"> <EditorialPage toc={tocItems} logo="playwriter">
<P> <P>
Playwriter lets you <strong>control your Chrome browser</strong> with the full You want your agent to control the browser. <strong>Your actual
Playwright API. A Chrome extension, a local relay, and a CLI. No new Chrome</strong> {" \u2014 "} with logins, extensions, and cookies already
browser windows, no Chrome flags, no context bloat.{" "} there. Not a headless instance that gets blocked by every captcha
and bot detector.{" "}
<A href="https://github.com/remorses/playwriter">Star on GitHub</A>. <A href="https://github.com/remorses/playwriter">Star on GitHub</A>.
</P> </P>
@@ -71,26 +73,36 @@ export default function IndexPage() {
<img <img
src="/screenshot@2x.png" src="/screenshot@2x.png"
alt="Playwriter controlling Chrome with accessibility labels overlay" alt="Playwriter controlling Chrome with accessibility labels overlay"
style={{ display: "block", maxWidth: "100%" }} width={1280}
height={800}
style={{ display: "block", maxWidth: "100%", height: "auto" }}
/> />
</div> </div>
<Caption> <Caption>
Your existing Chrome session. Extensions, logins, cookies &mdash; all there. Your existing Chrome session. Extensions, logins, cookies {" \u2014 "} all there.
</Caption> </Caption>
<P> <P>
Every browser automation MCP I tried either spawns a new Chrome Other browser MCPs either <strong>spawn a fresh Chrome</strong> or give agents
instance or forces you into a limited set of predefined tools. Playwriter a fixed set of tools. New Chrome means no logins, no extensions,
does neither. It connects to the browser you already have open, instant bot detection, and double the memory. Fixed tools mean the
exposes the <strong>full Playwright API</strong> through a single{" "} agent can{"'"}t profile performance, can{"'"}t set breakpoints,
<Code>execute</Code> tool, and gets out of the way. can{"'"}t intercept network requests {" \u2014 "} it can only do what someone
One tool. Any Playwright code. No wrappers. decided to expose.
</P>
<P>
Playwriter gives agents the <strong>full Playwright API</strong> through
a single <Code>execute</Code> tool. One tool, any Playwright code,
no wrappers. Low context usage because there{"'"}s no schema bloat
from dozens of tool definitions. And it runs in your existing browser,
so <strong>nothing extra gets spawned</strong>.
</P> </P>
<Section id="getting-started" title="Getting started"> <Section id="getting-started" title="Getting started">
<P> <P>
<strong>Three steps.</strong> Extension, icon click, then you&apos;re automating. <strong>Three steps</strong> and your agent is browsing.
</P> </P>
<OL> <OL>
@@ -130,12 +142,12 @@ export default function IndexPage() {
<Section id="how-it-works" title="How it works"> <Section id="how-it-works" title="How it works">
<P> <P>
The extension uses <Code>chrome.debugger</Code> to Click the extension icon on a tab {" \u2014 "} it attaches via{" "}
attach to tabs where you clicked the icon. It opens a WebSocket <Code>chrome.debugger</Code> and opens a WebSocket to a
connection to a local relay server. The CLI (or MCP, or your own local relay. Your agent (CLI, MCP, or a Playwright script) connects
Playwright script) connects to the same relay. <strong>CDP commands flow to the same relay. <strong>CDP commands flow through</strong>; the extension
through</strong>; the extension forwards them to Chrome and sends responses forwards them to Chrome and sends responses back. No Chrome restart,
back. no flags, no special setup.
</P> </P>
<CodeBlock lang="bash">{dedent` <CodeBlock lang="bash">{dedent`
@@ -156,10 +168,27 @@ export default function IndexPage() {
`}</CodeBlock> `}</CodeBlock>
<P> <P>
<strong>No Chrome restart required.</strong> No <Code>--remote-debugging-port</Code>{" "} The relay <strong>multiplexes sessions</strong>, so multiple agents
flags. The extension handles the CDP attachment transparently, and or CLI instances can work with the same browser at the same time.
the relay <strong>multiplexes sessions</strong> so multiple agents or CLI instances </P>
can work with the same browser simultaneously.
</Section>
<Section id="collaboration" title="Collaboration">
<P>
Because the agent works in <strong>your browser</strong>, you can
collaborate. You see everything it does in real time. When it hits
a captcha, <strong>you solve it</strong>. When a consent wall
appears, you click through it. When the agent gets stuck, you
disable the extension on that tab, fix things manually, re-enable
it, and the agent picks up where it left off.
</P>
<P>
You{"'"}re not watching a remote screen or reading logs after the
fact. You{"'"}re <strong>sharing a browser</strong> {" \u2014 "} the
agent does the repetitive work, you step in when it needs a human.
</P> </P>
</Section> </Section>
@@ -167,10 +196,11 @@ export default function IndexPage() {
<Section id="snapshots" title="Accessibility snapshots"> <Section id="snapshots" title="Accessibility snapshots">
<P> <P>
The core feedback loop is <strong>observe &rarr; act &rarr; observe</strong>. Your agent needs to <strong>see the page</strong> before it can act.
Accessibility snapshots are the primary way to read page state. They return Accessibility snapshots return every interactive element as text,
the full interactive element tree as text, with Playwright locators attached with Playwright locators attached. <strong>5{"\u2013"}20KB instead of
to every element. 100KB+</strong> for a screenshot {" \u2014 "} cheaper, faster, and the
agent can parse them without vision.
</P> </P>
<CodeBlock lang="bash">{dedent` <CodeBlock lang="bash">{dedent`
@@ -200,24 +230,22 @@ export default function IndexPage() {
`}</CodeBlock> `}</CodeBlock>
<P> <P>
Snapshots are <strong>text</strong>. They cost a fraction of what screenshots cost in Use snapshots as the <strong>primary way to read pages</strong>. Only
tokens. Use them as your <strong>primary debugging tool</strong>. Only reach for reach for screenshots when spatial layout matters {" \u2014 "} grids,
screenshots when spatial layout matters &mdash; grids, dashboards, maps. dashboards, maps.
</P> </P>
<Caption>
Accessibility tree as text. 5&ndash;20KB vs 100KB+ for screenshots.
</Caption>
</Section> </Section>
<Section id="visual-labels" title="Visual labels"> <Section id="visual-labels" title="Visual labels">
<P> <P>
For pages where spatial layout matters,{" "} When the agent needs to understand <strong>where things are on
screen</strong>,{" "}
<Code>screenshotWithAccessibilityLabels</Code> overlays{" "} <Code>screenshotWithAccessibilityLabels</Code> overlays{" "}
<strong>Vimium-style labels</strong> on every interactive element. Take a screenshot, <strong>Vimium-style labels</strong> on every interactive element.
read the labels, click by reference. The agent sees the screenshot, reads the labels, and clicks by
reference.
</P> </P>
<CodeBlock lang="bash">{dedent` <CodeBlock lang="bash">{dedent`
@@ -244,10 +272,10 @@ export default function IndexPage() {
<Section id="sessions" title="Sessions"> <Section id="sessions" title="Sessions">
<P> <P>
Each session runs in an <strong>isolated sandbox</strong> with its own{" "} Run <strong>multiple agents at once</strong> without them stepping on
<Code>state</Code> object. Variables, pages, listeners each other. Each session is an isolated sandbox with its own{" "}
persist between calls within a session. Different sessions get <Code>state</Code> object. Variables, pages, and listeners
different state. Browser tabs are shared. persist between calls. Browser tabs are shared, but state is not.
</P> </P>
<CodeBlock lang="bash">{dedent` <CodeBlock lang="bash">{dedent`
@@ -280,9 +308,10 @@ export default function IndexPage() {
<Section id="debugger-and-editor" title="Debugger & editor"> <Section id="debugger-and-editor" title="Debugger & editor">
<P> <P>
<strong>Full Chrome DevTools Protocol access.</strong> Set breakpoints, step through Things no other browser MCP can do. <strong>Set breakpoints</strong>,
code, inspect variables at runtime. Live-edit page scripts and CSS step through code, inspect variables at runtime. <strong>Live-edit
without reloading. page scripts and CSS</strong> without reloading. Full Chrome DevTools
Protocol access, not a watered-down subset.
</P> </P>
<CodeBlock lang="bash">{dedent` <CodeBlock lang="bash">{dedent`
@@ -304,7 +333,7 @@ export default function IndexPage() {
</P> </P>
<Caption> <Caption>
Breakpoints, stepping, variable inspection &mdash; from the CLI. Breakpoints, stepping, variable inspection {" \u2014 "} from the CLI.
</Caption> </Caption>
</Section> </Section>
@@ -312,9 +341,10 @@ export default function IndexPage() {
<Section id="network-interception" title="Network interception"> <Section id="network-interception" title="Network interception">
<P> <P>
<strong>Intercept requests and responses</strong> to reverse-engineer APIs, scrape Let the agent <strong>watch network traffic</strong> to
data, or debug network issues. Store captured data in{" "} reverse-engineer APIs, scrape data behind JavaScript rendering,
<Code>state</Code> and analyze across calls. or debug failing requests. Captured data lives in{" "}
<Code>state</Code> and persists across calls.
</P> </P>
<CodeBlock lang="bash">{dedent` <CodeBlock lang="bash">{dedent`
@@ -330,10 +360,10 @@ export default function IndexPage() {
`}</CodeBlock> `}</CodeBlock>
<P> <P>
This is <strong>faster than scrolling through DOM</strong>. Capture the real API <strong>Faster than scraping the DOM.</strong> The agent captures the
calls, inspect their schemas, and replay them with different real API calls, inspects their schemas, and replays them with
parameters. Works for pagination, authenticated endpoints, and different parameters. Works for pagination, authenticated endpoints,
anything behind JavaScript rendering. and anything behind client-side rendering.
</P> </P>
</Section> </Section>
@@ -341,10 +371,10 @@ export default function IndexPage() {
<Section id="screen-recording" title="Screen recording"> <Section id="screen-recording" title="Screen recording">
<P> <P>
<strong>Record the active tab as video</strong> using{" "} Have the agent <strong>record what it{"'"}s doing</strong> as MP4
<Code>chrome.tabCapture</Code>. The recording runs in video. The recording uses <Code>chrome.tabCapture</Code> and
the extension context, so it <strong>survives page navigation</strong>. Video is saved runs in the extension context, so it <strong>survives page
as MP4. navigation</strong>.
</P> </P>
<CodeBlock lang="bash">{dedent` <CodeBlock lang="bash">{dedent`
@@ -368,7 +398,7 @@ export default function IndexPage() {
</P> </P>
<Caption> <Caption>
Native tab capture. 30&ndash;60fps. Survives navigation. Native tab capture. 30{"\u2013"}60fps. Survives navigation.
</Caption> </Caption>
</Section> </Section>
@@ -376,7 +406,7 @@ export default function IndexPage() {
<Section id="comparison" title="Comparison"> <Section id="comparison" title="Comparison">
<P> <P>
How Playwriter compares to other browser automation approaches. Why use this over the alternatives.
</P> </P>
<ComparisonTable <ComparisonTable
@@ -422,12 +452,11 @@ export default function IndexPage() {
<Section id="remote-access" title="Remote access"> <Section id="remote-access" title="Remote access">
<P> <P>
Control Chrome on <strong>any machine from anywhere</strong> over the internet. Control Chrome on a <strong>remote machine</strong> {" \u2014 "} a headless
The relay runs on the host alongside Chrome. Mac mini, a cloud VM, a devcontainer. A{" "}
A{" "}
<A href="https://traforo.dev">traforo</A>{" "} <A href="https://traforo.dev">traforo</A>{" "}
tunnel exposes it through Cloudflare, giving you a <strong>secure public URL</strong>. tunnel exposes the relay through Cloudflare. <strong>No VPN, no
No VPN, no firewall rules, no port forwarding. firewall rules, no port forwarding.</strong>
</P> </P>
<CodeBlock lang="bash">{dedent` <CodeBlock lang="bash">{dedent`
@@ -441,9 +470,9 @@ export default function IndexPage() {
`}</CodeBlock> `}</CodeBlock>
<P> <P>
Also works on a <strong>LAN without tunnels</strong> &mdash; just set{" "} Also works on a <strong>LAN without tunnels</strong> {" \u2014 "} just set{" "}
<Code>PLAYWRITER_HOST=192.168.1.10</Code>. Works for MCP <Code>PLAYWRITER_HOST=192.168.1.10</Code>. Works for MCP
too &mdash; set <Code>PLAYWRITER_HOST</Code> and{" "} too {" \u2014 "} set <Code>PLAYWRITER_HOST</Code> and{" "}
<Code>PLAYWRITER_TOKEN</Code> in your MCP client env config. <Code>PLAYWRITER_TOKEN</Code> in your MCP client env config.
Use cases: headless Mac mini, remote user support, Use cases: headless Mac mini, remote user support,
multi-machine automation, dev from a VM or devcontainer. multi-machine automation, dev from a VM or devcontainer.
@@ -454,28 +483,27 @@ export default function IndexPage() {
<Section id="security" title="Security"> <Section id="security" title="Security">
<P> <P>
Playwriter is <strong>local by default</strong>. The relay runs on{" "} Everything runs <strong>on your machine</strong>. The relay binds
<Code>localhost:19988</Code> and only accepts connections to <Code>localhost:19988</Code> and only accepts connections
from the extension. There&apos;s no remote server, no account, no from the extension. No remote server, no account, no telemetry.
telemetry.
</P> </P>
<List> <List>
<Li> <Li>
<strong>Local only</strong> &mdash; WebSocket server binds to <strong>Local only</strong> {" \u2014 "} WebSocket server binds to
localhost. Nothing leaves your machine. localhost. Nothing leaves your machine.
</Li> </Li>
<Li> <Li>
<strong>Origin validation</strong> &mdash; only the Playwriter <strong>Origin validation</strong> {" \u2014 "} only the Playwriter
extension origin is accepted. Browsers cannot spoof the Origin extension origin is accepted. Browsers cannot spoof the Origin
header, so malicious websites cannot connect. header, so malicious websites cannot connect.
</Li> </Li>
<Li> <Li>
<strong>Explicit consent</strong> &mdash; only tabs where you <strong>Explicit consent</strong> {" \u2014 "} only tabs where you
clicked the extension icon are controlled. No background access. clicked the extension icon are controlled. No background access.
</Li> </Li>
<Li> <Li>
<strong>Visible automation</strong> &mdash; Chrome shows an <strong>Visible automation</strong> {" \u2014 "} Chrome shows an
automation banner on controlled tabs. automation banner on controlled tabs.
</Li> </Li>
</List> </List>