From 190e487b8516a385e81443fe7e5daf3288bffaf3 Mon Sep 17 00:00:00 2001 From: "Sameer .I. Siddiqui" Date: Mon, 12 Oct 2026 12:56:38 +0530 Subject: [PATCH 1/9] feat(marketing): public creative tool explorer and guided discovery --- apps/web/public/tool-art/audio-generation.svg | 1 + apps/web/public/tool-art/brand-story.svg | 1 + apps/web/public/tool-art/character-chat.svg | 1 + .../web/public/tool-art/creative-director.svg | 1 + apps/web/public/tool-art/explorer-hero.svg | 1 + apps/web/public/tool-art/image-studio.svg | 1 + apps/web/public/tool-art/mediakit.svg | 1 + apps/web/public/tool-art/precision-image.svg | 1 + apps/web/public/tool-art/scriptwriter.svg | 1 + apps/web/public/tool-art/spokesperson.svg | 1 + apps/web/public/tool-art/templates.svg | 1 + apps/web/public/tool-art/transcription.svg | 1 + apps/web/public/tool-art/video-editor.svg | 1 + apps/web/public/tool-art/video-studio.svg | 1 + apps/web/public/tool-art/voice-casting.svg | 1 + apps/web/public/tool-art/voice-studio.svg | 1 + apps/web/src/app/onboarding/page.tsx | 6 +- apps/web/src/app/page.tsx | 1 + apps/web/src/app/sitemap.ts | 10 +- apps/web/src/app/tools/[slug]/page.tsx | 52 ++++ .../src/app/tools/operations/[id]/page.tsx | 29 +++ apps/web/src/app/tools/page.tsx | 64 +++++ apps/web/src/app/tools/start/[id]/page.tsx | 20 ++ .../marketing/tools/tool-explorer.tsx | 107 ++++++++ .../marketing/tools/tools-shell.tsx | 56 +++++ apps/web/src/lib/navigation.test.ts | 8 + apps/web/src/lib/navigation.ts | 6 + apps/web/src/lib/public-tools.test.ts | 38 +++ apps/web/src/lib/public-tools.ts | 236 ++++++++++++++++++ 29 files changed, 644 insertions(+), 5 deletions(-) create mode 100644 apps/web/public/tool-art/audio-generation.svg create mode 100644 apps/web/public/tool-art/brand-story.svg create mode 100644 apps/web/public/tool-art/character-chat.svg create mode 100644 apps/web/public/tool-art/creative-director.svg create mode 100644 apps/web/public/tool-art/explorer-hero.svg create mode 100644 apps/web/public/tool-art/image-studio.svg create mode 100644 apps/web/public/tool-art/mediakit.svg create mode 100644 apps/web/public/tool-art/precision-image.svg create mode 100644 apps/web/public/tool-art/scriptwriter.svg create mode 100644 apps/web/public/tool-art/spokesperson.svg create mode 100644 apps/web/public/tool-art/templates.svg create mode 100644 apps/web/public/tool-art/transcription.svg create mode 100644 apps/web/public/tool-art/video-editor.svg create mode 100644 apps/web/public/tool-art/video-studio.svg create mode 100644 apps/web/public/tool-art/voice-casting.svg create mode 100644 apps/web/public/tool-art/voice-studio.svg create mode 100644 apps/web/src/app/tools/[slug]/page.tsx create mode 100644 apps/web/src/app/tools/operations/[id]/page.tsx create mode 100644 apps/web/src/app/tools/page.tsx create mode 100644 apps/web/src/app/tools/start/[id]/page.tsx create mode 100644 apps/web/src/components/marketing/tools/tool-explorer.tsx create mode 100644 apps/web/src/components/marketing/tools/tools-shell.tsx create mode 100644 apps/web/src/lib/public-tools.test.ts create mode 100644 apps/web/src/lib/public-tools.ts diff --git a/apps/web/public/tool-art/audio-generation.svg b/apps/web/public/tool-art/audio-generation.svg new file mode 100644 index 000000000..d4895af46 --- /dev/null +++ b/apps/web/public/tool-art/audio-generation.svg @@ -0,0 +1 @@ +Audio Generation illustrationOriginal geometric Pencil and Pixel artwork for the Audio Generation tool.Audio Generation \ No newline at end of file diff --git a/apps/web/public/tool-art/brand-story.svg b/apps/web/public/tool-art/brand-story.svg new file mode 100644 index 000000000..391e9b2bf --- /dev/null +++ b/apps/web/public/tool-art/brand-story.svg @@ -0,0 +1 @@ +Brand & Story illustrationOriginal geometric Pencil and Pixel artwork for the Brand & Story tool.Brand & Story \ No newline at end of file diff --git a/apps/web/public/tool-art/character-chat.svg b/apps/web/public/tool-art/character-chat.svg new file mode 100644 index 000000000..74b25c36f --- /dev/null +++ b/apps/web/public/tool-art/character-chat.svg @@ -0,0 +1 @@ +Character Chat illustrationOriginal geometric Pencil and Pixel artwork for the Character Chat tool.Character Chat \ No newline at end of file diff --git a/apps/web/public/tool-art/creative-director.svg b/apps/web/public/tool-art/creative-director.svg new file mode 100644 index 000000000..79224f3c5 --- /dev/null +++ b/apps/web/public/tool-art/creative-director.svg @@ -0,0 +1 @@ +Creative Director illustrationOriginal geometric Pencil and Pixel artwork for the Creative Director tool.Creative Director \ No newline at end of file diff --git a/apps/web/public/tool-art/explorer-hero.svg b/apps/web/public/tool-art/explorer-hero.svg new file mode 100644 index 000000000..ca6f45b4c --- /dev/null +++ b/apps/web/public/tool-art/explorer-hero.svg @@ -0,0 +1 @@ +A connected creative toolboxAn original illustrated collage of image, video, audio and storytelling tools.VOICE & SOUND \ No newline at end of file diff --git a/apps/web/public/tool-art/image-studio.svg b/apps/web/public/tool-art/image-studio.svg new file mode 100644 index 000000000..a7c794fc3 --- /dev/null +++ b/apps/web/public/tool-art/image-studio.svg @@ -0,0 +1 @@ +Image Studio illustrationOriginal geometric Pencil and Pixel artwork for the Image Studio tool.Image Studio \ No newline at end of file diff --git a/apps/web/public/tool-art/mediakit.svg b/apps/web/public/tool-art/mediakit.svg new file mode 100644 index 000000000..f616b9d16 --- /dev/null +++ b/apps/web/public/tool-art/mediakit.svg @@ -0,0 +1 @@ +MediaKit Tools illustrationOriginal geometric Pencil and Pixel artwork for the MediaKit Tools tool.MediaKit Tools \ No newline at end of file diff --git a/apps/web/public/tool-art/precision-image.svg b/apps/web/public/tool-art/precision-image.svg new file mode 100644 index 000000000..ff6c535c3 --- /dev/null +++ b/apps/web/public/tool-art/precision-image.svg @@ -0,0 +1 @@ +Precision Image Studio illustrationOriginal geometric Pencil and Pixel artwork for the Precision Image Studio tool.Precision Image Studio \ No newline at end of file diff --git a/apps/web/public/tool-art/scriptwriter.svg b/apps/web/public/tool-art/scriptwriter.svg new file mode 100644 index 000000000..90c0b3d9f --- /dev/null +++ b/apps/web/public/tool-art/scriptwriter.svg @@ -0,0 +1 @@ +Scriptwriter illustrationOriginal geometric Pencil and Pixel artwork for the Scriptwriter tool.Scriptwriter \ No newline at end of file diff --git a/apps/web/public/tool-art/spokesperson.svg b/apps/web/public/tool-art/spokesperson.svg new file mode 100644 index 000000000..d8787d0a1 --- /dev/null +++ b/apps/web/public/tool-art/spokesperson.svg @@ -0,0 +1 @@ +AI Spokesperson illustrationOriginal geometric Pencil and Pixel artwork for the AI Spokesperson tool.AI Spokesperson \ No newline at end of file diff --git a/apps/web/public/tool-art/templates.svg b/apps/web/public/tool-art/templates.svg new file mode 100644 index 000000000..876dbaca2 --- /dev/null +++ b/apps/web/public/tool-art/templates.svg @@ -0,0 +1 @@ +Generation Templates illustrationOriginal geometric Pencil and Pixel artwork for the Generation Templates tool.Generation Templates \ No newline at end of file diff --git a/apps/web/public/tool-art/transcription.svg b/apps/web/public/tool-art/transcription.svg new file mode 100644 index 000000000..5dff75f47 --- /dev/null +++ b/apps/web/public/tool-art/transcription.svg @@ -0,0 +1 @@ +Speech & Transcription illustrationOriginal geometric Pencil and Pixel artwork for the Speech & Transcription tool.Speech & Transcription \ No newline at end of file diff --git a/apps/web/public/tool-art/video-editor.svg b/apps/web/public/tool-art/video-editor.svg new file mode 100644 index 000000000..4fc64071b --- /dev/null +++ b/apps/web/public/tool-art/video-editor.svg @@ -0,0 +1 @@ +Video Editing Desk illustrationOriginal geometric Pencil and Pixel artwork for the Video Editing Desk tool.Video Editing Desk \ No newline at end of file diff --git a/apps/web/public/tool-art/video-studio.svg b/apps/web/public/tool-art/video-studio.svg new file mode 100644 index 000000000..ffaf03a1a --- /dev/null +++ b/apps/web/public/tool-art/video-studio.svg @@ -0,0 +1 @@ +Video Studio illustrationOriginal geometric Pencil and Pixel artwork for the Video Studio tool.Video Studio \ No newline at end of file diff --git a/apps/web/public/tool-art/voice-casting.svg b/apps/web/public/tool-art/voice-casting.svg new file mode 100644 index 000000000..9d2384903 --- /dev/null +++ b/apps/web/public/tool-art/voice-casting.svg @@ -0,0 +1 @@ +Voice Casting Booth illustrationOriginal geometric Pencil and Pixel artwork for the Voice Casting Booth tool.Voice Casting Booth \ No newline at end of file diff --git a/apps/web/public/tool-art/voice-studio.svg b/apps/web/public/tool-art/voice-studio.svg new file mode 100644 index 000000000..7a8f570be --- /dev/null +++ b/apps/web/public/tool-art/voice-studio.svg @@ -0,0 +1 @@ +Voice Studio illustrationOriginal geometric Pencil and Pixel artwork for the Voice Studio tool.Voice Studio \ No newline at end of file diff --git a/apps/web/src/app/onboarding/page.tsx b/apps/web/src/app/onboarding/page.tsx index 2808381cf..0c46574b6 100644 --- a/apps/web/src/app/onboarding/page.tsx +++ b/apps/web/src/app/onboarding/page.tsx @@ -17,7 +17,7 @@ export default async function OnboardingPage({ searchParams: Promise<{ returnTo?: string }>; }) { const requested = safeInvitationRoute((await searchParams).returnTo); - const learnReturn = requested.startsWith("/learn/start/") + const contentReturn = requested.startsWith("/learn/start/") || requested.startsWith("/tools/start/") ? requested : undefined; const session = await requireRequestSession("/onboarding"); @@ -31,7 +31,7 @@ export default async function OnboardingPage({ }); if (membership) { - redirect(learnReturn ?? `/app/${membership.organization.slug}`); + redirect(contentReturn ?? `/app/${membership.organization.slug}`); } return ( @@ -92,7 +92,7 @@ export default async function OnboardingPage({ - +
{( diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 10f7ba02c..8e8361f24 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -185,6 +185,7 @@ export default function HomePage() { aria-label="Public navigation" > {[ + ["/tools", "Explore Tools"], ["#platform", "Platform"], ["#models", "Models"], ["#workflow", "Workflow"], diff --git a/apps/web/src/app/sitemap.ts b/apps/web/src/app/sitemap.ts index 0aba425d2..fa0990f89 100644 --- a/apps/web/src/app/sitemap.ts +++ b/apps/web/src/app/sitemap.ts @@ -2,17 +2,23 @@ import type { MetadataRoute } from "next"; import { db } from "@aiwa/db"; import { publishedContent } from "@aiwa/learn"; import { learnOrigin } from "@/lib/learn/queries"; +import { publicTools } from "@/lib/public-tools"; +import { getPublicPricingCatalogue } from "@/lib/public-pricing"; export const dynamic = "force-dynamic"; export default async function sitemap(): Promise { const origin = learnOrigin(); - const posts = await db.learnPost.findMany({ + const [posts, pricing] = await Promise.all([db.learnPost.findMany({ where: { publishedAt: { not: null } }, take: 45000, select: { slug: true, published: true, modifiedAt: true }, - }); + }), getPublicPricingCatalogue().catch(() => ({ models: [] }))]); + const operations = pricing.models.filter((item) => item.kind === "tool" && item.category === "tools"); return [ { url: origin }, { url: `${origin}/pricing` }, + { url: `${origin}/tools` }, + ...publicTools.map((tool) => ({ url: `${origin}/tools/${tool.slug}` })), + ...operations.map((item) => ({ url: `${origin}/tools/operations/${encodeURIComponent(item.id)}` })), { url: `${origin}/learn` }, ...posts .filter((p) => !publishedContent(p.published).noindex) diff --git a/apps/web/src/app/tools/[slug]/page.tsx b/apps/web/src/app/tools/[slug]/page.tsx new file mode 100644 index 000000000..74fbacacf --- /dev/null +++ b/apps/web/src/app/tools/[slug]/page.tsx @@ -0,0 +1,52 @@ +import type { Metadata, Route } from "next"; +import Image from "next/image"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { getPublicPricingCatalogue } from "@/lib/public-pricing"; +import { publicToolById, publicToolBySlug, publicTools, toolCategories, toolHref } from "@/lib/public-tools"; +import { ToolCard } from "@/components/marketing/tools/tool-explorer"; +import { ToolsConversion, ToolsFooter, ToolsHeader } from "@/components/marketing/tools/tools-shell"; +import { Icon } from "@/components/ui/icon"; + +type PageProps = { params: Promise<{ slug: string }> }; +export function generateStaticParams() { return publicTools.map((tool) => ({ slug: tool.slug })); } +export async function generateMetadata({ params }: PageProps): Promise { + const tool = publicToolBySlug((await params).slug); + if (!tool) return { title: "Tool not found", robots: { index: false } }; + return { + title: tool.title + " — " + tool.headline, + description: tool.summary.slice(0, 160), + alternates: { canonical: "/tools/" + tool.slug }, + robots: { index: true, follow: true }, + openGraph: { title: tool.title + " · Aiwa Creators", description: tool.summary, type: "website", images: [{ url: tool.cover, width: 700, height: 430, alt: tool.title + " illustration" }] }, + }; +} +export default async function ToolDetailPage({ params }: PageProps) { + const tool = publicToolBySlug((await params).slug); + if (!tool) notFound(); + const related = tool.relatedIds.map(publicToolById).filter((item) => item !== undefined); + const group = toolCategories.find((category) => category.id === tool.category); + const operationCatalogue = tool.id === "mediakit" ? await getPublicPricingCatalogue().catch(() => ({ models: [] })) : null; + const operations = operationCatalogue?.models.filter((item) => item.category === "tools" && item.kind === "tool") ?? []; + const breadcrumbs = [ + { name: "Home", item: "/" }, { name: "All tools", item: "/tools" }, + { name: tool.title, item: "/tools/" + tool.slug }, + ]; + const origin = process.env.APP_URL ?? "https://creator.aiwamediagroup.com"; + const breadcrumbData = { "@context": "https://schema.org", "@type": "BreadcrumbList", itemListElement: breadcrumbs.map((crumb, index) => ({ "@type": "ListItem", position: index + 1, name: crumb.name, item: new URL(crumb.item, origin).toString() })) }; + return ( +
+ +