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/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 `, 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..a93347596 100644 --- a/website/src/pages/index.astro +++ b/website/src/pages/index.astro @@ -138,8 +138,6 @@ const formattedStars =

Try hunk on your next changeset.

Install hunk - Read the quick start - Compare with delta & difftastic
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");