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: 2 additions & 0 deletions .changeset/tidy-sitemap-alias.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
2 changes: 1 addition & 1 deletion website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"build": "astro build && bun scripts/publish-sitemap-alias.ts",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Sitemap alias lacks test coverage

The production build now relies on fixed sitemap output paths and filenames without an automated test covering the generated alias. A regression in the Astro output or copy behavior can fail deployment or publish an incorrect sitemap without being caught by the test suite.

Context Used: guidelines.mdc Cursor rule (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: website/package.json
Line: 7

Comment:
**Sitemap alias lacks test coverage**

The production build now relies on fixed sitemap output paths and filenames without an automated test covering the generated alias. A regression in the Astro output or copy behavior can fail deployment or publish an incorrect sitemap without being caught by the test suite.

**Context Used:** guidelines.mdc Cursor rule ([source](https://github.com/modem-dev/modem/blob/main/.cursor/rules/guidelines.mdc))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

"preview": "astro preview",
"check": "astro check",
"test:browser": "playwright test"
Expand Down
2 changes: 1 addition & 1 deletion website/public/robots.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ Allow: /
User-agent: meta-externalagent
Allow: /

Sitemap: https://hunk.dev/sitemap-index.xml
Sitemap: https://hunk.dev/sitemap.xml
11 changes: 11 additions & 0 deletions website/scripts/publish-sitemap-alias.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
await copyFile(join(outputDirectory, "sitemap-0.xml"), join(outputDirectory, "sitemap.xml"));
}

await publishSitemapAlias();
2 changes: 0 additions & 2 deletions website/src/components/BrandFooter.astro
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ const NPM = "https://www.npmjs.com/package/hunkdiff";
<a href="/compare/">Compare</a>
<a href={REPO}>GitHub</a>
<a href={NPM}>npm</a>
<a href="/llms.txt">llms.txt</a>
<a href={DISCORD}>Discord</a>
<a href={`${REPO}/blob/main/LICENSE`}>MIT</a>
</div>
<a
class="brand-modem"
Expand Down
2 changes: 1 addition & 1 deletion website/src/content/docs/docs/help/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
149 changes: 149 additions & 0 deletions website/src/data/vcsLandingPages.ts
Original file line number Diff line number Diff line change
@@ -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 <ref>`, 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 <target>` to compare and `hunk show <target>` 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}/`;
}
39 changes: 39 additions & 0 deletions website/src/lib/vcsLandingMarkdown.ts
Original file line number Diff line number Diff line change
@@ -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");
}
138 changes: 138 additions & 0 deletions website/src/pages/[vcs].astro
Original file line number Diff line number Diff line change
@@ -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 },
],
},
];
---

<!doctype html>
<html lang="en">
<head>
<PageHead
title={page.title}
description={page.description}
keywords={page.keywords.join(", ")}
path={path}
ogImageAlt={`Hunk terminal diff viewer for ${page.name}`}
schema={schema}
/>
<link rel="alternate" type="text/markdown" href={`${SITE_ORIGIN}/${page.slug}.md`} />
</head>
<body>
<div class="marketing">
<header class="top"><BrandHeader /></header>
<main>
<section class="cmp cmp-intro">
<nav class="crumbs" aria-label="Breadcrumb"><a href="/">Hunk</a> / {page.name}</nav>
<p class="kicker"># hunk for {page.command}</p>
<h1>Review {page.name} changes<br />without leaving the terminal.</h1>
<p class="cmp-answer">{page.answer}</p>
<p class="cmp-meta">
<span>Native {page.command} repository support</span>
<a href={`/${page.slug}.md`}>Read as Markdown</a>
</p>
</section>

<section class="cmp cmp-section" aria-labelledby="quick-start">
<h2 id="quick-start">Start reviewing</h2>
<p class="cmp-prose">Run these commands inside a {page.name} workspace:</p>
<figure class="cmp-code">
<div class="paper-bar"><span class="dot"></span><span class="dot"></span><span class="dot"></span><span class="pt">terminal</span></div>
<pre tabindex="0"><code>{page.quickStart.join("\n")}</code></pre>
</figure>
</section>

{page.workflows.map((workflow) => (
<section class="cmp cmp-section">
<h2>{workflow.heading}</h2>
<p class="cmp-prose" set:html={withInlineCode(workflow.body)} />
</section>
))}

{page.setup && (
<section class="cmp cmp-section">
<h2>{page.setup.heading}</h2>
<figure class="cmp-code">
<div class="paper-bar"><span class="dot"></span><span class="dot"></span><span class="dot"></span><span class="pt">config</span></div>
<pre tabindex="0"><code>{page.setup.lines.join("\n")}</code></pre>
</figure>
</section>
)}

<section class="cmp cmp-section">
<h2>What to know</h2>
<p class="cmp-prose">{page.limitations}</p>
<p class="cmp-prose"><a href={page.docsHref}>Read the complete {page.name} workflow documentation →</a></p>
</section>

<section class="cmp cmp-section" aria-labelledby="other-vcs">
<h2 id="other-vcs">Hunk for other version-control systems</h2>
<div class="cmp-cards">
{siblings.map((entry) => (
<a class="cmp-card" href={`/${entry.slug}/`}>
<b>Hunk for {entry.name}</b>
<span>{entry.description}</span>
</a>
))}
</div>
</section>

<section class="cmp cmp-tail">
<h2>Try Hunk on your next {page.name} changeset.</h2>
<div class="cta">
<a class="ghost" href="/#install">Install hunk</a>
<a class="plain" href={page.docsHref}>Read the workflow docs</a>
</div>
</section>
</main>
<BrandFooter />
</div>
</body>
</html>
Loading
Loading