fix: update tool count to 52 and add ai-canvas-expand to landing page

ai-canvas-expand was added to constants.ts and route files but never
added to the landing page bento grid, causing all hardcoded counts
to remain at 51. This updates all references across source, docs,
i18n, and tests to reflect the correct count of 52 tools.
This commit is contained in:
SnapOtter
2026-05-16 10:02:19 +08:00
parent 00f408b45e
commit 2c45a3a9e8
19 changed files with 45 additions and 39 deletions
+1 -1
View File
@@ -15,7 +15,7 @@
## Key Features ## Key Features
- **51 image tools** - Resize, crop, compress, convert, watermark, color adjust, beautify screenshots, generate memes, vectorize, create GIFs, find duplicates, generate passport photos, and more. Supports 55+ input formats (including 23 camera RAW formats) and 14 output formats - **52 image tools** - Resize, crop, compress, convert, watermark, color adjust, beautify screenshots, generate memes, vectorize, create GIFs, find duplicates, generate passport photos, and more. Supports 55+ input formats (including 23 camera RAW formats) and 14 output formats
- **Local AI** - Remove backgrounds, upscale images, restore and colorize old photos, erase objects, blur faces, enhance faces, extract text (OCR). All on your hardware - no internet required - **Local AI** - Remove backgrounds, upscale images, restore and colorize old photos, erase objects, blur faces, enhance faces, extract text (OCR). All on your hardware - no internet required
- **Pipelines** - Chain tools into reusable workflows with unlimited steps. Batch process unlimited images at once - **Pipelines** - Chain tools into reusable workflows with unlimited steps. Batch process unlimited images at once
- **REST API** - Every tool available via API with API key auth. Interactive docs at `/api/docs` - **REST API** - Every tool available via API with API key auth. Interactive docs at `/api/docs`
+1 -1
View File
@@ -3,7 +3,7 @@ info:
title: SnapOtter API title: SnapOtter API
version: 1.16.0 version: 1.16.0
description: | description: |
REST API for SnapOtter, a self-hosted image processing platform with 51 tools. REST API for SnapOtter, a self-hosted image processing platform with 52 tools.
## Authentication ## Authentication
+1 -1
View File
@@ -39,7 +39,7 @@ function generateLlmsTxt(spec: OpenAPISpec): string {
lines.push(`# ${spec.info.title}`); lines.push(`# ${spec.info.title}`);
lines.push(""); lines.push("");
lines.push( lines.push(
"> Self-hosted image processing API with 51 tools. Resize, compress, convert, remove backgrounds, upscale, run OCR, and more.", "> Self-hosted image processing API with 52 tools. Resize, compress, convert, remove backgrounds, upscale, run OCR, and more.",
); );
lines.push(""); lines.push("");
lines.push("## Docs"); lines.push("## Docs");
+2 -2
View File
@@ -5,7 +5,7 @@ import pkg from "../package.json";
export default defineConfig({ export default defineConfig({
title: "SnapOtter", title: "SnapOtter",
description: description:
"Documentation for SnapOtter - A Self Hosted Image Manipulator. 51 tools, local AI, pipelines, REST API.", "Documentation for SnapOtter - A Self Hosted Image Manipulator. 52 tools, local AI, pipelines, REST API.",
base: "/", base: "/",
appearance: { initialValue: "light" }, appearance: { initialValue: "light" },
srcDir: ".", srcDir: ".",
@@ -53,7 +53,7 @@ export default defineConfig({
`, `,
customTemplateVariables: { customTemplateVariables: {
description: description:
"SnapOtter is a self-hosted, open-source image processing platform with 51 tools including AI/ML. Runs in a single Docker container with GPU auto-detection.", "SnapOtter is a self-hosted, open-source image processing platform with 52 tools including AI/ML. Runs in a single Docker container with GPU auto-detection.",
details: details:
"Resize, compress, convert, remove backgrounds, upscale, run OCR, and more - without sending images to external services.", "Resize, compress, convert, remove backgrounds, upscale, run OCR, and more - without sending images to external services.",
}, },
+1 -1
View File
@@ -43,7 +43,7 @@ Shared TypeScript types, constants (like `APP_VERSION` and tool definitions), an
### API (`apps/api`) ### API (`apps/api`)
A Fastify v5 server exposing 51 tool routes (35 standard image operations + 15 AI-powered + editor) that handles: A Fastify v5 server exposing 52 tool routes (36 standard image operations + 15 AI-powered + editor) that handles:
- File uploads, temporary workspace management, and persistent file storage - File uploads, temporary workspace management, and persistent file storage
- User file library with version chains (`user_files` table) -- each processed result links back to its source file and records which tool was applied, with auto-generated thumbnails for the Files page - User file library with version chains (`user_files` table) -- each processed result links back to its source file and records which tool was applied, with auto-generated thumbnails for the Files page
- Tool execution (routes each tool request to the image engine or AI bridge) - Tool execution (routes each tool request to the image engine or AI bridge)
+1 -1
View File
@@ -64,7 +64,7 @@ pnpm dev
## What You Can Do ## What You Can Do
### Image Processing (51 Tools) ### Image Processing (52 Tools)
| Category | Tools | | Category | Tools |
|----------|-------| |----------|-------|
+2 -2
View File
@@ -4,7 +4,7 @@ layout: home
hero: hero:
name: "SnapOtter" name: "SnapOtter"
text: "A Self Hosted Image Manipulator" text: "A Self Hosted Image Manipulator"
tagline: 51 tools. Local AI. No cloud. Your images never leave your home. tagline: 52 tools. Local AI. No cloud. Your images never leave your home.
actions: actions:
- theme: brand - theme: brand
text: Get started text: Get started
@@ -14,7 +14,7 @@ hero:
link: /api/rest link: /api/rest
features: features:
- title: 51 Image Tools - title: 52 Image Tools
details: Resize, crop, compress, convert, watermark, color adjust, vectorize, create GIFs, build collages, generate passport photos, find duplicates, and more. details: Resize, crop, compress, convert, watermark, color adjust, vectorize, create GIFs, build collages, generate passport photos, find duplicates, and more.
- title: Local AI - title: Local AI
details: 15 AI-powered tools - remove backgrounds, upscale, enhance images, restore and colorize old photos, erase objects, blur faces, enhance faces, extract text (OCR), fix fake transparency. All on your hardware, no internet required. details: 15 AI-powered tools - remove backgrounds, upscale, enhance images, restore and colorize old photos, erase objects, blur faces, enhance faces, extract text (OCR), fix fake transparency. All on your hardware, no internet required.
+3 -3
View File
@@ -8,7 +8,7 @@ const nunito = Nunito({ subsets: ["latin"], variable: "--font-nunito" });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "SnapOtter | Self-Hosted Image Processing", title: "SnapOtter | Self-Hosted Image Processing",
description: description:
"51 image processing tools with local AI. Runs 100% offline. No data leaves your network. Open source and free forever.", "52 image processing tools with local AI. Runs 100% offline. No data leaves your network. Open source and free forever.",
metadataBase: new URL("https://snapotter.com"), metadataBase: new URL("https://snapotter.com"),
icons: { icons: {
icon: [ icon: [
@@ -24,7 +24,7 @@ export const metadata: Metadata = {
openGraph: { openGraph: {
title: "SnapOtter | Self-Hosted Image Processing", title: "SnapOtter | Self-Hosted Image Processing",
description: description:
"51 image processing tools with local AI. Runs 100% offline. No data leaves your network.", "52 image processing tools with local AI. Runs 100% offline. No data leaves your network.",
url: "https://snapotter.com", url: "https://snapotter.com",
siteName: "SnapOtter", siteName: "SnapOtter",
type: "website", type: "website",
@@ -41,7 +41,7 @@ export const metadata: Metadata = {
card: "summary_large_image", card: "summary_large_image",
title: "SnapOtter | Self-Hosted Image Processing", title: "SnapOtter | Self-Hosted Image Processing",
description: description:
"51 image processing tools with local AI. Runs 100% offline. No data leaves your network.", "52 image processing tools with local AI. Runs 100% offline. No data leaves your network.",
images: ["/og-image.svg"], images: ["/og-image.svg"],
}, },
}; };
+7 -1
View File
@@ -245,6 +245,12 @@ const tools: { name: string; description: string; category: string; icon: Lucide
category: "ai", category: "ai",
icon: Scaling, icon: Scaling,
}, },
{
name: "AI Canvas Expand",
description: "Expand canvas with AI-powered fill",
category: "ai",
icon: Maximize2,
},
{ {
name: "PNG Transparency Fixer", name: "PNG Transparency Fixer",
description: "Fix fake transparent PNGs with AI matting", description: "Fix fake transparent PNGs with AI matting",
@@ -415,7 +421,7 @@ export function BentoGrid() {
<div className="mx-auto max-w-6xl"> <div className="mx-auto max-w-6xl">
<FadeIn> <FadeIn>
<h2 className="font-[family-name:var(--font-nunito)] text-center text-3xl font-bold tracking-tight md:text-4xl"> <h2 className="font-[family-name:var(--font-nunito)] text-center text-3xl font-bold tracking-tight md:text-4xl">
51 tools. Zero cloud dependency. 52 tools. Zero cloud dependency.
</h2> </h2>
<p className="mx-auto mt-4 max-w-xl text-center text-lg text-muted"> <p className="mx-auto mt-4 max-w-xl text-center text-lg text-muted">
Search to find exactly what you need. Every tool runs 100% locally. Search to find exactly what you need. Every tool runs 100% locally.
+1 -1
View File
@@ -7,7 +7,7 @@ const freePlan = {
price: "Free", price: "Free",
subtitle: "For everyone. Forever.", subtitle: "For everyone. Forever.",
features: [ features: [
"All 51 image processing tools", "All 52 image processing tools",
"Unlimited usage, no hidden caps", "Unlimited usage, no hidden caps",
"Full REST API with OpenAPI docs", "Full REST API with OpenAPI docs",
"Pipeline automation", "Pipeline automation",
@@ -11,7 +11,7 @@ const phrases = [
"No limits. No hidden caps.", "No limits. No hidden caps.",
"Works fully offline.", "Works fully offline.",
"Unlimited batch processing.", "Unlimited batch processing.",
"51 image tools.", "52 image tools.",
"15 AI models. Your hardware.", "15 AI models. Your hardware.",
"Lightning fast. Built on Sharp.", "Lightning fast. Built on Sharp.",
"Air-gapped ready.", "Air-gapped ready.",
+2 -2
View File
@@ -1607,7 +1607,7 @@ export const en = {
heading: "About", heading: "About",
appName: "SnapOtter", appName: "SnapOtter",
appDescription: appDescription:
"A self-hosted, privacy-first image processing suite with 51 tools. Resize, compress, convert, watermark, and automate your image workflows without sending data to the cloud.", "A self-hosted, privacy-first image processing suite with 52 tools. Resize, compress, convert, watermark, and automate your image workflows without sending data to the cloud.",
versionLabel: "Version:", versionLabel: "Version:",
linksHeading: "Links", linksHeading: "Links",
githubLink: "GitHub Repository", githubLink: "GitHub Repository",
@@ -1650,7 +1650,7 @@ export const en = {
"No limits. No hidden caps.", "No limits. No hidden caps.",
"Works fully offline.", "Works fully offline.",
"Unlimited batch processing.", "Unlimited batch processing.",
"51 image tools.", "52 image tools.",
"15 AI models. Your hardware.", "15 AI models. Your hardware.",
"Lightning fast. Built on Sharp.", "Lightning fast. Built on Sharp.",
"Air-gapped ready.", "Air-gapped ready.",
+1 -1
View File
@@ -1663,7 +1663,7 @@ export const nl: TranslationKeys = {
heading: "Over", heading: "Over",
appName: "SnapOtter", appName: "SnapOtter",
appDescription: appDescription:
"Een zelf-gehoste, privacy-first beeldverwerkingssuite met 51 tools. Schalen, comprimeren, converteren, watermerken en je beeldworkflows automatiseren zonder data naar de cloud te sturen.", "Een zelf-gehoste, privacy-first beeldverwerkingssuite met 52 tools. Schalen, comprimeren, converteren, watermerken en je beeldworkflows automatiseren zonder data naar de cloud te sturen.",
versionLabel: "Versie:", versionLabel: "Versie:",
linksHeading: "Links", linksHeading: "Links",
githubLink: "GitHub-repository", githubLink: "GitHub-repository",
+2 -2
View File
@@ -13,7 +13,7 @@ test.describe("Docs Homepage", () => {
await expect(page.getByText("SnapOtter").first()).toBeVisible(); await expect(page.getByText("SnapOtter").first()).toBeVisible();
await expect(page.getByText("A Self Hosted Image Manipulator")).toBeVisible(); await expect(page.getByText("A Self Hosted Image Manipulator")).toBeVisible();
await expect( await expect(
page.getByText("51 tools. Local AI. No cloud. Your images never leave your home."), page.getByText("52 tools. Local AI. No cloud. Your images never leave your home."),
).toBeVisible(); ).toBeVisible();
}); });
@@ -29,7 +29,7 @@ test.describe("Docs Homepage", () => {
test("features section renders all 6 feature cards", async ({ page }) => { test("features section renders all 6 feature cards", async ({ page }) => {
const features = [ const features = [
"51 Image Tools", "52 Image Tools",
"Local AI", "Local AI",
"Pipelines", "Pipelines",
"REST API", "REST API",
+2 -2
View File
@@ -57,9 +57,9 @@ test.describe("Landing Homepage", () => {
}); });
test("bento grid renders with search and tool count", async ({ page }) => { test("bento grid renders with search and tool count", async ({ page }) => {
await expect(page.getByText("51 tools. Zero cloud dependency.")).toBeVisible(); await expect(page.getByText("52 tools. Zero cloud dependency.")).toBeVisible();
await expect(page.getByPlaceholder("Search tools...")).toBeVisible(); await expect(page.getByPlaceholder("Search tools...")).toBeVisible();
await expect(page.getByText(/Showing 51 of 51 tools/)).toBeVisible(); await expect(page.getByText(/Showing 52 of 52 tools/)).toBeVisible();
}); });
test("enterprise section renders feature cards", async ({ page }) => { test("enterprise section renders feature cards", async ({ page }) => {
+6 -6
View File
@@ -9,28 +9,28 @@ test.describe("BentoGrid Interactions", () => {
const input = page.getByPlaceholder("Search tools..."); const input = page.getByPlaceholder("Search tools...");
await input.fill("resize"); await input.fill("resize");
await expect(page.getByText("Resize", { exact: true }).first()).toBeVisible(); await expect(page.getByText("Resize", { exact: true }).first()).toBeVisible();
await expect(page.getByText(/Showing \d+ of 51 tools/)).toBeVisible(); await expect(page.getByText(/Showing \d+ of 52 tools/)).toBeVisible();
}); });
test("category pill filters tools", async ({ page }) => { test("category pill filters tools", async ({ page }) => {
await page.getByText(/AI Tools/).click(); await page.getByText(/AI Tools/).click();
await expect(page.getByText(/Showing 15 of 51 tools/)).toBeVisible(); await expect(page.getByText(/Showing 16 of 52 tools/)).toBeVisible();
await expect(page.getByText("Remove Background")).toBeVisible(); await expect(page.getByText("Remove Background")).toBeVisible();
}); });
test("clicking All resets category filter", async ({ page }) => { test("clicking All resets category filter", async ({ page }) => {
await page.getByText(/AI Tools/).click(); await page.getByText(/AI Tools/).click();
await expect(page.getByText(/Showing 15 of 51 tools/)).toBeVisible(); await expect(page.getByText(/Showing 16 of 52 tools/)).toBeVisible();
await page.getByText(/All \(51\)/).click(); await page.getByText(/All \(52\)/).click();
await expect(page.getByText(/Showing 51 of 51 tools/)).toBeVisible(); await expect(page.getByText(/Showing 52 of 52 tools/)).toBeVisible();
}); });
test("search with no results shows empty state", async ({ page }) => { test("search with no results shows empty state", async ({ page }) => {
const input = page.getByPlaceholder("Search tools..."); const input = page.getByPlaceholder("Search tools...");
await input.fill("xyznonexistent"); await input.fill("xyznonexistent");
await expect(page.getByText("No tools found. Try a different search.")).toBeVisible(); await expect(page.getByText("No tools found. Try a different search.")).toBeVisible();
await expect(page.getByText(/Showing 0 of 51 tools/)).toBeVisible(); await expect(page.getByText(/Showing 0 of 52 tools/)).toBeVisible();
}); });
test("combined search and category filter works", async ({ page }) => { test("combined search and category filter works", async ({ page }) => {
+1 -1
View File
@@ -47,7 +47,7 @@ function generateLlmsTxt(spec: OpenAPISpec): string {
lines.push(`# ${spec.info.title}`); lines.push(`# ${spec.info.title}`);
lines.push(""); lines.push("");
lines.push( lines.push(
"> Self-hosted image processing API with 51 tools. Resize, compress, convert, remove backgrounds, upscale, run OCR, and more.", "> Self-hosted image processing API with 52 tools. Resize, compress, convert, remove backgrounds, upscale, run OCR, and more.",
); );
lines.push(""); lines.push("");
lines.push("## Docs"); lines.push("## Docs");
+9 -9
View File
@@ -25,7 +25,7 @@ function getCountText(): string {
describe("BentoGrid", () => { describe("BentoGrid", () => {
it("renders the section heading", () => { it("renders the section heading", () => {
render(<BentoGrid />); render(<BentoGrid />);
expect(screen.getByText("51 tools. Zero cloud dependency.")).toBeDefined(); expect(screen.getByText("52 tools. Zero cloud dependency.")).toBeDefined();
}); });
it("renders the search input", () => { it("renders the search input", () => {
@@ -36,12 +36,12 @@ describe("BentoGrid", () => {
it("shows all tools by default", () => { it("shows all tools by default", () => {
render(<BentoGrid />); render(<BentoGrid />);
const text = getCountText(); const text = getCountText();
expect(text).toMatch(/Showing 51 of 51 tools/); expect(text).toMatch(/Showing 52 of 52 tools/);
}); });
it("renders category filter pills including All", () => { it("renders category filter pills including All", () => {
render(<BentoGrid />); render(<BentoGrid />);
expect(screen.getByText((_, el) => el?.textContent === "All (51)")).toBeDefined(); expect(screen.getByText((_, el) => el?.textContent === "All (52)")).toBeDefined();
expect(screen.getByText(/Essentials/)).toBeDefined(); expect(screen.getByText(/Essentials/)).toBeDefined();
expect(screen.getByText(/AI Tools/)).toBeDefined(); expect(screen.getByText(/AI Tools/)).toBeDefined();
expect(screen.getByText(/Optimization/)).toBeDefined(); expect(screen.getByText(/Optimization/)).toBeDefined();
@@ -53,7 +53,7 @@ describe("BentoGrid", () => {
fireEvent.change(input, { target: { value: "resize" } }); fireEvent.change(input, { target: { value: "resize" } });
expect(screen.getByText("Resize")).toBeDefined(); expect(screen.getByText("Resize")).toBeDefined();
const text = getCountText(); const text = getCountText();
expect(text).toMatch(/Showing \d+ of 51 tools/); expect(text).toMatch(/Showing \d+ of 52 tools/);
expect(screen.queryByText("OCR / Text Extraction")).toBeNull(); expect(screen.queryByText("OCR / Text Extraction")).toBeNull();
}); });
@@ -62,7 +62,7 @@ describe("BentoGrid", () => {
const aiButton = screen.getByText(/AI Tools/); const aiButton = screen.getByText(/AI Tools/);
fireEvent.click(aiButton); fireEvent.click(aiButton);
const text = getCountText(); const text = getCountText();
expect(text).toMatch(/Showing 15 of 51 tools/); expect(text).toMatch(/Showing 16 of 52 tools/);
expect(screen.getByText("Remove Background")).toBeDefined(); expect(screen.getByText("Remove Background")).toBeDefined();
expect(screen.queryByText("Resize")).toBeNull(); expect(screen.queryByText("Resize")).toBeNull();
}); });
@@ -73,7 +73,7 @@ describe("BentoGrid", () => {
fireEvent.change(input, { target: { value: "xyznonexistent" } }); fireEvent.change(input, { target: { value: "xyznonexistent" } });
expect(screen.getByText("No tools found. Try a different search.")).toBeDefined(); expect(screen.getByText("No tools found. Try a different search.")).toBeDefined();
const text = getCountText(); const text = getCountText();
expect(text).toMatch(/Showing 0 of 51 tools/); expect(text).toMatch(/Showing 0 of 52 tools/);
}); });
it("combines search and category filter", () => { it("combines search and category filter", () => {
@@ -89,9 +89,9 @@ describe("BentoGrid", () => {
it("clicking All resets category filter", () => { it("clicking All resets category filter", () => {
render(<BentoGrid />); render(<BentoGrid />);
fireEvent.click(screen.getByText(/AI Tools/)); fireEvent.click(screen.getByText(/AI Tools/));
expect(getCountText()).toMatch(/Showing 15 of 51 tools/); expect(getCountText()).toMatch(/Showing 16 of 52 tools/);
fireEvent.click(screen.getByText((_, el) => el?.textContent === "All (51)")); fireEvent.click(screen.getByText((_, el) => el?.textContent === "All (52)"));
expect(getCountText()).toMatch(/Showing 51 of 51 tools/); expect(getCountText()).toMatch(/Showing 52 of 52 tools/);
}); });
it("renders each tool with name and description", () => { it("renders each tool with name and description", () => {
+1 -1
View File
@@ -157,7 +157,7 @@ describe("Pricing", () => {
it("renders free plan features", () => { it("renders free plan features", () => {
render(<Pricing />); render(<Pricing />);
expect(screen.getByText("All 51 image processing tools")).toBeDefined(); expect(screen.getByText("All 52 image processing tools")).toBeDefined();
expect(screen.getByText("Unlimited usage, no hidden caps")).toBeDefined(); expect(screen.getByText("Unlimited usage, no hidden caps")).toBeDefined();
expect(screen.getByText("15 local AI models included")).toBeDefined(); expect(screen.getByText("15 local AI models included")).toBeDefined();
expect(screen.getByText("AGPL-3.0 licensed")).toBeDefined(); expect(screen.getByText("AGPL-3.0 licensed")).toBeDefined();