diff --git a/apps/web/public/tool-art/pixel-ai.svg b/apps/web/public/tool-art/pixel-ai.svg new file mode 100644 index 00000000..d8cd46c4 --- /dev/null +++ b/apps/web/public/tool-art/pixel-ai.svg @@ -0,0 +1,29 @@ + + Pixel AI illustration + Original geometric artwork showing Pixel as a creative assistant beside a prompt card. + + + + + + + + + + + + + + + + + + + + + + + + Pixel AI + + diff --git a/apps/web/public/tool-screenshots/ai-creative-director.jpg b/apps/web/public/tool-screenshots/ai-creative-director.jpg new file mode 100644 index 00000000..1839260a Binary files /dev/null and b/apps/web/public/tool-screenshots/ai-creative-director.jpg differ diff --git a/apps/web/public/tool-screenshots/ai-image-editor.jpg b/apps/web/public/tool-screenshots/ai-image-editor.jpg new file mode 100644 index 00000000..5dbac509 Binary files /dev/null and b/apps/web/public/tool-screenshots/ai-image-editor.jpg differ diff --git a/apps/web/public/tool-screenshots/ai-image-generator.jpg b/apps/web/public/tool-screenshots/ai-image-generator.jpg new file mode 100644 index 00000000..ef20294d Binary files /dev/null and b/apps/web/public/tool-screenshots/ai-image-generator.jpg differ diff --git a/apps/web/public/tool-screenshots/ai-scriptwriter.jpg b/apps/web/public/tool-screenshots/ai-scriptwriter.jpg new file mode 100644 index 00000000..d571291d Binary files /dev/null and b/apps/web/public/tool-screenshots/ai-scriptwriter.jpg differ diff --git a/apps/web/public/tool-screenshots/ai-spokesperson.jpg b/apps/web/public/tool-screenshots/ai-spokesperson.jpg new file mode 100644 index 00000000..d1970356 Binary files /dev/null and b/apps/web/public/tool-screenshots/ai-spokesperson.jpg differ diff --git a/apps/web/public/tool-screenshots/ai-video-generator.jpg b/apps/web/public/tool-screenshots/ai-video-generator.jpg new file mode 100644 index 00000000..4534ac4f Binary files /dev/null and b/apps/web/public/tool-screenshots/ai-video-generator.jpg differ diff --git a/apps/web/public/tool-screenshots/ai-voice-generator.jpg b/apps/web/public/tool-screenshots/ai-voice-generator.jpg new file mode 100644 index 00000000..863cef2d Binary files /dev/null and b/apps/web/public/tool-screenshots/ai-voice-generator.jpg differ diff --git a/apps/web/public/tool-screenshots/character-chat.jpg b/apps/web/public/tool-screenshots/character-chat.jpg new file mode 100644 index 00000000..6a307559 Binary files /dev/null and b/apps/web/public/tool-screenshots/character-chat.jpg differ diff --git a/apps/web/public/tool-screenshots/media-toolkit.jpg b/apps/web/public/tool-screenshots/media-toolkit.jpg new file mode 100644 index 00000000..3874043f Binary files /dev/null and b/apps/web/public/tool-screenshots/media-toolkit.jpg differ diff --git a/apps/web/public/tool-screenshots/pixel-ai.jpg b/apps/web/public/tool-screenshots/pixel-ai.jpg new file mode 100644 index 00000000..c4e2388f Binary files /dev/null and b/apps/web/public/tool-screenshots/pixel-ai.jpg differ diff --git a/apps/web/public/tool-screenshots/video-editor.jpg b/apps/web/public/tool-screenshots/video-editor.jpg new file mode 100644 index 00000000..1c4d1f1f Binary files /dev/null and b/apps/web/public/tool-screenshots/video-editor.jpg differ diff --git a/apps/web/public/tool-screenshots/voice-casting.jpg b/apps/web/public/tool-screenshots/voice-casting.jpg new file mode 100644 index 00000000..9a723259 Binary files /dev/null and b/apps/web/public/tool-screenshots/voice-casting.jpg differ diff --git a/apps/web/src/app/tools/[slug]/page.tsx b/apps/web/src/app/tools/[slug]/page.tsx index e34742d8..d1559400 100644 --- a/apps/web/src/app/tools/[slug]/page.tsx +++ b/apps/web/src/app/tools/[slug]/page.tsx @@ -1,8 +1,12 @@ import type { Metadata, Route } from "next"; import Image from "next/image"; import Link from "next/link"; -import { notFound } from "next/navigation"; +import { notFound, permanentRedirect } from "next/navigation"; import { getPublicPricingCatalogue } from "@/lib/public-pricing"; +import { + modelsForToolLanding, + toolLandingBySlug, +} from "@/lib/public-tool-landings"; import { publicToolById, publicToolBySlug, @@ -16,6 +20,7 @@ import { ToolsHeader, } from "@/components/marketing/tools/tools-shell"; import { Icon } from "@/components/ui/icon"; +import { ToolLandingPage } from "@/components/marketing/tools/tool-landing-page"; export const dynamic = "force-dynamic"; type PageProps = { params: Promise<{ slug: string }> }; @@ -25,8 +30,45 @@ export function generateStaticParams() { export async function generateMetadata({ params, }: PageProps): Promise { - const tool = publicToolBySlug((await params).slug); + const slug = (await params).slug; + if (slug === "mediakit") { + return { + title: "MediaKit Tools", + alternates: { canonical: "/tools/media-toolkit" }, + robots: { index: false, follow: true }, + }; + } + const tool = publicToolBySlug(slug); if (!tool) return { title: "Tool not found", robots: { index: false } }; + const landing = toolLandingBySlug(slug); + if (landing) { + return { + title: landing.seoTitle, + description: landing.seoDescription, + keywords: [...tool.keywords], + alternates: { canonical: "/tools/" + tool.slug }, + robots: { index: true, follow: true }, + openGraph: { + title: landing.seoTitle, + description: landing.seoDescription, + type: "website", + images: [ + { + url: landing.screenshot.src, + width: 1060, + height: 740, + alt: landing.screenshot.alt, + }, + ], + }, + twitter: { + card: "summary_large_image", + title: landing.seoTitle, + description: landing.seoDescription, + images: [landing.screenshot.src], + }, + }; + } return { title: tool.title + " — " + tool.headline, description: tool.summary.slice(0, 160), @@ -48,8 +90,26 @@ export async function generateMetadata({ }; } export default async function ToolDetailPage({ params }: PageProps) { - const tool = publicToolBySlug((await params).slug); + const slug = (await params).slug; + if (slug === "mediakit") permanentRedirect("/tools/media-toolkit"); + const tool = publicToolBySlug(slug); if (!tool) notFound(); + const landing = toolLandingBySlug(slug); + const origin = process.env.APP_URL ?? "https://creator.aiwamediagroup.com"; + if (landing) { + const catalogue = await getPublicPricingCatalogue().catch(() => ({ + models: [], + generatedAt: new Date(0).toISOString(), + })); + return ( + + ); + } const related = tool.relatedIds .map(publicToolById) .filter((item) => item !== undefined); @@ -69,7 +129,6 @@ export default async function ToolDetailPage({ params }: PageProps) { { 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", diff --git a/apps/web/src/app/tools/operations/[id]/page.tsx b/apps/web/src/app/tools/operations/[id]/page.tsx index b0cf0a9d..68ed350f 100644 --- a/apps/web/src/app/tools/operations/[id]/page.tsx +++ b/apps/web/src/app/tools/operations/[id]/page.tsx @@ -57,7 +57,7 @@ export default async function OperationPage({ params }: Props) { / Tools / - MediaKit + MediaKit / {operation.name} diff --git a/apps/web/src/components/marketing/tools/tool-landing-page.tsx b/apps/web/src/components/marketing/tools/tool-landing-page.tsx new file mode 100644 index 00000000..dd9efdd3 --- /dev/null +++ b/apps/web/src/components/marketing/tools/tool-landing-page.tsx @@ -0,0 +1,581 @@ +import type { Route } from "next"; +import Image from "next/image"; +import Link from "next/link"; +import type { ToolLanding } from "@/lib/public-tool-landings"; +import type { PublicPricingItem } from "@/lib/public-pricing-contract"; +import type { PublicTool } from "@/lib/public-tools"; +import { publicToolById, toolCategories } from "@/lib/public-tools"; +import { Icon } from "@/components/ui/icon"; +import { ToolCard } from "./tool-explorer"; +import { ToolsConversion, ToolsFooter, ToolsHeader } from "./tools-shell"; + +function SectionHeading({ + eyebrow, + title, + description, +}: { + eyebrow: string; + title: string; + description?: string; +}) { + return ( +
+

+ {eyebrow} +

+

+ {title} +

+ {description ? ( +

+ {description} +

+ ) : null} +
+ ); +} + +function ModelPrice({ model }: { model: PublicPricingItem }) { + if (model.tiers[0]) { + return ( +

+ From {model.tiers[0].inputCreditsPerMillion} input /{" "} + {model.tiers[0].outputCreditsPerMillion} output credits per 1M tokens +

+ ); + } + return ( +
+

+ {model.baseCredits} credits +

+

+ {model.baseOmr} + {model.exampleOnly ? " · reference configuration" : ""} +

+
+ ); +} + +function ToolModels({ + landing, + models, +}: { + landing: ToolLanding; + models: readonly PublicPricingItem[]; +}) { + return ( +
+
+
+ + + Compare all pricing + +
+ {models.length ? ( +
+ {models.map((model) => ( +
+
+
+

+ {model.name} +

+

+ {model.provider} · {model.category} +

+
+ + {model.kind} + +
+

+ {model.description} +

+
+ {model.availableIn.slice(0, 3).map((label) => ( + + {label} + + ))} + {model.capabilities.slice(0, 3).map((capability) => ( + + {capability} + + ))} +
+
+ +
+
+ ))} +
+ ) : ( +
+ There are no enabled models or operations with a published customer + price in this category right now. The workspace will show the + current options when you open the tool. Check{" "} + + Pricing + {" "} + for the complete live catalogue. +
+ )} +

+ Public prices are reference information. Duration, resolution, + references, source media and token use can change the final cost. The + authenticated quote shown before submission is authoritative. +

+
+
+ ); +} + +export function ToolLandingPage({ + tool, + landing, + models, + origin, +}: { + tool: PublicTool; + landing: ToolLanding; + models: readonly PublicPricingItem[]; + origin: string; +}) { + const group = toolCategories.find( + (category) => category.id === tool.category, + ); + const relatedTools = tool.relatedIds + .map(publicToolById) + .filter((item) => item !== undefined); + const breadcrumbItems = [ + { name: "Home", item: "/" }, + { name: "All tools", item: "/tools" }, + { name: tool.title, item: `/tools/${tool.slug}` }, + ]; + const schema = [ + { + "@context": "https://schema.org", + "@type": "BreadcrumbList", + itemListElement: breadcrumbItems.map((item, index) => ({ + "@type": "ListItem", + position: index + 1, + name: item.name, + item: new URL(item.item, origin).toString(), + })), + }, + { + "@context": "https://schema.org", + "@type": "WebApplication", + name: tool.title, + applicationCategory: "MultimediaApplication", + operatingSystem: "Web", + url: new URL(`/tools/${tool.slug}`, origin).toString(), + description: landing.seoDescription, + image: new URL(landing.screenshot.src, origin).toString(), + featureList: landing.features.map((feature) => feature.title), + }, + { + "@context": "https://schema.org", + "@type": "FAQPage", + mainEntity: landing.questions.map((item) => ({ + "@type": "Question", + name: item.question, + acceptedAnswer: { "@type": "Answer", text: item.answer }, + })), + }, + ]; + + return ( +
+ +