Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# <img src="assets/icon/icon.iconset/icon_128x128.png" alt="Diffy chameleon" width="40" height="40" align="middle"> Diffy

[Downloads and installation](https://diffy.hitslop.com/downloads) · macOS **beta** · Windows/Linux **alpha**
[Downloads and installation](https://diffy.run/downloads) · macOS **beta** · Windows/Linux **alpha**

![Diffy on macOS: a split Rust diff, project change badges, a searchable file list grouped by directory, and project import](assets/screenshot.png)

Expand Down
2 changes: 1 addition & 1 deletion RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ gh release create v0.2.3 --repo hitSlop/diffy --draft --verify-tag \

The manifest archive URL points to its immutable version tag. The app checks `releases/latest/download/update-macos-aarch64.json`, so draft releases are intentionally invisible to automatic checks. Publish only after release verification and explicit release authorization.

Always upload the fixed-name DMG with every release. The website redirects `https://diffy.hitslop.com/download` to GitHub's latest published release asset with that name. Include both DMGs in `SHA256SUMS`; retain the versioned app archive and manifest used by in-app updates.
Always upload the fixed-name DMG with every release. The website redirects `https://diffy.run/download` to GitHub's latest published release asset with that name. Include both DMGs in `SHA256SUMS`; retain the versioned app archive and manifest used by in-app updates.

## Update verification

Expand Down
14 changes: 10 additions & 4 deletions site/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Diffy landing page

Astro static site for https://diffy.hitslop.com. The page uses Commit Mono and a compact, responsive hero banner from `src/assets/hero.png`, with one small bundled script for the cursor-following SVG eye. It has no tracking or payment verification. Touch devices, reduced-motion users, and JavaScript failures keep the original static artwork. Animation stops when settled or offscreen.
Astro static site for https://diffy.run. The page uses Commit Mono and a compact, responsive hero banner from `src/assets/hero.png`, with one small bundled script for the cursor-following SVG eye. It has no tracking or payment verification. Touch devices, reduced-motion users, and JavaScript failures keep the original static artwork. Animation stops when settled or offscreen.

```sh
cd site
Expand All @@ -14,7 +14,7 @@ npm run preview

Set `DODO_CHECKOUT_URL` to the HTTPS hosted product checkout URL (for example `https://checkout.dodopayments.com/buy/pdt_example`) at build time. Do not set a private API key. Without the URL, the purchase button is disabled and shows “Checkout coming soon.”

Configure a one-time USD $14.99 product in Dodo. The build sets the checkout return URL to `https://diffy.hitslop.com/thanks`. Put `https://diffy.hitslop.com/downloads` in a Digital Files entitlement's external delivery link and attach it to the product as well, so buyers can keep it. Dodo setup remains manual until the product is ready.
Configure a one-time USD $14.99 product in Dodo. The build sets the checkout return URL to `https://diffy.run/thanks`. Put `https://diffy.run/downloads` in a Digital Files entitlement's external delivery link and attach it to the product as well, so buyers can keep it. Dodo setup remains manual until the product is ready.

```sh
DODO_CHECKOUT_URL='https://checkout.dodopayments.com/buy/YOUR_PRODUCT_ID' npm run build
Expand Down Expand Up @@ -46,7 +46,13 @@ Production builds separately from the test matrix, so the placeholder checkout U

The commands above remain available for manual deployment with your existing local Wrangler login. Rebuild with the correct production checkout setting before uploading; never deploy an artifact built with the CI checkout fixture.

Associate `diffy.hitslop.com` in the Pages project's custom domains, then point its DNS CNAME at the project's Pages hostname. Cloudflare must register the custom domain before its DNS redirect can work.
Keep domain registration at Namecheap and use Cloudflare DNS for `diffy.run`. Associate `diffy.run` in the Pages project's custom domains, then point its proxied apex CNAME at `diffy-site.pages.dev`. The Pages custom-domain association is required in addition to DNS. `www.diffy.run` redirects to the apex, preserving paths and query strings. The former subdomain has been retired without a redirect.

## Crawlers and product facts

The site is prerendered HTML; product information and download instructions do not require JavaScript. `robots.txt` allows all crawlers and advertises `/sitemap.xml`. Keep Cloudflare's Search, Agent, and Training crawler policies set to Allow, with no AI crawler paywall or bot challenges on public documentation.

`src/data/product.ts` supplies the product definition, visible questions, and `/llms-full.txt`; `/llms.txt` links to the canonical documentation. These text endpoints are an additional convenience for assistants, not a ranking guarantee. `/sitemap.xml` includes the homepage and downloads; `/thanks` and the error page use `noindex, follow`. JSON-LD describes the publisher, website, pages, and software application using factual capabilities, with no fabricated reviews or unavailable purchase offers. Update the shared facts when availability or pricing changes.

The post-build `scripts/write-redirects.mjs` generates `dist/_redirects` from the same platform metadata used by the pages. `/download` retains its macOS behavior. `/download/macos`, `/download/windows`, and `/download/linux` use fixed-name assets in GitHub's latest release; unavailable targets redirect to `/downloads` instead. The release script creates an identical copy of the signed and notarized versioned DMG. Upload it with every release; never point the website at an unsigned build.

Expand All @@ -64,7 +70,7 @@ The landing page displays regular price $24.99 crossed out and $14.99 during bet

## Dodo dashboard setup

No Dodo CLI or product is configured for this repository. Create one **Diffy — all desktop platforms** product with a one-time **USD $14.99** price (1499 cents), covering macOS beta and Windows/Linux alpha. Create a Digital Files entitlement named **Diffy downloads**, set its external link to **https://diffy.hitslop.com/downloads**, and attach it to the product. Suggested delivery instructions:
No Dodo CLI or product is configured for this repository. Create one **Diffy — all desktop platforms** product with a one-time **USD $14.99** price (1499 cents), covering macOS beta and Windows/Linux alpha. Create a Digital Files entitlement named **Diffy downloads**, set its external link to **https://diffy.run/downloads**, and attach it to the product. Suggested delivery instructions:

> Choose your platform on the download page. All versions require Git. macOS is beta; Windows and Linux are early alphas with limited hardware testing. Save the page for future releases.

Expand Down
2 changes: 1 addition & 1 deletion site/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineConfig } from 'astro/config';

export default defineConfig({
site: 'https://diffy.hitslop.com',
site: 'https://diffy.run',
output: 'static',
// Keep the eye controller external so script-src 'self' can remain strict.
vite: { build: { assetsInlineLimit: 0 } },
Expand Down
2 changes: 2 additions & 0 deletions site/public/robots.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
User-agent: *
Allow: /

Sitemap: https://diffy.run/sitemap.xml
37 changes: 35 additions & 2 deletions site/scripts/check-build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ assert(home.includes('<s>$24.99</s>') && home.includes('during beta'));
assert(!home.includes('Downloads &amp; installation') && !home.includes('Diffy is in beta.'));
const checkout = process.env.DODO_CHECKOUT_URL;
assert.equal(home.includes('checkout.dodopayments.com/buy/'), !!checkout);
if (checkout) assert(home.includes('redirect_url=https%3A%2F%2Fdiffy.hitslop.com%2Fthanks'));
if (checkout) assert(home.includes('redirect_url=https%3A%2F%2Fdiffy.run%2Fthanks'));
const published = new Set((process.env.DIFFY_RELEASED_PLATFORMS || 'macos').split(','));
const redirects = read('_redirects');
for (const page of ['downloads/index.html', 'thanks/index.html']) {
Expand All @@ -21,4 +21,37 @@ for (const page of ['downloads/index.html', 'thanks/index.html']) {
}
}
assert(redirects.includes('/download https://github.com/hitSlop/diffy/releases/latest/download/Diffy-macos-aarch64.dmg 302'));
console.log('Pricing, checkout, platform availability, and redirects verified.');
for (const [page, path] of [['index.html', '/'], ['downloads/index.html', '/downloads'], ['thanks/index.html', '/thanks']]) {
const html = read(page);
assert(!html.includes('diffy.hitslop.com'));
assert(html.includes(`<link rel="canonical" href="https://diffy.run${path}"`));
assert(html.includes('content="https://diffy.run/_astro/'));
assert.equal(html.includes('noindex, follow'), path === '/thanks');
}
const schema = JSON.parse(home.match(/<script type="application\/ld\+json">(.*?)<\/script>/s)[1]);
const app = schema['@graph'].find(entity => entity['@type'] === 'SoftwareApplication');
assert.equal(app.url, 'https://diffy.run/');
assert.deepEqual(app.operatingSystem, ['macOS', 'Windows', 'Linux']);
assert(app.sameAs.includes('https://github.com/hitSlop/Diffy'));
assert(!app.offers && !app.aggregateRating);
assert(home.includes('Diffy is a native Git diff viewer built in Rust with GPUI Kit.'));
const robots = read('robots.txt');
assert(robots.includes('User-agent: *\nAllow: /'));
assert(robots.includes('Sitemap: https://diffy.run/sitemap.xml'));
const sitemap = read('sitemap.xml');
assert(sitemap.includes('<loc>https://diffy.run/</loc>'));
assert(sitemap.includes('<loc>https://diffy.run/downloads</loc>'));
assert(!sitemap.includes('thanks') && !sitemap.includes('404'));
for (const file of ['llms.txt', 'llms-full.txt']) {
const markdown = read(file);
assert(markdown.startsWith('# Diffy\n'));
assert(markdown.includes('https://diffy.run/downloads'));
assert(!markdown.includes('diffy.hitslop.com'));
}
const full = read('llms-full.txt');
assert.equal(full.includes('Checkout is not yet available.'), !checkout);
for (const platform of ['macos', 'windows', 'linux']) {
assert.equal(full.includes(`/downloads#${platform}`), published.has(platform));
}
assert(read('404.html').includes('noindex, follow'));
console.log('Pricing, checkout, downloads, canonical URLs, crawler access, schema, sitemap, and AI documentation verified.');
14 changes: 14 additions & 0 deletions site/src/data/product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { platforms, price } from './releases';

export const source = 'https://github.com/hitSlop/Diffy';
export const summary = 'Diffy is a native Git diff viewer built in Rust with GPUI Kit. Review live changes, keep worktrees together, and read GitHub pull requests without checking out a branch.';
export const questions = [
{ question: 'Which platforms does Diffy support?', answer: 'Diffy supports macOS, Windows, and Linux. macOS is in beta; Windows and Linux are early alphas with limited hardware testing. Every platform requires Git.' },
{ question: 'Does Diffy work with coding agents?', answer: 'Diffy discovers local Git projects from Herdr, Codex, and Claude Code. Choose which projects to import, then review their live file changes. Discovery reads local project metadata; it does not run agents or read their conversations.' },
{ question: 'Can Diffy review GitHub pull requests?', answer: 'Yes. Diffy shows read-only diffs for open and draft GitHub.com pull requests using your existing GitHub CLI login. It does not check out a branch. Review comments, approvals, merging, and GitHub Enterprise are not supported.' },
{ question: 'Is Diffy open source, and what does it cost?', answer: `Diffy is open source under the MIT license and can be built for free. The suggested one-time purchase is ${price.beta} during beta (regular price ${price.regular}) and supports all platforms. Public downloads are not payment gated.${import.meta.env.DODO_CHECKOUT_URL?.trim() ? '' : ' Checkout is not yet available.'}` },
];

export function productMarkdown(root: string) {
return `# Diffy\n\n> ${summary}\n\nOfficial website: ${root}\nSource code: ${source}\nPublisher: hitSlop (https://labs.hitslop.com)\nLicense: MIT\n\n## Features\n\n- Live staged, unstaged, and untracked Git diffs.\n- Split or unified views, syntax highlighting, expandable context, and source copying.\n- Worktree tabs that remember the selected file and reading position.\n- Whole-file staging and unstaging, with keyboard navigation and a command palette.\n- Read-only GitHub.com pull request diffs using an existing GitHub CLI login.\n- Optional discovery of local projects from Herdr, Codex, and Claude Code.\n\n## Platforms and downloads\n\nAll platforms require Git. macOS is beta; Windows/Linux are early alphas with limited hardware testing.\n\n${platforms.map(platform => `- ${platform.name} (${platform.stage.toLowerCase()}): ${platform.requirements}. ${platform.available ? `Download and installation: ${new URL(`/downloads#${platform.id}`, root)}` : 'A published binary is not yet available.'}`).join('\n')}\n\n## Questions\n\n${questions.map(({ question, answer }) => `### ${question}\n\n${answer}`).join('\n\n')}\n\n## Official links\n\n- Product and features: ${root}\n- Downloads and installation: ${new URL('/downloads', root)}\n- Source and issue tracker: ${source}\n\nDiffy is a separate product from other tools named diffy. Use the official website and repository above to identify it.\n`;
}
20 changes: 18 additions & 2 deletions site/src/layouts/Page.astro
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
---
import '../styles/site.css';
import socialImage from '../assets/hero.png';
const { title = 'Diffy — a native Git diff viewer, in beta', description = 'Review live Git diffs in a native app built in Rust. macOS beta; Windows and Linux alpha. $14.99 during beta, regular price $24.99. One purchase supports all platforms.' } = Astro.props;
const canonical = new URL(Astro.url.pathname, Astro.site);
import { summary, source } from '../data/product';
import { platforms } from '../data/releases';
const { title = 'Diffy — native Git diff viewer for macOS, Windows & Linux', description = summary, applicationSchema = false, noindex = false } = Astro.props;
const canonical = new URL(Astro.url.pathname.replace(/\/+$/, '') || '/', Astro.site);
const root = new URL('/', Astro.site).href;
const schema = {
'@context': 'https://schema.org',
'@graph': [
{ '@type': 'Organization', '@id': `${root}#publisher`, name: 'hitSlop', url: 'https://labs.hitslop.com' },
{ '@type': 'WebSite', '@id': `${root}#website`, name: 'Diffy', url: root, publisher: { '@id': `${root}#publisher` }, inLanguage: 'en' },
{ '@type': 'WebPage', '@id': `${canonical}#page`, url: canonical.href, name: title, description, isPartOf: { '@id': `${root}#website` }, ...(applicationSchema ? { mainEntity: { '@id': `${root}#app` } } : {}) },
...(applicationSchema ? [{ '@type': 'SoftwareApplication', '@id': `${root}#app`, name: 'Diffy', url: root, description: summary, applicationCategory: 'DeveloperApplication', operatingSystem: platforms.map(platform => platform.name), publisher: { '@id': `${root}#publisher` }, sameAs: [source], license: 'https://opensource.org/license/mit', screenshot: new URL('/diffy.webp', root).href, downloadUrl: new URL('/downloads', root).href, featureList: ['Live Git diffs', 'Split and unified views', 'Git worktree tabs', 'Read-only GitHub pull request diffs', 'Whole-file staging and unstaging', 'Project discovery from Herdr, Codex, and Claude Code'] }] : []),
],
};
const socialImageUrl = new URL(socialImage.src, Astro.site);
const socialImageAlt = 'A tearful developer at a keyboard says, “thank you for changing my life.” Diffy’s colorful chameleon replies, “i literally only show you diffs.”';
---
Expand All @@ -14,7 +26,11 @@ const socialImageAlt = 'A tearful developer at a keyboard says, “thank you for
<meta name="theme-color" content="#000000" />
<title>{title}</title>
<meta name="description" content={description} />
<meta name="robots" content={noindex ? 'noindex, follow' : 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1'} />
<link rel="canonical" href={canonical} />
<link rel="sitemap" type="application/xml" href="/sitemap.xml" />
<link rel="alternate" type="text/plain" href="/llms.txt" title="Diffy product documentation for AI assistants" />
{!noindex && <script type="application/ld+json" set:html={JSON.stringify(schema).replace(/</g, '\\u003c')} />}
<link rel="icon" type="image/png" href="/favicon.png" />
<link rel="preload" href="/fonts/commit-mono.woff2" as="font" type="font/woff2" crossorigin />
<meta property="og:type" content="website" />
Expand Down
2 changes: 1 addition & 1 deletion site/src/pages/404.astro
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
import Page from '../layouts/Page.astro';
---
<Page title="Page not found — Diffy">
<Page noindex title="Page not found — Diffy">
<main id="main" class="download-page frame"><h1>Nothing to diff here.</h1><p class="intro">This page doesn’t exist.</p><a class="button" href="/">Back to Diffy ↗</a></main>
</Page>
14 changes: 10 additions & 4 deletions site/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
import Page from '../layouts/Page.astro';
import Mascot from '../components/Mascot.astro';
import { summary, questions } from '../data/product';
import { platforms, price } from '../data/releases';
import { getImage } from 'astro:assets';
import screenshot from '../../public/diffy.webp';
Expand All @@ -13,16 +14,15 @@ if (checkout) {
if (url.protocol !== 'https:' || url.hostname !== 'checkout.dodopayments.com') {
throw new Error('DODO_CHECKOUT_URL must be an HTTPS Dodo hosted checkout URL.');
}
url.searchParams.set('redirect_url', 'https://diffy.hitslop.com/thanks');
url.searchParams.set('redirect_url', new URL('/thanks', Astro.site).href);
purchaseUrl = url.href;
}
---
<Page>
<Page applicationSchema>
<main id="main">
<section class="hero frame" aria-labelledby="hero-title">
<h1 id="hero-title" class="visually-hidden">Diffy — i literally only show you diffs</h1>
<Mascot />
<p class="intro">A native Git diff viewer, built in Rust.</p>
<h1 id="hero-title" class="intro">Diffy — a native Git diff viewer, built in Rust.</h1>
<p class="agent-line">Keep up with your agents’ changes.</p>
<ul class="hero-badges" aria-label="Highlights">
<li>Freakishly fast</li>
Expand All @@ -45,6 +45,7 @@ if (checkout) {
</figure>
<section class="features frame" aria-labelledby="features-title">
<h2 id="features-title">A few things it does.</h2>
<p class="product-summary">{summary}</p>
<dl>
<div class="feature-row">
<dt>Keep your worktrees together.</dt>
Expand Down Expand Up @@ -87,5 +88,10 @@ if (checkout) {
</div>
</dl>
</section>
<section class="features questions frame" aria-labelledby="questions-title">
<h2 id="questions-title">Before you download.</h2>
{questions.map(({ question, answer }) => <section class="product-question"><h3>{question}</h3><p>{answer}</p></section>)}
<p class="product-summary"><a href="/downloads">Downloads and installation ↗</a></p>
</section>
</main>
</Page>
6 changes: 6 additions & 0 deletions site/src/pages/llms-full.txt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { APIRoute } from 'astro';
import { productMarkdown } from '../data/product';

export const GET: APIRoute = ({ site }) => new Response(productMarkdown(new URL('/', site).href), {
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
});
7 changes: 7 additions & 0 deletions site/src/pages/llms.txt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { APIRoute } from 'astro';
import { summary, source } from '../data/product';

export const GET: APIRoute = ({ site }) => {
const root = new URL('/', site).href;
return new Response(`# Diffy\n\n> ${summary}\n\nDiffy is published by hitSlop. macOS is beta; Windows and Linux are alpha. All platforms require Git.\n\n## Official documentation\n\n- [Product and features](${root}): What Diffy does and its current capabilities.\n- [Downloads and installation](${new URL('/downloads', root)}): Requirements, availability, and installation for each platform.\n- [Complete product documentation](${new URL('/llms-full.txt', root)}): Plain-text features, limitations, pricing, and platform facts.\n- [Source code](${source}): The official MIT-licensed repository.\n`, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
};
5 changes: 5 additions & 0 deletions site/src/pages/sitemap.xml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { APIRoute } from 'astro';

export const GET: APIRoute = ({ site }) => new Response(`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${['/', '/downloads'].map(path => ` <url><loc>${new URL(path, site)}</loc></url>`).join('\n')}\n</urlset>\n`, {
headers: { 'Content-Type': 'application/xml; charset=utf-8' },
});
Loading
Loading