From 61024853ca881f681fee3cc1988f6ec70eb647f5 Mon Sep 17 00:00:00 2001 From: 0thernet <894119+0thernet@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:15:13 -0400 Subject: [PATCH 1/2] Polish GhostGet editorial content and illustration system --- STYLE.md | 43 +- bun.lock | 4 +- package.json | 2 +- website/analytics.test.ts | 29 + website/blog.test.ts | 74 +- website/blog.ts | 803 +++++++++++------- website/build.ts | 48 +- website/editorial-images.ts | 470 +++++++--- .../agentic-web-spoofing/job.json | 1 + .../agentic-web-spoofing/prompt.txt | 3 + .../agentic-web-spoofing/receipt.json | 1 + .../brand-series/built-on-ghostget/job.json | 1 + .../brand-series/built-on-ghostget/prompt.txt | 3 + .../built-on-ghostget/receipt.json | 1 + .../ghostget-claims-register/job.json | 1 + .../ghostget-claims-register/prompt.txt | 3 + .../ghostget-claims-register/receipt.json | 1 + .../how-agents-reach-the-web/job.json | 1 + .../how-agents-reach-the-web/prompt.txt | 3 + .../how-agents-reach-the-web/receipt.json | 1 + .../introducing-ghostget/job.json | 1 + .../introducing-ghostget/prompt.txt | 3 + .../introducing-ghostget/receipt.json | 1 + .../personal-agents-browser-use/job.json | 1 + .../personal-agents-browser-use/prompt.txt | 3 + .../personal-agents-browser-use/receipt.json | 1 + .../releases-that-prove-their-origin/job.json | 1 + .../prompt.txt | 3 + .../receipt.json | 1 + .../replayable-property-tests/job.json | 1 + .../replayable-property-tests/prompt.txt | 3 + .../replayable-property-tests/receipt.json | 1 + .../brand-series/review.json | 21 + .../vms-cannot-contain-agents/job.json | 1 + .../vms-cannot-contain-agents/prompt.txt | 3 + .../vms-cannot-contain-agents/receipt.json | 1 + .../brand-series/webmcp/job.json | 1 + .../brand-series/webmcp/prompt.txt | 3 + .../brand-series/webmcp/receipt.json | 1 + .../reviews/blog-editorial-review.json | 114 +++ .../reviews/primary-source-checks.json | 97 +++ website/essay-reviews.ts | 65 ++ website/launch/launch.test.ts | 4 +- .../agentic-web-spoofing-paper-384.webp | Bin 0 -> 5766 bytes .../agentic-web-spoofing-paper-768.webp | Bin 0 -> 40438 bytes .../editorial/agentic-web-spoofing-paper.webp | Bin 0 -> 283964 bytes .../built-on-ghostget-paper-384.webp | Bin 0 -> 4784 bytes .../built-on-ghostget-paper-768.webp | Bin 0 -> 22082 bytes .../editorial/built-on-ghostget-paper.webp | Bin 0 -> 159402 bytes .../ghostget-claims-register-paper-384.webp | Bin 0 -> 8458 bytes .../ghostget-claims-register-paper-768.webp | Bin 0 -> 45168 bytes .../ghostget-claims-register-paper.webp | Bin 0 -> 260158 bytes .../how-agents-reach-the-web-paper-384.webp | Bin 0 -> 9346 bytes .../how-agents-reach-the-web-paper-768.webp | Bin 0 -> 35944 bytes .../how-agents-reach-the-web-paper.webp | Bin 0 -> 247520 bytes .../introducing-ghostget-paper-384.webp | Bin 0 -> 6274 bytes .../introducing-ghostget-paper-768.webp | Bin 0 -> 25636 bytes .../editorial/introducing-ghostget-paper.webp | Bin 0 -> 167244 bytes ...personal-agents-browser-use-paper-384.webp | Bin 0 -> 5868 bytes ...personal-agents-browser-use-paper-768.webp | Bin 0 -> 37706 bytes .../personal-agents-browser-use-paper.webp | Bin 0 -> 294648 bytes ...ses-that-prove-their-origin-paper-384.webp | Bin 0 -> 5648 bytes ...ses-that-prove-their-origin-paper-768.webp | Bin 0 -> 41858 bytes ...eleases-that-prove-their-origin-paper.webp | Bin 0 -> 296548 bytes .../replayable-property-tests-paper-384.webp | Bin 0 -> 7832 bytes .../replayable-property-tests-paper-768.webp | Bin 0 -> 36100 bytes .../replayable-property-tests-paper.webp | Bin 0 -> 237686 bytes .../vms-cannot-contain-agents-paper-384.webp | Bin 0 -> 4478 bytes .../vms-cannot-contain-agents-paper-768.webp | Bin 0 -> 18980 bytes .../vms-cannot-contain-agents-paper.webp | Bin 0 -> 149464 bytes .../images/editorial/webmcp-paper-384.webp | Bin 0 -> 5658 bytes .../images/editorial/webmcp-paper-768.webp | Bin 0 -> 33518 bytes .../public/images/editorial/webmcp-paper.webp | Bin 0 -> 273754 bytes website/site.test.ts | 276 +----- website/source/404.html | 3 +- website/source/about.html | 3 +- website/source/agentic-web-spoofing.html | 115 +-- website/source/analytics-contract.ts | 3 + website/source/blog.html | 3 +- website/source/blog/built-on-ghostget.html | 14 +- .../source/blog/ghostget-claims-register.html | 136 +-- website/source/blog/introducing-ghostget.html | 59 +- .../releases-that-prove-their-origin.html | 119 +-- .../blog/replayable-property-tests.html | 91 +- website/source/claims.html | 3 +- website/source/compare-agent-browser.html | 6 +- website/source/compare-browser-use.html | 6 +- website/source/compare-browserbase.html | 6 +- website/source/compare-firecrawl.html | 6 +- website/source/compare-index.html | 61 +- website/source/compare-jina-reader.html | 6 +- .../compare-personal-agents-browser-use.html | 119 +-- website/source/compare-playwright-mcp.html | 6 +- website/source/contact.html | 3 +- .../docs-explanation-security-model.html | 3 +- .../docs-how-to-author-provider-plugin.html | 3 +- .../docs-how-to-capture-and-archive.html | 3 +- .../source/docs-how-to-connect-beeper.html | 3 +- .../source/docs-how-to-export-whatsapp.html | 3 +- .../source/docs-how-to-use-webmcp-sites.html | 3 +- website/source/docs-index.html | 3 +- .../docs-reference-provider-capabilities.html | 3 +- .../docs-tutorials-getting-started.html | 3 +- website/source/index.html | 2 - website/source/llms.txt | 4 +- website/source/omarchy-root-escalation.html | 107 +-- .../source/paypal-grapheneos-attestation.html | 108 +-- website/source/privacy.html | 3 +- website/source/rumour-is-the-exploit.html | 107 +-- website/source/styles.css | 14 + website/source/vms-cannot-contain-agents.html | 114 +-- website/source/webmcp.html | 30 +- 112 files changed, 1698 insertions(+), 1686 deletions(-) create mode 100644 website/editorial-provenance/brand-series/agentic-web-spoofing/job.json create mode 100644 website/editorial-provenance/brand-series/agentic-web-spoofing/prompt.txt create mode 100644 website/editorial-provenance/brand-series/agentic-web-spoofing/receipt.json create mode 100644 website/editorial-provenance/brand-series/built-on-ghostget/job.json create mode 100644 website/editorial-provenance/brand-series/built-on-ghostget/prompt.txt create mode 100644 website/editorial-provenance/brand-series/built-on-ghostget/receipt.json create mode 100644 website/editorial-provenance/brand-series/ghostget-claims-register/job.json create mode 100644 website/editorial-provenance/brand-series/ghostget-claims-register/prompt.txt create mode 100644 website/editorial-provenance/brand-series/ghostget-claims-register/receipt.json create mode 100644 website/editorial-provenance/brand-series/how-agents-reach-the-web/job.json create mode 100644 website/editorial-provenance/brand-series/how-agents-reach-the-web/prompt.txt create mode 100644 website/editorial-provenance/brand-series/how-agents-reach-the-web/receipt.json create mode 100644 website/editorial-provenance/brand-series/introducing-ghostget/job.json create mode 100644 website/editorial-provenance/brand-series/introducing-ghostget/prompt.txt create mode 100644 website/editorial-provenance/brand-series/introducing-ghostget/receipt.json create mode 100644 website/editorial-provenance/brand-series/personal-agents-browser-use/job.json create mode 100644 website/editorial-provenance/brand-series/personal-agents-browser-use/prompt.txt create mode 100644 website/editorial-provenance/brand-series/personal-agents-browser-use/receipt.json create mode 100644 website/editorial-provenance/brand-series/releases-that-prove-their-origin/job.json create mode 100644 website/editorial-provenance/brand-series/releases-that-prove-their-origin/prompt.txt create mode 100644 website/editorial-provenance/brand-series/releases-that-prove-their-origin/receipt.json create mode 100644 website/editorial-provenance/brand-series/replayable-property-tests/job.json create mode 100644 website/editorial-provenance/brand-series/replayable-property-tests/prompt.txt create mode 100644 website/editorial-provenance/brand-series/replayable-property-tests/receipt.json create mode 100644 website/editorial-provenance/brand-series/review.json create mode 100644 website/editorial-provenance/brand-series/vms-cannot-contain-agents/job.json create mode 100644 website/editorial-provenance/brand-series/vms-cannot-contain-agents/prompt.txt create mode 100644 website/editorial-provenance/brand-series/vms-cannot-contain-agents/receipt.json create mode 100644 website/editorial-provenance/brand-series/webmcp/job.json create mode 100644 website/editorial-provenance/brand-series/webmcp/prompt.txt create mode 100644 website/editorial-provenance/brand-series/webmcp/receipt.json create mode 100644 website/editorial-provenance/reviews/blog-editorial-review.json create mode 100644 website/editorial-provenance/reviews/primary-source-checks.json create mode 100644 website/essay-reviews.ts create mode 100644 website/public/images/editorial/agentic-web-spoofing-paper-384.webp create mode 100644 website/public/images/editorial/agentic-web-spoofing-paper-768.webp create mode 100644 website/public/images/editorial/agentic-web-spoofing-paper.webp create mode 100644 website/public/images/editorial/built-on-ghostget-paper-384.webp create mode 100644 website/public/images/editorial/built-on-ghostget-paper-768.webp create mode 100644 website/public/images/editorial/built-on-ghostget-paper.webp create mode 100644 website/public/images/editorial/ghostget-claims-register-paper-384.webp create mode 100644 website/public/images/editorial/ghostget-claims-register-paper-768.webp create mode 100644 website/public/images/editorial/ghostget-claims-register-paper.webp create mode 100644 website/public/images/editorial/how-agents-reach-the-web-paper-384.webp create mode 100644 website/public/images/editorial/how-agents-reach-the-web-paper-768.webp create mode 100644 website/public/images/editorial/how-agents-reach-the-web-paper.webp create mode 100644 website/public/images/editorial/introducing-ghostget-paper-384.webp create mode 100644 website/public/images/editorial/introducing-ghostget-paper-768.webp create mode 100644 website/public/images/editorial/introducing-ghostget-paper.webp create mode 100644 website/public/images/editorial/personal-agents-browser-use-paper-384.webp create mode 100644 website/public/images/editorial/personal-agents-browser-use-paper-768.webp create mode 100644 website/public/images/editorial/personal-agents-browser-use-paper.webp create mode 100644 website/public/images/editorial/releases-that-prove-their-origin-paper-384.webp create mode 100644 website/public/images/editorial/releases-that-prove-their-origin-paper-768.webp create mode 100644 website/public/images/editorial/releases-that-prove-their-origin-paper.webp create mode 100644 website/public/images/editorial/replayable-property-tests-paper-384.webp create mode 100644 website/public/images/editorial/replayable-property-tests-paper-768.webp create mode 100644 website/public/images/editorial/replayable-property-tests-paper.webp create mode 100644 website/public/images/editorial/vms-cannot-contain-agents-paper-384.webp create mode 100644 website/public/images/editorial/vms-cannot-contain-agents-paper-768.webp create mode 100644 website/public/images/editorial/vms-cannot-contain-agents-paper.webp create mode 100644 website/public/images/editorial/webmcp-paper-384.webp create mode 100644 website/public/images/editorial/webmcp-paper-768.webp create mode 100644 website/public/images/editorial/webmcp-paper.webp diff --git a/STYLE.md b/STYLE.md index 58e5f9bd..ad4b21ca 100644 --- a/STYLE.md +++ b/STYLE.md @@ -1,10 +1,10 @@ # Public writing style -This guide covers everything written for readers outside a repository: product pages, documentation, READMEs, interface text, metadata, and text a model writes for publication. Apply the voice rules in [`WRITING.md`](WRITING.md) first. The [documentation guidelines](https://github.com/hraness/.github/blob/main/DOCUMENTATION_GUIDELINES.md) choose a document's purpose and shape, and the [README guidelines](https://github.com/hraness/.github/blob/main/README_GUIDELINES.md) cover the repository front door. +This guide covers everything written for readers outside a repository: product pages, documentation, READMEs, interface text, metadata, and text a model writes for publication. Apply the voice rules in [`WRITING.md`](WRITING.md) first. The [documentation guidelines](DOCUMENTATION_GUIDELINES.md) choose a document's purpose and shape, and the [README guidelines](README_GUIDELINES.md) cover the repository front door. Public prose must be precise, useful, and free of hype. Use a direct, natural voice that reads well aloud. -This is a synced copy of the [Hraness public writing style](https://github.com/hraness/.github/blob/main/STYLE.md), kept here so agents can read it offline. Repository-specific rules appear under “Repository additions” below. +This is the canonical copy. Repositories keep a synced copy so agents can read it offline; rules that apply to one repository go under “Repository additions” at the end of that copy. ## Leave the reader with a clearer model @@ -120,11 +120,20 @@ Most Hraness copy is drafted by agents working inside repository guides full of - Use shared foreground and surface pairs for interactive controls. Control text and glyphs must reach at least 4.5:1 contrast in both themes, including selected, hover, and focus states. Never assume white is readable on a brand accent. - Review the page as a new visitor at desktop and phone widths. Confirm that the headline, example, and next action make sense before reading the documentation, and that essential limits appear beside the claims they qualify. +## Write evergreen explanations + +- Give an educational article one useful question, a clear answer, and a concrete example the reader can reason through or apply. Explain the cause, choice, or tradeoff; a feature list with an introduction is not an explanation. +- Write about the enduring idea and the public behavior a reader can use. Keep implementation diaries, internal file paths, test totals, task records, deployment history, and plans out of the article body. Link to public reference material when exact syntax or architecture helps the reader go further. +- Keep release chronology in release notes and current setup requirements beside the relevant command. Do not date an evergreen explanation with “as of”, “recently”, a build number, or the author's review date. Preserve real publication and review dates in their metadata; include a historical date in prose only when the event's timing is part of the explanation. +- State claims at their supported scope. Remove repeated permissions language, generic disclaimers, and defensive lists of things the article does not establish. Keep a material limitation beside the decision it changes, and link to detailed policy or reference where needed. +- Check product names, destination links, public behavior, and image credits together. Replace a stale explanation rather than adding a caveat about its age. Never relabel historical evidence or a tool-generated asset as if it came from a different source. +- Improve the existing collection before adding articles. Add a post for a distinct reader question that existing pages do not answer, with its own example and sources. Do not create thin variations to fill a series or repeat the product's pitch. + ## State each limit once Readers trust a page that states its limits plainly. They skim a page that repeats them. -- State the product's status once, near the top, with one of these labels: *In development*, *Preview*, *Beta*, *Latest release: vX.Y.Z*, *Paused*, or *Retired*. Follow it with one sentence on how to install or use it today, such as “Install from source; there is no signed release yet.” +- State release status where it changes how someone can use the product. Keep the current version with installation instructions and explain an unavailable capability beside the affected action. Do not add a release label or status paragraph to every educational page. - Put each other limit beside the feature it limits, once. Link to the status or limits page instead of restating the caveat in each section. Never drop a true limit to make the copy read better. - Write a claim at its true scope instead of following it with what it does not prove. “Tests cover local networks only” replaces “These are tested local cases, not hosted private networking or evidence about independent devices.” - State a privacy or scope rule once, positively (“Only documents you choose to publish become public”), and keep the full list of exclusions on the privacy or security page. @@ -164,7 +173,7 @@ Readers trust a page that states its limits plainly. They skim a page that repea - Put literal input and interface values in `code`. - Use an ellipsis glyph (`…`) only when an action opens another input step. - Do not use em dashes in authored text: prose, titles, meta descriptions, social text, alt text, captions, image credits, list separators, and the templates that generate them. Rewrite the sentence instead of substituting a spaced hyphen. Quoted third-party titles keep their own punctuation. Use parentheses only for a short, necessary explanation. -- Use each product's prose name exactly as its messaging record spells it (`names.name`), including case (xcb, Textbutler, AI Charts, Soundfish, Sys1). The all-capitals `names.catalog` form belongs only in designs that set every name in capitals. Do not use the repository slug or the domain as the name in prose, and do not use a product name as a common noun. +- Use each product's prose name exactly as its messaging record spells it (`names.name`), including case. The all-capitals `names.catalog` form belongs only in designs that set every name in capitals. Do not use the repository slug or the domain as the name in prose, and do not use a product name as a common noun. - Give each destination one label across the header, footer, breadcrumbs, and Markdown twins. - Make interpolated counts agree with their nouns (“1 check”, “2 checks”), and test zero, one, and several. - Spell out zero through nine in prose. Use numerals for 10 or more, measurements, dates, and money. @@ -177,6 +186,22 @@ Readers trust a page that states its limits plainly. They skim a page that repea - Preserve meaningful evidence captions, legal notices, and actual feature limits. State each once beside the claim it qualifies. - Credit tools and models by their current names. +## Keep article illustrations coherent + +- Give a product's article collection one authored visual direction: consistent drawing technique, line weight, texture, framing, and degree of abstraction. Start with a reviewed image from the collection and the product's brand palette. +- Use a quiet neutral ground, one dominant brand color, and at most one supporting accent. Keep the accent subordinate and use tonal variations for depth. Avoid unrelated bright colors, glossy stock-art treatments, and a different style for each topic. +- Generate editorial artwork through [SlopCamera](https://slopcamera.com). Retain its authored prompt, reference assets, and generation record with the source. Inspect the image at article and card sizes in both page themes before accepting it. +- Make each illustration explain the article's central idea through one clear visual relationship. Avoid generic technology collages, decorative interface fragments, and text baked into images. A diagram may use labels when those labels carry the explanation. +- Give every illustrated article a complete, intentional card and lead-image treatment. Preserve the actual generation history when replacing an old asset; credit the tool that made the new image and link its current public site. + +## Write social text + +- Posts on X, Bluesky, Threads, and LinkedIn follow every rule here, including no em dashes and no exclamation marks. +- Use no emoji, hashtags, or thread numbering unless a repository addition allows them for one channel. +- Keep image alt text to 125 characters or fewer. +- Link the canonical URL without tracking parameters. +- Launch posts follow the launch beats and social posts addendum in [`GENERATION_STYLE.md`](GENERATION_STYLE.md), and channel limits are in [`MESSAGING.md`](MESSAGING.md). + ## Write focused documentation - Decide whether a page is a tutorial, how-to guide, explanation, or reference. @@ -196,7 +221,7 @@ Readers trust a page that states its limits plainly. They skim a page that repea - Use one literal heading for the object, task, data view, or state. - Add supporting text only for a distinct instruction, constraint, status, or scope. - Name the action, object, current state, limit, or recovery step. -- Do not narrate the interface or repeat visible information. +- Do not narrate the interface or repeat visible information. Omit labels announcing how many cards are on screen or describing an obvious preview; retain counts only when they help navigation, selection, or comparison. - Keep normal readiness silent. Show status text for pending work, important results, or problems that the reader can fix. - Add search only when the collection is too large or varied for direct selection. - Move secondary actions and settings out of persistent primary controls. @@ -211,7 +236,7 @@ Readers trust a page that states its limits plainly. They skim a page that repea - Name the consequence in a confirmation. Repeat the exact verb and object for a destructive action. - Use nouns for labels. Use placeholders for a format or example, not a repeated label. - State the completed result in past tense in a toast notification. -- Follow [`CLI_MENU_STYLE.md`](https://github.com/hraness/.github/blob/main/CLI_MENU_STYLE.md) for command-line output, menu bar menus, and macOS permission notices. +- Follow [`CLI_MENU_STYLE.md`](https://github.com/hraness/.github/blob/main/CLI_MENU_STYLE.md) for command-line output, the shared status and control commands, and macOS permission notices. ## Vary a generated series @@ -236,7 +261,7 @@ A prompt, skill, or template that makes a model write published text is public c - Read a sample of real outputs after every prompt change. - Tell the model who reads the output and that the reader has not seen the inputs or the instructions. Name every field that is published, including rationales and labels. - Set length limits as maximums. A minimum longer than the evidence forces padding. -- Include the shared generation block from [`GENERATION_STYLE.md`](https://github.com/hraness/.github/blob/main/GENERATION_STYLE.md) and record its version with the prompt version. +- Include the shared generation block from [`GENERATION_STYLE.md`](GENERATION_STYLE.md) and record its version with the prompt version. - Check the prompt, skill, and examples for the patterns they forbid; a prompt that uses em dashes and staged contrasts produces them. ## Keep tests and guides from freezing copy @@ -248,7 +273,7 @@ A prompt, skill, or template that makes a model write published text is public c ## Say who wrote and who checked -- Show AI-drafting disclosure on hraness.com through its shared disclosure component, on every page with AI-drafted text. Essays and blog posts on any Hraness site also show the provenance note from [`GENERATION_STYLE.md`](https://github.com/hraness/.github/blob/main/GENERATION_STYLE.md), naming the recorded reviewer. Other pages on other Hraness sites and products do not carry AI-drafting disclosures, labels, or badges. +- Show AI-drafting disclosure on hraness.com through its shared disclosure component, on every page with AI-drafted text. Essays and blog posts on any Hraness site also show the provenance note from [`GENERATION_STYLE.md`](GENERATION_STYLE.md), naming the recorded reviewer. Other pages on other Hraness sites and products do not carry AI-drafting disclosures, labels, or badges. - Everywhere, keep a record of who drafted and who reviewed generated or agent-drafted text: the author, an independent human, or an AI agent, by name. - Never credit AI-drafted text to a person as its sole author, never describe AI review as human review, and never claim a review that has no record. A page without a review record makes no review claim. - Text an agent posts from a person's account does not claim that person wrote AI-drafted work. @@ -258,5 +283,5 @@ A prompt, skill, or template that makes a model write published text is public c ### Generated pages and status notes - The WebMCP provider pages are generated from third-party registry data. Every interpolated count agrees with its noun (test zero, one, and several), third-party text is clipped at a word boundary and ends with “…”, and a page promises use (“Use X with your agent”) only when the registry lists a tool the agent can call. -- A review note states which Ghostget release a page describes. It does not say the page “was checked” unless a review record exists. +- Keep release versions and check dates in reference material and review metadata. Evergreen editorial pages omit publication, update, and review-date chrome. Cite dates in the prose only when the date affects the explanation. - `website/AGENTS.md` lists the internal words that most often reach ghostget.com and what to write instead. diff --git a/bun.lock b/bun.lock index 5e8ababf..5e1cf1da 100644 --- a/bun.lock +++ b/bun.lock @@ -20,7 +20,7 @@ "typescript": "6.0.3", }, "devDependencies": { - "@hraness/design-kit": "github:hraness/design-kit#v0.35.0", + "@hraness/design-kit": "github:hraness/design-kit#v0.35.2", "@hraness/direct": "https://github.com/hraness/direct/releases/download/v0.7.21/hraness-direct-0.7.21.tgz", "@hraness/posthog": "https://github.com/hraness/posthog/releases/download/v0.3.9/hraness-posthog-0.3.9.tgz", "@hraness/site-footer": "https://github.com/hraness/site-footer/releases/download/v0.20.5/hraness-site-footer-0.20.5.tgz", @@ -128,7 +128,7 @@ "@hraness/accounts-cli": ["@hraness/accounts-cli@github:hraness/accounts-cli#eaad8cb", { "dependencies": { "@hraness/suite-accounts": "github:hraness/suite-accounts#v0.9.13" } }, "hraness-accounts-cli-eaad8cb", "sha512-/r6nTHa9buZj9dLP4dgRtPkrKpfhs3/7gwcR/r3aAFnNmLXtUBms60NPP7O4EOel524638UhEnvYuZ1TSlgtAA=="], - "@hraness/design-kit": ["@hraness/design-kit@github:hraness/design-kit#1f4aa08", { "dependencies": { "@hugeicons/core-free-icons": "4.2.3", "@stylexjs/stylex": "0.19.0", "motion": "12.42.0", "next-themes": "0.4.6", "react-aria-components": "1.19.0", "recharts": "3.8.0", "sugar-high": "^1.2.1", "web-haptics": "0.0.6" }, "peerDependencies": { "@hraness/ui": ">=0.5.16 <0.6.0", "react": ">=18 <20", "react-dom": ">=18 <20" }, "optionalPeers": ["@hraness/ui"] }, "hraness-design-kit-1f4aa08", "sha512-3Xa04ZJGeSFcwWJ+ncG5opo2lp8op4dLqdzjVokoVe4Itd3YUHNtJbOk9wb5t9TF8EX1T7jWfWlJCkSTmejkPA=="], + "@hraness/design-kit": ["@hraness/design-kit@github:hraness/design-kit#229720e", { "dependencies": { "@hugeicons/core-free-icons": "4.2.3", "@stylexjs/stylex": "0.19.0", "motion": "12.42.0", "next-themes": "0.4.6", "react-aria-components": "1.19.0", "recharts": "3.8.0", "sugar-high": "^1.2.1", "web-haptics": "0.0.6" }, "peerDependencies": { "@hraness/ui": ">=0.5.16 <0.6.0", "react": ">=18 <20", "react-dom": ">=18 <20" }, "optionalPeers": ["@hraness/ui"] }, "hraness-design-kit-229720e", "sha512-GTknQ8JDWEVucTlpy+mEpipgc4vVOanbwp0k9R6/w93KsKSXL/xdpuCoZBJh3XUbSFs5HdrIXI2s5IldmbPovQ=="], "@hraness/desktop-foundation": ["@hraness/desktop-foundation@https://github.com/hraness/desktop-foundation/releases/download/v0.9.0/hraness-desktop-foundation-0.9.0.tgz", { "bin": { "companion": "./dist/src/cli.js" } }, "sha512-+TlrHoM1wZ2rE4p3u1CIAz1+aGKPZhmiNOKjEKOYPZmTx8BFSyFLPeL06tupfX2WxcRzRZ3QwmOsyjlgSu/p1Q=="], diff --git a/package.json b/package.json index 5c94bad0..25240c4b 100644 --- a/package.json +++ b/package.json @@ -708,7 +708,7 @@ "@steipete/sweet-cookie": "0.4.3" }, "devDependencies": { - "@hraness/design-kit": "github:hraness/design-kit#v0.35.0", + "@hraness/design-kit": "github:hraness/design-kit#v0.35.2", "@hraness/direct": "https://github.com/hraness/direct/releases/download/v0.7.21/hraness-direct-0.7.21.tgz", "@hraness/posthog": "https://github.com/hraness/posthog/releases/download/v0.3.9/hraness-posthog-0.3.9.tgz", "@hraness/site-footer": "https://github.com/hraness/site-footer/releases/download/v0.20.5/hraness-site-footer-0.20.5.tgz", diff --git a/website/analytics.test.ts b/website/analytics.test.ts index c03bbf63..71201d42 100644 --- a/website/analytics.test.ts +++ b/website/analytics.test.ts @@ -216,6 +216,35 @@ describe("ghostget.com analytics contract", () => { expect(ATTRIBUTION_PARAMETERS.has("ref")).toBe(false); }); + test("drops private data in property names at the root and inside objects and arrays", () => { + for (const key of [ + "0@-.AA", + "reader@example.com", + "reader%40example.com", + "phx_private_key", + "Bearer private_access_token", + "https://example.com/path?code=private_value", + "/path?token=private_value", + ]) { + const capture = sanitizeCapture({ + event: "$pageview", + properties: { + [key]: "present", + $current_url: home.href, + $lib: "posthog-js", + token, + utm_source: "news", + safe_metric: 1, + diagnostic: { [key]: "present", safe_metric: 2, items: [{ [key]: "present", safe_metric: 3 }] }, + }, + }, home); + expect(capture).not.toBeNull(); + expect(Object.hasOwn(capture!.properties, key)).toBe(false); + expect(capture!.properties.diagnostic).toEqual({ safe_metric: 2, items: [{ safe_metric: 3 }] }); + expect(capture!.properties).toMatchObject({ $lib: "posthog-js", token, utm_source: "news", safe_metric: 1 }); + } + }); + test("drops foreign, preview, and local hosts", () => { for (const href of ["https://preview.example.com/", "https://ghostget-git-x.vercel.app/", "http://localhost:3000/"]) { expect(sanitizeCapture({ event: "$pageview", properties: { $current_url: href, token } }, evidenceFor("/", href))).toBeNull(); diff --git a/website/blog.test.ts b/website/blog.test.ts index c3f9e0d8..474922aa 100644 --- a/website/blog.test.ts +++ b/website/blog.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -14,6 +15,7 @@ import { BLOG_POSTS, BUILT_ON_RELATIONS, blogPostPath, + blogPostJsonLd, blogSitemapPaths, fillBlogShell, ghostgetRelations, @@ -27,6 +29,8 @@ import { type BlogSite, } from "./blog"; +import { editorialImages, editorialImage, editorialImageUrl, editorialImageSrcSet } from "./editorial-images"; + const websiteRoot = dirname(fileURLToPath(import.meta.url)); const site: BlogSite = { description: "GhostGet test site.", @@ -47,7 +51,7 @@ describe("GhostGet blog admission", () => { expect(() => assertArticleAdmissions(admissions)).not.toThrow(); for (const post of BLOG_POSTS) { expect(post.admission.href).toBe(blogPostPath(post.slug)); - expect(post.admission.drafting).toBe("ai-from-source"); + expect(post.admission.drafting).toBe("ai"); expect(post.admission.review?.reviewerType).toBe("ai"); expect(post.admission.humanReview).toBeNull(); if (post.admission.lifecycle === "indexable") { @@ -60,24 +64,25 @@ describe("GhostGet blog admission", () => { test("an AI review is disclosed as AI and never called human", () => { for (const post of BLOG_POSTS) { const sentence = articleProvenanceSentence(articleProvenanceFromAdmission(post.admission)); - expect(sentence).toBe(`Drafted with AI from the source code and reviewed by ${post.admission.review?.reviewer}.`); - expect(sentence).toMatch(/\bClaude\b/u); + expect(sentence).toBe(`Drafted with AI and reviewed by ${post.admission.review?.reviewer}.`); + expect(sentence).toMatch(/\bAI\b/u); expect(sentence).not.toMatch(/human/iu); } }); - test("every post body renders with the Hraness byline, provenance note, sources, and related products", async () => { + test("every post body renders with the Hraness byline, provenance note, sources, and the article image", async () => { for (const post of BLOG_POSTS) { const fragment = await readFile(join(websiteRoot, "source/blog", post.bodyFile), "utf8"); expect(fragment).not.toContain("By Hraness"); + expect(main).toContain('By '); expect(main).toContain('class="plain-publication__provenance"'); expect(main).toContain('class="plain-publication__sources"'); - expect(main).toContain('class="plain-publication__related"'); - expect(main.match(/\{\{[A-Z0-9_]+\}\}/gu) ?? []).toEqual( - fragment.includes("{{GHOSTGET_RELEASE}}") ? expect.arrayContaining(["{{GHOSTGET_RELEASE}}"]) : [], - ); + expect(main).toContain('class="editorial-figure"'); + expect(main).not.toMatch(/(?:Published|Updated|Checked)