From fd030cd695ba9beb86382d935c1c6fb2221549a7 Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Tue, 1 Sep 2026 20:55:04 -0400 Subject: [PATCH 1/5] fix(website): publish sitemap.xml --- .changeset/tidy-sitemap-alias.md | 2 ++ website/package.json | 2 +- website/public/robots.txt | 2 +- website/scripts/publish-sitemap-alias.ts | 11 +++++++++++ website/src/content/docs/docs/help/deployment.md | 2 +- 5 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 .changeset/tidy-sitemap-alias.md create mode 100644 website/scripts/publish-sitemap-alias.ts diff --git a/.changeset/tidy-sitemap-alias.md b/.changeset/tidy-sitemap-alias.md new file mode 100644 index 000000000..a845151cc --- /dev/null +++ b/.changeset/tidy-sitemap-alias.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/website/package.json b/website/package.json index 1e07c5cea..dbb24c4c2 100644 --- a/website/package.json +++ b/website/package.json @@ -4,7 +4,7 @@ "type": "module", "scripts": { "dev": "astro dev", - "build": "astro build", + "build": "astro build && bun scripts/publish-sitemap-alias.ts", "preview": "astro preview", "check": "astro check", "test:browser": "playwright test" diff --git a/website/public/robots.txt b/website/public/robots.txt index 835184f55..c2baffd38 100644 --- a/website/public/robots.txt +++ b/website/public/robots.txt @@ -38,4 +38,4 @@ Allow: / User-agent: meta-externalagent Allow: / -Sitemap: https://hunk.dev/sitemap-index.xml +Sitemap: https://hunk.dev/sitemap.xml diff --git a/website/scripts/publish-sitemap-alias.ts b/website/scripts/publish-sitemap-alias.ts new file mode 100644 index 000000000..095b71e00 --- /dev/null +++ b/website/scripts/publish-sitemap-alias.ts @@ -0,0 +1,11 @@ +import { copyFile } from "node:fs/promises"; +import { join } from "node:path"; + +const outputDirectory = join(import.meta.dirname, "..", "dist"); + +/** Publishes the generated URL set at the conventional sitemap.xml path. */ +async function publishSitemapAlias(): Promise { + await copyFile(join(outputDirectory, "sitemap-0.xml"), join(outputDirectory, "sitemap.xml")); +} + +await publishSitemapAlias(); diff --git a/website/src/content/docs/docs/help/deployment.md b/website/src/content/docs/docs/help/deployment.md index adc62675b..df0d5512e 100644 --- a/website/src/content/docs/docs/help/deployment.md +++ b/website/src/content/docs/docs/help/deployment.md @@ -52,7 +52,7 @@ The domain move is the only production switch. No proxy, rewrite, or `/docs` pat ```bash curl --fail --location https://hunk.dev/ curl --fail --location https://hunk.dev/docs/ -curl --fail https://hunk.dev/sitemap-index.xml +curl --fail https://hunk.dev/sitemap.xml curl --fail https://hunk.dev/pagefind/pagefind.js curl --fail https://hunk.dev/docs/hunk-review-skill.md curl --fail https://hunk.dev/og.png From 8522f65fd82094d78402703f5b8c0f7a73b79091 Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Tue, 1 Sep 2026 21:04:49 -0400 Subject: [PATCH 2/5] feat(website): add VCS landing pages --- website/src/data/vcsLandingPages.ts | 149 ++++++++++++++++++++++++++ website/src/lib/vcsLandingMarkdown.ts | 39 +++++++ website/src/pages/[vcs].astro | 138 ++++++++++++++++++++++++ website/src/pages/[vcs].md.ts | 12 +++ website/src/pages/index.astro | 3 + 5 files changed, 341 insertions(+) create mode 100644 website/src/data/vcsLandingPages.ts create mode 100644 website/src/lib/vcsLandingMarkdown.ts create mode 100644 website/src/pages/[vcs].astro create mode 100644 website/src/pages/[vcs].md.ts diff --git a/website/src/data/vcsLandingPages.ts b/website/src/data/vcsLandingPages.ts new file mode 100644 index 000000000..6e6ca3f90 --- /dev/null +++ b/website/src/data/vcsLandingPages.ts @@ -0,0 +1,149 @@ +export interface VcsLandingPage { + slug: "git" | "jujutsu" | "sapling"; + name: string; + command: string; + title: string; + description: string; + keywords: string[]; + answer: string; + quickStart: string[]; + setup?: { heading: string; lines: string[] }; + workflows: { heading: string; body: string }[]; + limitations: string; + docsHref: string; +} + +/** Search-focused VCS guides rendered as both HTML and Markdown. */ +export const VCS_LANDING_PAGES: VcsLandingPage[] = [ + { + slug: "git", + name: "Git", + command: "git", + title: "Terminal Git diff viewer for reviewing changesets | Hunk", + description: + "Use Hunk as a terminal Git diff viewer for working trees, staged changes, commits, stashes, pager output, and difftool workflows.", + keywords: [ + "git diff viewer", + "terminal git diff", + "git difftool", + "git pager", + "review git changes", + ], + answer: + "Hunk turns Git changes into one review-first terminal UI: a multi-file stream, file navigation, split or stack layouts, expandable context, watch mode, and inline agent notes. Run Hunk directly for a complete changeset, or connect it to Git as a pager or difftool.", + quickStart: ["hunk diff", "hunk diff --staged", "hunk show HEAD~1", "hunk stash show"], + setup: { + heading: "Use Hunk from Git commands", + lines: [ + 'git config --global core.pager "hunk pager"', + 'git config --global difftool.hunk.cmd \'hunk difftool "$LOCAL" "$REMOTE" "$MERGED"\'', + "git config --global difftool.prompt false", + ], + }, + workflows: [ + { + heading: "Review the whole working tree", + body: "`hunk diff` includes tracked changes and untracked files in one ordered review stream. Add `--exclude-untracked` when you only want files Git already knows about.", + }, + { + heading: "Review staged work, commits, and stashes", + body: "Use `hunk diff --staged`, `hunk show `, or `hunk stash show [stash]`. Pathspecs after `--` narrow the input without changing Hunk's review navigation.", + }, + { + heading: "Pager or difftool", + body: "Pager mode lets `git diff` and `git show` open in Hunk. Git difftool integration is pair-oriented because Git invokes it once per file; use `hunk diff` for Hunk's native full-changeset stream.", + }, + ], + limitations: + "Git controls the input in pager mode, so untracked files are not synthesized there. Use `hunk diff` when you want Hunk to assemble the complete working-tree review.", + docsHref: "/docs/workflows/git-pager-and-difftool/", + }, + { + slug: "jujutsu", + name: "Jujutsu", + command: "jj", + title: "Terminal diff viewer for Jujutsu (jj) | Hunk", + description: + "Review Jujutsu working copies and changes in Hunk with native jj revsets, pager integration, multi-file navigation, and agent annotations.", + keywords: [ + "jujutsu diff viewer", + "jj diff tui", + "jj pager", + "terminal diff viewer", + "review jj changes", + ], + answer: + "Hunk detects Jujutsu repositories and passes native jj revsets to its Jujutsu backend. Review a working copy or change in the same multi-file terminal UI Hunk provides for Git, without translating your workflow into Git refs.", + quickStart: ["hunk diff", "hunk diff @-", "hunk show @"], + setup: { + heading: "Use Hunk as the jj pager", + lines: [ + "# jj config edit --user", + "[ui]", + 'pager = ["hunk", "pager"]', + 'diff-formatter = ":git"', + ], + }, + workflows: [ + { + heading: "Review the working copy", + body: "Run `hunk diff` inside a Jujutsu workspace. Hunk detects jj and builds one navigable review stream from the current working-copy changes.", + }, + { + heading: "Use native revsets", + body: "Targets such as `@`, `@-`, and larger revset expressions stay in Jujutsu syntax. Use `hunk diff ` to compare and `hunk show ` to inspect one change.", + }, + { + heading: "Keep Hunk in the jj workflow", + body: "Configure jj to emit Git-format diffs through `hunk pager`, so pager-driven commands can open the same review UI while ordinary text still falls back to a text pager.", + }, + ], + limitations: + "Jujutsu has no Git staging area or stash workflow. Hunk's jj watch mode currently polls rather than observing repository files directly.", + docsHref: "/docs/workflows/jujutsu-and-sapling/", + }, + { + slug: "sapling", + name: "Sapling", + command: "sl", + title: "Terminal diff viewer for Sapling SCM | Hunk", + description: + "Review Sapling working copies and commits in Hunk with native revsets, pager integration, multi-file navigation, and untracked-file support.", + keywords: [ + "sapling diff viewer", + "sl diff tui", + "sapling pager", + "terminal diff viewer", + "review sapling changes", + ], + answer: + "Hunk detects Sapling repositories and accepts Sapling's own revision syntax. It turns working-copy and committed changes into a multi-file terminal review with fast navigation, split or stack layouts, expandable context, and inline agent notes.", + quickStart: ["hunk diff", "hunk diff .^", "hunk show ."], + setup: { + heading: "Use Hunk as the Sapling pager", + lines: ["# sl config -u", "[pager]", "pager = hunk pager"], + }, + workflows: [ + { + heading: "Review tracked and unknown files", + body: "`hunk diff` includes Sapling working-copy changes and unknown files by default. Add `--exclude-untracked` when the review should contain tracked files only.", + }, + { + heading: "Use Sapling revisions", + body: "Pass native targets such as `.^` and `.` directly to `hunk diff` or `hunk show`. Hunk sends them to the detected Sapling backend rather than treating them as Git refs.", + }, + { + heading: "Open pager output in Hunk", + body: "Set Sapling's pager to `hunk pager` to route patch-like command output into the review UI. Non-diff output continues through Hunk's plain-text fallback.", + }, + ], + limitations: + "Sapling has no Git staging-area or stash commands in Hunk. Its watch mode currently polls rather than observing repository files directly.", + docsHref: "/docs/workflows/jujutsu-and-sapling/", + }, +]; + +/** Returns the canonical public URL for one VCS guide. */ +export function vcsLandingUrl(page: VcsLandingPage): string { + return `https://hunk.dev/${page.slug}/`; +} diff --git a/website/src/lib/vcsLandingMarkdown.ts b/website/src/lib/vcsLandingMarkdown.ts new file mode 100644 index 000000000..eded9c1fb --- /dev/null +++ b/website/src/lib/vcsLandingMarkdown.ts @@ -0,0 +1,39 @@ +import type { VcsLandingPage } from "../data/vcsLandingPages"; + +/** Renders the agent-readable counterpart of one VCS landing page. */ +export function renderVcsLandingMarkdown(page: VcsLandingPage): string { + const sections = page.workflows.flatMap((workflow) => [ + `## ${workflow.heading}`, + "", + workflow.body, + "", + ]); + const setup = page.setup + ? [`## ${page.setup.heading}`, "", "```text", ...page.setup.lines, "```", ""] + : []; + + return [ + `# Hunk for ${page.name}`, + "", + `> ${page.description}`, + "", + page.answer, + "", + "## Start reviewing", + "", + "```bash", + ...page.quickStart, + "```", + "", + ...sections, + ...setup, + "## What to know", + "", + page.limitations, + "", + `[Read the complete ${page.name} workflow documentation](https://hunk.dev${page.docsHref})`, + "", + "[Install Hunk](https://hunk.dev/#install)", + "", + ].join("\n"); +} diff --git a/website/src/pages/[vcs].astro b/website/src/pages/[vcs].astro new file mode 100644 index 000000000..6f705a189 --- /dev/null +++ b/website/src/pages/[vcs].astro @@ -0,0 +1,138 @@ +--- +import BrandFooter from "../components/BrandFooter.astro"; +import BrandHeader from "../components/BrandHeader.astro"; +import PageHead from "../components/PageHead.astro"; +import { VCS_LANDING_PAGES, vcsLandingUrl, type VcsLandingPage } from "../data/vcsLandingPages"; +import { withInlineCode, withoutInlineCode } from "../lib/inlineCode"; +import { SITE_ORIGIN } from "../lib/site"; +import "../styles/marketing.css"; +import "../styles/compare.css"; + +/** Builds one canonical workflow guide for every VCS Hunk supports. */ +export function getStaticPaths() { + return VCS_LANDING_PAGES.map((page) => ({ params: { vcs: page.slug }, props: { page } })); +} + +interface Props { + page: VcsLandingPage; +} + +const { page } = Astro.props; +const path = `/${page.slug}/`; +const url = vcsLandingUrl(page); +const siblings = VCS_LANDING_PAGES.filter((entry) => entry.slug !== page.slug); +const schema = [ + { + "@context": "https://schema.org", + "@type": "TechArticle", + headline: page.title, + description: page.description, + url, + mainEntityOfPage: url, + inLanguage: "en", + author: { "@type": "Organization", name: "Modem", url: "https://modem.dev" }, + publisher: { "@type": "Organization", name: "Modem", url: "https://modem.dev" }, + about: { + "@type": "SoftwareApplication", + name: "Hunk", + url: `${SITE_ORIGIN}/`, + applicationCategory: "DeveloperApplication", + operatingSystem: "macOS, Linux, Windows", + }, + abstract: withoutInlineCode(page.answer), + }, + { + "@context": "https://schema.org", + "@type": "BreadcrumbList", + itemListElement: [ + { "@type": "ListItem", position: 1, name: "Hunk", item: `${SITE_ORIGIN}/` }, + { "@type": "ListItem", position: 2, name: `Hunk for ${page.name}`, item: url }, + ], + }, +]; +--- + + + + + + + + +
+
+
+
+ +

# hunk for {page.command}

+

Review {page.name} changes
without leaving the terminal.

+

{page.answer}

+

+ Native {page.command} repository support + Read as Markdown +

+
+ +
+

Start reviewing

+

Run these commands inside a {page.name} workspace:

+
+
terminal
+
{page.quickStart.join("\n")}
+
+
+ + {page.workflows.map((workflow) => ( +
+

{workflow.heading}

+

+

+ ))} + + {page.setup && ( +
+

{page.setup.heading}

+
+
config
+
{page.setup.lines.join("\n")}
+
+
+ )} + +
+

What to know

+

{page.limitations}

+

Read the complete {page.name} workflow documentation →

+
+ +
+

Hunk for other version-control systems

+
+ {siblings.map((entry) => ( + + Hunk for {entry.name} + {entry.description} + + ))} +
+
+ +
+

Try Hunk on your next {page.name} changeset.

+ +
+
+ +
+ + diff --git a/website/src/pages/[vcs].md.ts b/website/src/pages/[vcs].md.ts new file mode 100644 index 000000000..e1908bafd --- /dev/null +++ b/website/src/pages/[vcs].md.ts @@ -0,0 +1,12 @@ +import type { APIRoute, GetStaticPaths } from "astro"; +import { VCS_LANDING_PAGES, type VcsLandingPage } from "../data/vcsLandingPages"; +import { renderVcsLandingMarkdown } from "../lib/vcsLandingMarkdown"; + +/** Serves each VCS workflow guide as Markdown beside its HTML page. */ +export const getStaticPaths: GetStaticPaths = () => + VCS_LANDING_PAGES.map((page) => ({ params: { vcs: page.slug }, props: { page } })); + +export const GET: APIRoute = ({ props }) => + new Response(renderVcsLandingMarkdown(props.page as VcsLandingPage), { + headers: { "Content-Type": "text/markdown; charset=utf-8" }, + }); diff --git a/website/src/pages/index.astro b/website/src/pages/index.astro index 5e7bd061d..fd84a5d56 100644 --- a/website/src/pages/index.astro +++ b/website/src/pages/index.astro @@ -140,6 +140,9 @@ const formattedStars = Install hunk Read the quick start Compare with delta & difftastic + Hunk for Git + Hunk for Jujutsu + Hunk for Sapling From c1ce3a677746b04c4f879d3d7e4e6b660aba3b84 Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Tue, 1 Sep 2026 21:55:44 -0400 Subject: [PATCH 3/5] refactor(website): simplify marketing links --- website/src/components/BrandFooter.astro | 2 -- website/src/pages/index.astro | 5 ----- 2 files changed, 7 deletions(-) diff --git a/website/src/components/BrandFooter.astro b/website/src/components/BrandFooter.astro index 9c431f011..b2714ece9 100644 --- a/website/src/components/BrandFooter.astro +++ b/website/src/components/BrandFooter.astro @@ -15,9 +15,7 @@ const NPM = "https://www.npmjs.com/package/hunkdiff"; Compare GitHub npm - llms.txt Discord - MIT Try hunk on your next changeset. From e564178dbd2898a3380238d84e5591994b8b439b Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Tue, 1 Sep 2026 22:03:48 -0400 Subject: [PATCH 4/5] docs(website): link VCS workflow guides --- website/src/content/docs/docs/workflows/jujutsu-and-sapling.md | 2 +- .../content/docs/docs/workflows/working-trees-and-commits.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/content/docs/docs/workflows/jujutsu-and-sapling.md b/website/src/content/docs/docs/workflows/jujutsu-and-sapling.md index 02dfb19ae..08e6d3741 100644 --- a/website/src/content/docs/docs/workflows/jujutsu-and-sapling.md +++ b/website/src/content/docs/docs/workflows/jujutsu-and-sapling.md @@ -3,7 +3,7 @@ title: Jujutsu and Sapling description: Use native revsets and pager integration in jj and Sapling workspaces. --- -Hunk detects Git, Jujutsu (`jj`), and Sapling (`sl`) repositories. `hunk diff [target]` and `hunk show [target]` pass native revsets to the detected backend. +Hunk detects Git, Jujutsu (`jj`), and Sapling (`sl`) repositories. `hunk diff [target]` and `hunk show [target]` pass native revsets to the detected backend. See the concise [Jujutsu](/jujutsu/) and [Sapling](/sapling/) workflow overviews for a quick introduction. ## Jujutsu diff --git a/website/src/content/docs/docs/workflows/working-trees-and-commits.md b/website/src/content/docs/docs/workflows/working-trees-and-commits.md index 226cd5479..55523f54e 100644 --- a/website/src/content/docs/docs/workflows/working-trees-and-commits.md +++ b/website/src/content/docs/docs/workflows/working-trees-and-commits.md @@ -3,7 +3,7 @@ title: Working trees and commits description: Review repository changes, staged work, refs, and path-limited changesets. --- -Use `diff` for working-copy or comparison input and `show` for one committed change. +Use `diff` for working-copy or comparison input and `show` for one committed change. For a concise overview of Git-specific workflows, see [Hunk for Git](/git/). ## Review the working tree From 3f9dd06f490cb6e5709e134a3c12fdbe47794bb3 Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Tue, 1 Sep 2026 22:37:24 -0400 Subject: [PATCH 5/5] test(website): match simplified homepage links --- website/tests/compare-smoke.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/website/tests/compare-smoke.spec.ts b/website/tests/compare-smoke.spec.ts index 9ec368df0..dc02ae64e 100644 --- a/website/tests/compare-smoke.spec.ts +++ b/website/tests/compare-smoke.spec.ts @@ -129,7 +129,6 @@ test("every comparison is also served as Markdown for agents", async ({ page, re test("the comparison cluster is reachable from the site's own navigation", async ({ page }) => { await page.goto("/"); await expect(page.locator(".brand-footer a[href='/compare/']")).toHaveCount(1); - await expect(page.locator("main a[href='/compare/']")).toHaveCount(1); await page.goto("/compare/hunk-vs-delta/"); await expect(page.getByRole("navigation", { name: "Breadcrumb" })).toContainText("Compare");