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
- **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
- **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`
+1 -1
View File
@@ -3,7 +3,7 @@ info:
title: SnapOtter API
version: 1.16.0
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
+1 -1
View File
@@ -39,7 +39,7 @@ function generateLlmsTxt(spec: OpenAPISpec): string {
lines.push(`# ${spec.info.title}`);
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("## Docs");
+2 -2
View File
@@ -5,7 +5,7 @@ import pkg from "../package.json";
export default defineConfig({
title: "SnapOtter",
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: "/",
appearance: { initialValue: "light" },
srcDir: ".",
@@ -53,7 +53,7 @@ export default defineConfig({
`,
customTemplateVariables: {
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:
"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`)
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
- 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)
+1 -1
View File
@@ -64,7 +64,7 @@ pnpm dev
## What You Can Do
### Image Processing (51 Tools)
### Image Processing (52 Tools)
| Category | Tools |
|----------|-------|
+2 -2
View File
@@ -4,7 +4,7 @@ layout: home
hero:
name: "SnapOtter"
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:
- theme: brand
text: Get started
@@ -14,7 +14,7 @@ hero:
link: /api/rest
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.
- 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.
+3 -3
View File
@@ -8,7 +8,7 @@ const nunito = Nunito({ subsets: ["latin"], variable: "--font-nunito" });
export const metadata: Metadata = {
title: "SnapOtter | Self-Hosted Image Processing",
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"),
icons: {
icon: [
@@ -24,7 +24,7 @@ export const metadata: Metadata = {
openGraph: {
title: "SnapOtter | Self-Hosted Image Processing",
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",
siteName: "SnapOtter",
type: "website",
@@ -41,7 +41,7 @@ export const metadata: Metadata = {
card: "summary_large_image",
title: "SnapOtter | Self-Hosted Image Processing",
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"],
},
};
+7 -1
View File
@@ -245,6 +245,12 @@ const tools: { name: string; description: string; category: string; icon: Lucide
category: "ai",
icon: Scaling,
},
{
name: "AI Canvas Expand",
description: "Expand canvas with AI-powered fill",
category: "ai",
icon: Maximize2,
},
{
name: "PNG Transparency Fixer",
description: "Fix fake transparent PNGs with AI matting",
@@ -415,7 +421,7 @@ export function BentoGrid() {
<div className="mx-auto max-w-6xl">
<FadeIn>
<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>
<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.
+1 -1
View File
@@ -7,7 +7,7 @@ const freePlan = {
price: "Free",
subtitle: "For everyone. Forever.",
features: [
"All 51 image processing tools",
"All 52 image processing tools",
"Unlimited usage, no hidden caps",
"Full REST API with OpenAPI docs",
"Pipeline automation",
@@ -11,7 +11,7 @@ const phrases = [
"No limits. No hidden caps.",
"Works fully offline.",
"Unlimited batch processing.",
"51 image tools.",
"52 image tools.",
"15 AI models. Your hardware.",
"Lightning fast. Built on Sharp.",
"Air-gapped ready.",
+2 -2
View File
@@ -1607,7 +1607,7 @@ export const en = {
heading: "About",
appName: "SnapOtter",
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:",
linksHeading: "Links",
githubLink: "GitHub Repository",
@@ -1650,7 +1650,7 @@ export const en = {
"No limits. No hidden caps.",
"Works fully offline.",
"Unlimited batch processing.",
"51 image tools.",
"52 image tools.",
"15 AI models. Your hardware.",
"Lightning fast. Built on Sharp.",
"Air-gapped ready.",
+1 -1
View File
@@ -1663,7 +1663,7 @@ export const nl: TranslationKeys = {
heading: "Over",
appName: "SnapOtter",
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:",
linksHeading: "Links",
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("A Self Hosted Image Manipulator")).toBeVisible();
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();
});
@@ -29,7 +29,7 @@ test.describe("Docs Homepage", () => {
test("features section renders all 6 feature cards", async ({ page }) => {
const features = [
"51 Image Tools",
"52 Image Tools",
"Local AI",
"Pipelines",
"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 }) => {
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.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 }) => {
+6 -6
View File
@@ -9,28 +9,28 @@ test.describe("BentoGrid Interactions", () => {
const input = page.getByPlaceholder("Search tools...");
await input.fill("resize");
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 }) => {
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();
});
test("clicking All resets category filter", async ({ page }) => {
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 expect(page.getByText(/Showing 51 of 51 tools/)).toBeVisible();
await page.getByText(/All \(52\)/).click();
await expect(page.getByText(/Showing 52 of 52 tools/)).toBeVisible();
});
test("search with no results shows empty state", async ({ page }) => {
const input = page.getByPlaceholder("Search tools...");
await input.fill("xyznonexistent");
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 }) => {
+1 -1
View File
@@ -47,7 +47,7 @@ function generateLlmsTxt(spec: OpenAPISpec): string {
lines.push(`# ${spec.info.title}`);
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("## Docs");
+9 -9
View File
@@ -25,7 +25,7 @@ function getCountText(): string {
describe("BentoGrid", () => {
it("renders the section heading", () => {
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", () => {
@@ -36,12 +36,12 @@ describe("BentoGrid", () => {
it("shows all tools by default", () => {
render(<BentoGrid />);
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", () => {
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(/AI Tools/)).toBeDefined();
expect(screen.getByText(/Optimization/)).toBeDefined();
@@ -53,7 +53,7 @@ describe("BentoGrid", () => {
fireEvent.change(input, { target: { value: "resize" } });
expect(screen.getByText("Resize")).toBeDefined();
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();
});
@@ -62,7 +62,7 @@ describe("BentoGrid", () => {
const aiButton = screen.getByText(/AI Tools/);
fireEvent.click(aiButton);
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.queryByText("Resize")).toBeNull();
});
@@ -73,7 +73,7 @@ describe("BentoGrid", () => {
fireEvent.change(input, { target: { value: "xyznonexistent" } });
expect(screen.getByText("No tools found. Try a different search.")).toBeDefined();
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", () => {
@@ -89,9 +89,9 @@ describe("BentoGrid", () => {
it("clicking All resets category filter", () => {
render(<BentoGrid />);
fireEvent.click(screen.getByText(/AI Tools/));
expect(getCountText()).toMatch(/Showing 15 of 51 tools/);
fireEvent.click(screen.getByText((_, el) => el?.textContent === "All (51)"));
expect(getCountText()).toMatch(/Showing 51 of 51 tools/);
expect(getCountText()).toMatch(/Showing 16 of 52 tools/);
fireEvent.click(screen.getByText((_, el) => el?.textContent === "All (52)"));
expect(getCountText()).toMatch(/Showing 52 of 52 tools/);
});
it("renders each tool with name and description", () => {
+1 -1
View File
@@ -157,7 +157,7 @@ describe("Pricing", () => {
it("renders free plan features", () => {
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("15 local AI models included")).toBeDefined();
expect(screen.getByText("AGPL-3.0 licensed")).toBeDefined();