Add SEO comparison landing pages and footer compare links.

Introduces dynamic marketing comparison routes, competitor data, and Compare sections on the homepage and marketing footer.
This commit is contained in:
yusufipk
2026-06-14 16:36:26 +02:00
parent d301f3d808
commit 51257e004f
14 changed files with 2248 additions and 25 deletions
+58
View File
@@ -0,0 +1,58 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { ComparisonPage } from '@/components/marketing/comparison-page';
import { auth } from '@/lib/auth';
import { comparisonPages, getComparisonPage } from '@/lib/marketing/comparison-pages';
import { buildComparisonJsonLd, buildComparisonMetadata } from '@/lib/marketing/metadata';
interface MarketingSlugPageProps {
params: Promise<{ slug: string }>;
}
export function generateStaticParams() {
return comparisonPages.map((page) => ({ slug: page.slug }));
}
export async function generateMetadata({ params }: MarketingSlugPageProps): Promise<Metadata> {
const { slug } = await params;
const page = getComparisonPage(slug);
if (!page) {
return {};
}
return buildComparisonMetadata({
title: page.title,
description: page.metaDescription,
path: `/${page.slug}`,
keywords: page.keywords,
});
}
export default async function MarketingSlugPage({ params }: MarketingSlugPageProps) {
const { slug } = await params;
const page = getComparisonPage(slug);
if (!page) {
notFound();
}
const session = await auth();
const structuredData = buildComparisonJsonLd({
title: page.title,
description: page.metaDescription,
path: `/${page.slug}`,
faq: page.faq,
});
const safeStructuredData = JSON.stringify(structuredData).replace(/</g, '\\u003c');
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: safeStructuredData }}
/>
<ComparisonPage page={page} isLoggedIn={Boolean(session?.user)} />
</>
);
}
+25
View File
@@ -1,4 +1,5 @@
import type { MetadataRoute } from 'next';
import { comparisonPages } from '@/lib/marketing/comparison-pages';
import { seoConfig } from '@/lib/seo';
export default function sitemap(): MetadataRoute.Sitemap {
@@ -11,6 +12,12 @@ export default function sitemap(): MetadataRoute.Sitemap {
changeFrequency: 'daily',
priority: 1,
},
...comparisonPages.map((page) => ({
url: `${seoConfig.url}/${page.slug}`,
lastModified,
changeFrequency: 'monthly' as const,
priority: 0.8,
})),
{
url: `${seoConfig.url}/login`,
lastModified,
@@ -23,5 +30,23 @@ export default function sitemap(): MetadataRoute.Sitemap {
changeFrequency: 'monthly',
priority: 0.7,
},
{
url: `${seoConfig.url}/terms`,
lastModified,
changeFrequency: 'yearly',
priority: 0.3,
},
{
url: `${seoConfig.url}/privacy`,
lastModified,
changeFrequency: 'yearly',
priority: 0.3,
},
{
url: `${seoConfig.url}/refund`,
lastModified,
changeFrequency: 'yearly',
priority: 0.3,
},
];
}
+32 -25
View File
@@ -2,6 +2,7 @@
import Image from 'next/image';
import Link from 'next/link';
import { MarketingCompareLinks } from '@/components/marketing/marketing-compare-links';
import { useEffect, useRef } from 'react';
import { gsap } from 'gsap';
import {
@@ -880,35 +881,41 @@ export function LandingPage({ isLoggedIn }: LandingPageProps) {
</main>
<footer className="border-t border-border bg-background px-4 py-8 sm:px-6 lg:px-8">
<div className="mx-auto flex max-w-[1200px] flex-col items-center justify-between gap-4 sm:flex-row">
<div className="flex items-center gap-2">
<Video className="h-4 w-4 text-primary" />
<div className="mx-auto grid max-w-[1200px] gap-8 sm:grid-cols-2 lg:grid-cols-3">
<div className="flex items-start gap-2">
<Video className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
<span className="font-mono text-xs text-muted-foreground">
© 2026 IPEK TECH LLC. All rights reserved.
</span>
</div>
<div className="flex flex-wrap justify-center gap-4">
<a
href="mailto:[email protected]"
className="text-xs text-muted-foreground hover:text-foreground"
>
info@open-frame.net
</a>
<a
href="https://github.com/yusufipk/OpenFrame"
className="text-xs text-muted-foreground hover:text-foreground"
>
GitHub
</a>
<Link href="/terms" className="text-xs text-muted-foreground hover:text-foreground">
Terms
</Link>
<Link href="/privacy" className="text-xs text-muted-foreground hover:text-foreground">
Privacy
</Link>
<Link href="/refund" className="text-xs text-muted-foreground hover:text-foreground">
Refund Policy
</Link>
<MarketingCompareLinks />
<div className="flex flex-col gap-2">
<span className="font-mono text-[10px] uppercase tracking-wider text-muted-foreground">
Legal
</span>
<div className="flex flex-col gap-1.5">
<a
href="mailto:[email protected]"
className="text-xs text-muted-foreground hover:text-foreground"
>
info@open-frame.net
</a>
<a
href="https://github.com/yusufipk/OpenFrame"
className="text-xs text-muted-foreground hover:text-foreground"
>
GitHub
</a>
<Link href="/terms" className="text-xs text-muted-foreground hover:text-foreground">
Terms
</Link>
<Link href="/privacy" className="text-xs text-muted-foreground hover:text-foreground">
Privacy
</Link>
<Link href="/refund" className="text-xs text-muted-foreground hover:text-foreground">
Refund Policy
</Link>
</div>
</div>
</div>
</footer>
+224
View File
@@ -0,0 +1,224 @@
import Link from 'next/link';
import { ArrowRight, Github, MoveRight } from 'lucide-react';
import { FeatureComparisonTable } from '@/components/marketing/feature-comparison-table';
import { MarketingFooter } from '@/components/marketing/marketing-footer';
import { MarketingHeader } from '@/components/marketing/marketing-header';
import { PricingComparison } from '@/components/marketing/pricing-comparison';
import { ProductVisual } from '@/components/marketing/product-visual';
import type { ComparisonPageDefinition } from '@/lib/marketing/comparison-types';
import { getCompetitorName, comparisonPageMap } from '@/lib/marketing/comparison-pages';
import { seoConfig } from '@/lib/seo';
interface ComparisonPageProps {
page: ComparisonPageDefinition;
isLoggedIn: boolean;
}
export function ComparisonPage({ page, isLoggedIn }: ComparisonPageProps) {
const hostedCtaHref = isLoggedIn ? '/dashboard' : '/register';
const competitorName = getCompetitorName(page) ?? 'Alternatives';
const relatedSlugs = page.relatedSlugs.filter((slug) => slug in comparisonPageMap);
return (
<div className="min-h-screen bg-background text-foreground font-sans">
<MarketingHeader isLoggedIn={isLoggedIn} />
<main>
<section className="border-b border-border px-4 py-16 sm:px-6 lg:px-8 lg:py-24">
<div className="mx-auto grid w-full max-w-[1200px] gap-12 lg:grid-cols-[1.1fr_0.9fr] lg:items-center">
<div>
<p className="text-xs font-medium uppercase tracking-[0.16em] text-primary">
{page.eyebrow}
</p>
<h1 className="mt-4 text-4xl font-semibold tracking-[-0.03em] md:text-6xl">
{page.headline}
</h1>
<p className="mt-6 max-w-2xl text-base text-muted-foreground md:text-lg">
{page.subheadline}
</p>
<p className="mt-4 max-w-2xl border border-primary/30 bg-primary/5 px-4 py-3 text-sm text-foreground/90 md:text-base">
<span className="font-medium text-primary">No per-member or guest fees.</span> One
$10/month hosted plan covers your whole team and every client reviewer link.
</p>
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
<Link
href={hostedCtaHref}
className="group relative isolate inline-flex h-12 items-center justify-center overflow-hidden border border-primary bg-primary px-8 text-sm font-medium text-primary-foreground transition-transform duration-300 hover:scale-[1.02]"
>
Start free trial
<MoveRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1" />
</Link>
<a
href={seoConfig.githubUrl}
target="_blank"
rel="noreferrer"
className="inline-flex h-12 items-center justify-center border border-border bg-background px-8 text-sm font-medium text-foreground transition-colors hover:bg-card"
>
<Github className="mr-2 h-4 w-4" />
View GitHub
</a>
</div>
</div>
<ProductVisual variant={page.visualVariant} />
</div>
</section>
<section className="border-b border-border px-4 py-16 sm:px-6 lg:px-8 lg:py-20">
<div className="mx-auto w-full max-w-[1200px]">
<h2 className="text-3xl font-semibold tracking-[-0.02em] md:text-4xl">
{page.solutionTitle}
</h2>
<p className="mt-4 max-w-3xl text-base text-muted-foreground md:text-lg">
{page.solutionNarrative}
</p>
</div>
</section>
<section className="border-b border-border bg-background px-4 py-16 sm:px-6 lg:px-8 lg:py-20">
<div className="mx-auto grid w-full max-w-[1200px] gap-8 lg:grid-cols-2">
<div className="border border-primary/30 bg-primary/5 p-6">
<h3 className="text-xl font-semibold">Where OpenFrame fits best</h3>
<ul className="mt-4 space-y-3 text-sm text-foreground/90 md:text-base">
{page.bestForOpenFrame.map((item) => (
<li key={item}>- {item}</li>
))}
</ul>
</div>
<div className="border border-border bg-card p-6">
<h3 className="text-xl font-semibold">Where {competitorName} may still fit</h3>
<ul className="mt-4 space-y-3 text-sm text-muted-foreground md:text-base">
{page.bestForCompetitor.map((item) => (
<li key={item}>- {item}</li>
))}
</ul>
</div>
</div>
</section>
<section className="border-b border-border bg-card/10 px-4 py-16 sm:px-6 lg:px-8 lg:py-20">
<div className="mx-auto grid w-full max-w-[1200px] gap-8 lg:grid-cols-2">
<div>
<h3 className="text-2xl font-semibold">OpenFrame advantages</h3>
<ul className="mt-4 space-y-3 text-sm md:text-base">
{page.openframeWins.map((item) => (
<li key={item} className="border border-border bg-background p-4">
{item}
</li>
))}
</ul>
</div>
<div>
<h3 className="text-2xl font-semibold">{competitorName} advantages</h3>
<ul className="mt-4 space-y-3 text-sm text-muted-foreground md:text-base">
{page.competitorWins.map((item) => (
<li key={item} className="border border-border bg-background p-4">
{item}
</li>
))}
</ul>
</div>
</div>
</section>
<section className="border-b border-border px-4 py-16 sm:px-6 lg:px-8 lg:py-20">
<div className="mx-auto w-full max-w-[1200px] space-y-8">
<div>
<h2 className="text-3xl font-semibold tracking-[-0.02em] md:text-4xl">
Feature comparison
</h2>
<p className="mt-3 max-w-3xl text-sm text-muted-foreground md:text-base">
Honest tradeoffs based on official product, pricing, and help documentation. Verify
current plans before buying.
</p>
</div>
<FeatureComparisonTable rows={page.featureRows} competitorName={competitorName} />
</div>
</section>
<section className="border-b border-border bg-[#0a0a0a] px-4 py-16 sm:px-6 lg:px-8 lg:py-20">
<div className="mx-auto w-full max-w-[1200px] space-y-8">
<div>
<h2 className="text-3xl font-semibold tracking-[-0.02em] md:text-4xl">
Pricing comparison
</h2>
<p className="mt-3 max-w-3xl text-sm text-muted-foreground md:text-base">
OpenFrame is $10/month flat with a 7-day free trial. You do not pay per team member,
collaborator, or guest reviewer. Self-hosting is free with Docker.
</p>
</div>
<PricingComparison
rows={page.pricingRows}
footnote={page.pricingFootnote}
competitorName={competitorName}
/>
</div>
</section>
<section className="border-b border-border px-4 py-16 sm:px-6 lg:px-8 lg:py-20">
<div className="mx-auto w-full max-w-[1200px]">
<h2 className="text-3xl font-semibold tracking-[-0.02em] md:text-4xl">FAQ</h2>
<div className="mt-8 space-y-4">
{page.faq.map((item) => (
<div key={item.question} className="border border-border bg-card p-6">
<h3 className="text-lg font-semibold">{item.question}</h3>
<p className="mt-2 text-sm text-muted-foreground md:text-base">{item.answer}</p>
</div>
))}
</div>
</div>
</section>
{relatedSlugs.length > 0 ? (
<section className="border-b border-border bg-card/10 px-4 py-12 sm:px-6 lg:px-8">
<div className="mx-auto w-full max-w-[1200px]">
<h2 className="text-xl font-semibold">Related comparisons</h2>
<div className="mt-4 flex flex-wrap gap-3">
{relatedSlugs.map((slug) => (
<Link
key={slug}
href={`/${slug}`}
className="inline-flex items-center gap-2 border border-border bg-background px-4 py-2 text-sm text-muted-foreground transition-colors hover:text-foreground"
>
{slug.replaceAll('-', ' ')}
<ArrowRight className="h-3.5 w-3.5" />
</Link>
))}
</div>
</div>
</section>
) : null}
<section className="px-4 py-16 sm:px-6 lg:px-8">
<div className="mx-auto flex w-full max-w-[1200px] flex-col items-center justify-between gap-4 border border-border bg-card p-8 text-center md:flex-row md:text-left">
<div>
<h2 className="text-3xl font-semibold tracking-[-0.02em] md:text-4xl">
Start your free trial
</h2>
<p className="mt-2 text-sm text-muted-foreground">
Your first client review link takes minutes.
</p>
</div>
<div className="flex flex-col gap-3 sm:flex-row">
<Link
href={hostedCtaHref}
className="inline-flex h-12 items-center justify-center border border-primary bg-primary px-8 text-sm font-medium text-primary-foreground"
>
Start free trial
</Link>
<a
href={seoConfig.githubUrl}
target="_blank"
rel="noreferrer"
className="inline-flex h-12 items-center justify-center border border-border bg-background px-8 text-sm font-medium text-foreground"
>
View GitHub
</a>
</div>
</div>
</section>
</main>
<MarketingFooter />
</div>
);
}
@@ -0,0 +1,68 @@
import { Check, Minus, X } from 'lucide-react';
import type { FeatureRow } from '@/lib/marketing/comparison-types';
function renderStatus(value: FeatureRow['openframe']) {
if (value === 'yes') {
return (
<span className="inline-flex items-center gap-1 text-emerald-400">
<Check className="h-4 w-4" />
Yes
</span>
);
}
if (value === 'no') {
return (
<span className="inline-flex items-center gap-1 text-red-400/90">
<X className="h-4 w-4" />
No
</span>
);
}
if (value === 'partial') {
return (
<span className="inline-flex items-center gap-1 text-amber-300">
<Minus className="h-4 w-4" />
Partial
</span>
);
}
return <span className="text-foreground/90">{value}</span>;
}
interface FeatureComparisonTableProps {
rows: FeatureRow[];
competitorName: string;
}
export function FeatureComparisonTable({ rows, competitorName }: FeatureComparisonTableProps) {
return (
<div className="overflow-x-auto border border-border bg-card">
<table className="min-w-full text-left text-sm">
<thead className="border-b border-border bg-background/60">
<tr>
<th className="px-4 py-3 font-medium text-muted-foreground">Feature</th>
<th className="px-4 py-3 font-medium text-primary">OpenFrame</th>
<th className="px-4 py-3 font-medium text-muted-foreground">{competitorName}</th>
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.label} className="border-b border-border/60 last:border-b-0">
<td className="px-4 py-3 align-top">
<div className="font-medium text-foreground">{row.label}</div>
{row.note ? (
<div className="mt-1 text-xs text-muted-foreground">{row.note}</div>
) : null}
</td>
<td className="px-4 py-3 align-top">{renderStatus(row.openframe)}</td>
<td className="px-4 py-3 align-top">{renderStatus(row.competitor)}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
@@ -0,0 +1,23 @@
import Link from 'next/link';
import { compareFooterLinks } from '@/lib/marketing/comparison-pages';
export function MarketingCompareLinks() {
return (
<div className="flex flex-col gap-2">
<span className="font-mono text-[10px] uppercase tracking-wider text-muted-foreground">
Compare
</span>
<nav className="flex flex-col gap-1.5">
{compareFooterLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="text-xs text-muted-foreground hover:text-foreground"
>
{link.label}
</Link>
))}
</nav>
</div>
);
}
+48
View File
@@ -0,0 +1,48 @@
import Link from 'next/link';
import { Video } from 'lucide-react';
import { seoConfig } from '@/lib/seo';
import { MarketingCompareLinks } from '@/components/marketing/marketing-compare-links';
export function MarketingFooter() {
return (
<footer className="border-t border-border bg-background px-4 py-8 sm:px-6 lg:px-8">
<div className="mx-auto grid max-w-[1200px] gap-8 sm:grid-cols-2 lg:grid-cols-3">
<div className="flex items-start gap-2">
<Video className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
<span className="font-mono text-xs text-muted-foreground">
© 2026 IPEK TECH LLC. All rights reserved.
</span>
</div>
<MarketingCompareLinks />
<div className="flex flex-col gap-2">
<span className="font-mono text-[10px] uppercase tracking-wider text-muted-foreground">
Legal
</span>
<div className="flex flex-col gap-1.5">
<a
href="mailto:[email protected]"
className="text-xs text-muted-foreground hover:text-foreground"
>
info@open-frame.net
</a>
<a
href={seoConfig.githubUrl}
className="text-xs text-muted-foreground hover:text-foreground"
>
GitHub
</a>
<Link href="/terms" className="text-xs text-muted-foreground hover:text-foreground">
Terms
</Link>
<Link href="/privacy" className="text-xs text-muted-foreground hover:text-foreground">
Privacy
</Link>
<Link href="/refund" className="text-xs text-muted-foreground hover:text-foreground">
Refund Policy
</Link>
</div>
</div>
</div>
</footer>
);
}
+77
View File
@@ -0,0 +1,77 @@
import Link from 'next/link';
import { MoveRight, Video } from 'lucide-react';
import { seoConfig } from '@/lib/seo';
const controlButtonClass =
'group relative isolate inline-flex h-8 items-center justify-center overflow-hidden border border-border bg-background px-2.5 text-[11px] font-medium text-foreground transition-colors duration-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring sm:h-9 sm:px-4 sm:text-xs';
interface MarketingHeaderProps {
isLoggedIn: boolean;
}
export function MarketingHeader({ isLoggedIn }: MarketingHeaderProps) {
const hostedCtaHref = isLoggedIn ? '/dashboard' : '/register';
return (
<header className="border-b border-border bg-background/90 backdrop-blur-md">
<div className="mx-auto flex h-14 w-full max-w-[1200px] items-center justify-between px-4 sm:h-16 sm:px-6 lg:px-10">
<Link
href="/"
className="group relative isolate inline-flex items-center gap-2 overflow-hidden border border-border bg-background px-3 py-2"
>
<span className="pointer-events-none absolute inset-0 -translate-x-[101%] bg-primary/10 transition-transform duration-300 group-hover:translate-x-0" />
<Video className="relative z-10 h-4 w-4 text-primary" />
<span className="relative z-10 text-xs font-semibold tracking-[0.12em]">OPENFRAME</span>
</Link>
<nav className="hidden items-center gap-6 text-[11px] font-medium uppercase tracking-[0.14em] md:flex">
<Link
className="text-muted-foreground transition-colors hover:text-foreground"
href="/#features"
>
Features
</Link>
<Link
className="text-muted-foreground transition-colors hover:text-foreground"
href="/#pricing"
>
Pricing
</Link>
<a
className="text-muted-foreground transition-colors hover:text-foreground"
href={seoConfig.githubUrl}
target="_blank"
rel="noreferrer"
>
GitHub
</a>
</nav>
<div className="flex items-center gap-2">
{isLoggedIn ? (
<Link href="/dashboard" className={controlButtonClass}>
<span className="pointer-events-none absolute inset-0 -translate-x-[101%] bg-primary/10 transition-transform duration-300 group-hover:translate-x-0" />
<span className="relative z-10 inline-flex items-center gap-2">
Dashboard
<MoveRight className="h-3.5 w-3.5" />
</span>
</Link>
) : (
<>
<Link
href="/login"
className="mr-4 hidden text-xs font-medium text-muted-foreground hover:text-foreground sm:block"
>
Log in
</Link>
<Link href={hostedCtaHref} className={controlButtonClass}>
<span className="pointer-events-none absolute inset-0 -translate-x-[101%] bg-primary/10 transition-transform duration-300 group-hover:translate-x-0" />
<span className="relative z-10">Start free trial</span>
</Link>
</>
)}
</div>
</div>
</header>
);
}
@@ -0,0 +1,34 @@
import type { PricingRow } from '@/lib/marketing/comparison-types';
interface PricingComparisonProps {
rows: PricingRow[];
footnote?: string;
competitorName: string;
}
export function PricingComparison({ rows, footnote, competitorName }: PricingComparisonProps) {
return (
<div className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
{rows.map((row) => (
<div key={row.label} className="border border-border bg-card p-5">
<p className="text-xs uppercase tracking-[0.14em] text-muted-foreground">{row.label}</p>
<div className="mt-4 grid gap-3 sm:grid-cols-2">
<div>
<p className="text-[10px] uppercase tracking-[0.12em] text-primary">OpenFrame</p>
<p className="mt-1 text-sm text-foreground">{row.openframe}</p>
</div>
<div>
<p className="text-[10px] uppercase tracking-[0.12em] text-muted-foreground">
{competitorName}
</p>
<p className="mt-1 text-sm text-foreground/90">{row.competitor}</p>
</div>
</div>
</div>
))}
</div>
{footnote ? <p className="text-xs text-muted-foreground">{footnote}</p> : null}
</div>
);
}
+175
View File
@@ -0,0 +1,175 @@
import Image from 'next/image';
import { CheckCircle, Link as LinkIcon, MessageSquare, Mic, PenTool } from 'lucide-react';
import type { VisualVariant } from '@/lib/marketing/comparison-types';
interface ProductVisualProps {
variant: VisualVariant;
}
export function ProductVisual({ variant }: ProductVisualProps) {
if (variant === 'landing-compare') {
return (
<div className="relative aspect-[16/10] overflow-hidden border border-border bg-card">
<Image
src="/landing/compare-v2.webp"
alt="OpenFrame side-by-side version compare"
fill
className="object-cover object-top"
sizes="(max-width: 768px) 100vw, 540px"
priority
/>
</div>
);
}
if (variant === 'landing-dashboard') {
return (
<div className="relative aspect-[16/10] overflow-hidden border border-border bg-card">
<Image
src="/landing/deep-dive-dashboard-2.webp"
alt="OpenFrame video review dashboard"
fill
className="object-cover object-top"
sizes="(max-width: 768px) 100vw, 540px"
priority
/>
</div>
);
}
if (variant === 'version-compare') {
return (
<div className="border border-border bg-card p-4">
<div className="grid gap-3 md:grid-cols-2">
{['V2', 'V3'].map((version) => (
<div key={version} className="border border-border/60 bg-background p-3">
<div className="mb-3 flex items-center justify-between text-[10px] uppercase tracking-[0.14em] text-muted-foreground">
<span>{version}</span>
<span className="text-primary">Compare mode</span>
</div>
<div className="aspect-video bg-gradient-to-br from-zinc-900 via-zinc-800 to-zinc-900" />
<div className="mt-3 space-y-2">
<div className="h-1.5 w-full rounded-full bg-border">
<div
className={`h-1.5 rounded-full bg-primary ${version === 'V2' ? 'w-1/3' : 'w-2/3'}`}
/>
</div>
<p className="font-mono text-[10px] text-muted-foreground">
{version === 'V2' ? '00:01:12 color too cool' : '00:01:12 warmed up +2'}
</p>
</div>
</div>
))}
</div>
</div>
);
}
if (variant === 'approval-workflow') {
return (
<div className="border border-border bg-card p-5">
<div className="flex items-center justify-between border-b border-border pb-4">
<div>
<p className="text-xs uppercase tracking-[0.14em] text-muted-foreground">
Client Cut V3
</p>
<p className="mt-1 text-lg font-semibold">Approval requests</p>
</div>
<span className="inline-flex items-center gap-2 border border-emerald-500/30 bg-emerald-500/10 px-3 py-1 text-xs text-emerald-300">
<CheckCircle className="h-3.5 w-3.5" />2 of 3 approved
</span>
</div>
<div className="mt-4 space-y-3">
{[
{ name: 'Client Producer', status: 'Approved', tone: 'text-emerald-300' },
{ name: 'Brand Manager', status: 'Approved', tone: 'text-emerald-300' },
{ name: 'Legal Review', status: 'Pending', tone: 'text-amber-300' },
].map((item) => (
<div
key={item.name}
className="flex items-center justify-between border border-border/60 bg-background px-4 py-3"
>
<span className="text-sm">{item.name}</span>
<span className={`text-xs uppercase tracking-[0.12em] ${item.tone}`}>
{item.status}
</span>
</div>
))}
</div>
</div>
);
}
if (variant === 'guest-review') {
return (
<div className="border border-border bg-card p-5">
<div className="flex items-start gap-3 border border-primary/30 bg-primary/5 p-4">
<LinkIcon className="mt-0.5 h-5 w-5 text-primary" />
<div>
<p className="text-sm font-medium">open-frame.net/watch/share-7f3a</p>
<p className="mt-1 text-sm text-muted-foreground">
Client opens the link, enters a name, and reviews in the browser. No account required.
</p>
</div>
</div>
<div className="mt-4 aspect-video border border-border/60 bg-gradient-to-br from-zinc-900 via-zinc-800 to-zinc-950" />
<div className="mt-4 flex items-center gap-3 border border-border/60 bg-background p-3">
<MessageSquare className="h-4 w-4 text-primary" />
<div>
<p className="text-sm font-medium">Client note at 00:02:14</p>
<p className="text-xs text-muted-foreground">Lower the music under the VO here.</p>
</div>
</div>
</div>
);
}
if (variant === 'voice-notes') {
return (
<div className="border border-border bg-card p-5">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center bg-secondary font-mono text-xs">
C
</div>
<div>
<p className="font-mono text-[11px] font-medium">Client</p>
<p className="font-mono text-[10px] text-muted-foreground">00:03:45</p>
</div>
</div>
<Mic className="h-4 w-4 text-primary" />
</div>
<div className="mt-6 flex h-16 items-center gap-1 overflow-hidden">
{Array.from({ length: 36 }).map((_, index) => (
<span
key={index}
className="w-full flex-1 bg-primary/60"
style={{
height: `${[30, 80, 50, 90, 40, 70, 60, 45, 85, 55][index % 10]}%`,
}}
/>
))}
</div>
<p className="mt-4 text-sm text-muted-foreground">
Voice note pinned to the exact frameno more typing long explanations.
</p>
</div>
);
}
return (
<div className="border border-border bg-card p-5">
<div className="aspect-video border border-border/60 bg-gradient-to-br from-zinc-900 via-zinc-800 to-zinc-950" />
<div className="relative mt-4 border border-border/60 bg-background p-4">
<div className="absolute right-4 top-4 rounded-full border border-primary/40 p-2 text-primary">
<PenTool className="h-4 w-4" />
</div>
<p className="font-mono text-[10px] text-muted-foreground">00:01:48</p>
<p className="mt-2 text-sm">Move the lower third up one line so it clears the subject.</p>
<div className="mt-4 h-1.5 w-full rounded-full bg-border">
<div className="h-1.5 w-[42%] rounded-full bg-primary" />
</div>
</div>
</div>
);
}
+716
View File
@@ -0,0 +1,716 @@
import type {
ComparisonPageDefinition,
FeatureRow,
PricingRow,
} from '@/lib/marketing/comparison-types';
import { competitorProfiles, openFrameProfile } from '@/lib/marketing/comparison-sources';
const commonOpenFrameWins = [
'$10/month flat hosted pricing — no per-member or per-guest fees',
'7-day free trial, then unlimited collaborators on one plan',
'Self-host for free with Docker when you need full data control',
'Voice notes and drawn annotations on the timeline',
'Formal approval requests with per-reviewer status',
'PDF and CSV exports for client handoff',
'Unlimited YouTube imports on the hosted plan',
];
const commonFreelancerPain = {
painTitle: 'Feedback should not live in five different apps',
painNarrative:
'Freelancers lose hours when clients send vague notes in email, WhatsApp, and screenshots. You end up guessing timecodes, re-uploading cuts, and chasing approvals that never feel final.',
painBullets: [
'Comments scattered across email threads and chat apps',
'Vague notes like "fix the intro around 1:12"',
'No clear record of which version was approved',
'Clients stall when the review tool feels complicated',
],
};
const commonClientApprovalPain = {
painTitle: 'Clients do not want another login',
painNarrative:
'Approval delays usually start before the first note. If a client has to create an account, install software, or decode a complex interface, the review slows down before it starts.',
painBullets: [
'Clients abandon review flows that require signup',
'Stakeholders comment on the wrong version',
'There is no single place to see approval status',
'Handoff reports get rebuilt manually after the fact',
],
};
const competitorFeatureRows: Record<string, FeatureRow[]> = {
'frame-io': [
{ label: 'Timestamped comments', openframe: 'yes', competitor: 'yes' },
{ label: 'Voice notes on timeline', openframe: 'yes', competitor: 'no' },
{ label: 'Drawn frame annotations', openframe: 'yes', competitor: 'yes' },
{ label: 'Version compare', openframe: 'yes', competitor: 'yes' },
{ label: 'Approval workflow', openframe: 'yes', competitor: 'yes' },
{ label: 'Guest review without account', openframe: 'yes', competitor: 'yes' },
{ label: 'Comment export', openframe: 'yes', competitor: 'yes' },
{ label: 'Self-hosting option', openframe: 'yes', competitor: 'no' },
{
label: 'Flat pricing without per-member fees',
openframe: 'yes',
competitor: 'no',
note: 'Frame.io guests review free via links, but team seats are billed per member.',
},
],
wipster: [
{ label: 'Timestamped comments', openframe: 'yes', competitor: 'yes' },
{ label: 'Voice notes on timeline', openframe: 'yes', competitor: 'no' },
{ label: 'Drawn frame annotations', openframe: 'yes', competitor: 'yes' },
{ label: 'Version compare', openframe: 'yes', competitor: 'yes' },
{ label: 'Approval workflow', openframe: 'yes', competitor: 'yes' },
{ label: 'Guest review without account', openframe: 'yes', competitor: 'yes' },
{ label: 'Comment export', openframe: 'yes', competitor: 'partial' },
{ label: 'Self-hosting option', openframe: 'yes', competitor: 'no' },
{
label: 'Flat pricing without per-member fees',
openframe: 'yes',
competitor: 'partial',
note: 'Wipster reviewers are free, but Team plans bill per user seat.',
},
],
'dropbox-replay': [
{ label: 'Timestamped comments', openframe: 'yes', competitor: 'yes' },
{ label: 'Voice notes on timeline', openframe: 'yes', competitor: 'no' },
{ label: 'Drawn frame annotations', openframe: 'yes', competitor: 'yes' },
{ label: 'Version compare', openframe: 'yes', competitor: 'yes' },
{ label: 'Approval workflow', openframe: 'yes', competitor: 'yes' },
{ label: 'Guest review without account', openframe: 'yes', competitor: 'yes' },
{ label: 'Comment export', openframe: 'yes', competitor: 'partial' },
{ label: 'Self-hosting option', openframe: 'yes', competitor: 'no' },
{
label: 'Flat pricing without per-member fees',
openframe: 'yes',
competitor: 'no',
note: 'Replay Add-On is priced per user on top of a Dropbox plan.',
},
],
flask: [
{ label: 'Voice feedback', openframe: 'yes', competitor: 'yes' },
{ label: 'Auto-structured spoken notes', openframe: 'partial', competitor: 'yes' },
{ label: 'Formal approval workflow', openframe: 'yes', competitor: 'partial' },
{ label: 'Guest review links', openframe: 'yes', competitor: 'yes' },
{ label: 'Version compare', openframe: 'yes', competitor: 'yes' },
{ label: 'Self-hosting', openframe: 'yes', competitor: 'no' },
{
label: 'Flat pricing without per-member fees',
openframe: 'yes',
competitor: 'partial',
note: 'Flask charges $0 per guest on Pro, but team seats are $15/user/month billed yearly.',
},
{ label: 'Published pricing', openframe: 'yes', competitor: 'yes' },
{ label: 'PDF/CSV export', openframe: 'yes', competitor: 'partial' },
],
'vimeo-review': [
{ label: 'Timestamped comments', openframe: 'yes', competitor: 'yes' },
{ label: 'Voice notes on timeline', openframe: 'yes', competitor: 'no' },
{ label: 'Drawn frame annotations', openframe: 'yes', competitor: 'partial' },
{ label: 'Version compare', openframe: 'yes', competitor: 'yes' },
{ label: 'Approval workflow', openframe: 'yes', competitor: 'yes' },
{ label: 'Guest review without account', openframe: 'yes', competitor: 'yes' },
{ label: 'Comment export', openframe: 'yes', competitor: 'no' },
{ label: 'Self-hosting option', openframe: 'yes', competitor: 'no' },
{
label: 'Flat pricing without per-member fees',
openframe: 'yes',
competitor: 'partial',
note: 'Vimeo review guests are free via links, but team hosting plans scale by seat.',
},
],
};
function defaultFeatureRows(competitorId: string): FeatureRow[] {
return competitorFeatureRows[competitorId] ?? [];
}
function competitorPricingRows(competitorId: string): PricingRow[] {
const profile = competitorProfiles[competitorId];
if (!profile) return [];
return [
{
label: 'Per-member or guest fees',
openframe: '$10/mo flat — no per-seat or guest charges',
competitor: profile.pricingSummary,
},
{
label: 'External reviewer access',
openframe: 'Share link, no account required',
competitor: profile.reviewerAccess,
},
{
label: 'Self-hosted option',
openframe: 'Free (Docker)',
competitor: profile.selfHosted ? 'Yes' : 'No',
},
{
label: 'Trial',
openframe: '7-day free trial on hosted',
competitor: profile.pricingNotes[0] ?? 'See vendor site',
},
];
}
function competitorPage(
slug: string,
competitorId: string,
overrides: Partial<ComparisonPageDefinition> &
Pick<
ComparisonPageDefinition,
| 'title'
| 'metaDescription'
| 'keywords'
| 'headline'
| 'subheadline'
| 'solutionTitle'
| 'solutionNarrative'
| 'openframeWins'
| 'competitorWins'
| 'faq'
>
): ComparisonPageDefinition {
const profile = competitorProfiles[competitorId];
return {
slug,
competitorId,
pageType: 'competitor',
eyebrow: `${profile.name} alternative`,
painTitle: commonFreelancerPain.painTitle,
painNarrative: commonFreelancerPain.painNarrative,
painBullets: commonFreelancerPain.painBullets,
bestForOpenFrame: openFrameProfile.bestFor,
bestForCompetitor: profile.bestFor,
featureRows: defaultFeatureRows(competitorId),
pricingRows: competitorPricingRows(competitorId),
pricingFootnote: 'Pricing and features change. Verify current plans on each vendor website.',
visualVariant: 'landing-dashboard',
relatedSlugs: [
'frame-io-alternative',
'video-review-tool-for-freelancers',
'client-video-approval-tool',
],
...overrides,
};
}
export const comparisonPages: ComparisonPageDefinition[] = [
competitorPage('frame-io-alternative', 'frame-io', {
title: 'Frame.io Alternative for Small Teams',
metaDescription:
'Compare OpenFrame vs Frame.io for freelancers and small teams. Get timestamped review, approvals, guest links, and self-hosting without per-seat enterprise pricing.',
keywords: ['frame.io alternative', 'frame io alternative', 'open source frame.io alternative'],
headline: 'Frame.io power without the enterprise bill',
subheadline:
'Frame.io is the industry standard for large post teams. OpenFrame gives freelancers and small studios the review workflow they actually need: one link, one timeline, clear approvals, and optional self-hosting.',
solutionTitle: 'Built for small teams, not studio overhead',
solutionNarrative:
'OpenFrame keeps the parts that save approval time—timestamped comments, voice notes, annotations, version compare, and sign-off tracking—without tying you to per-member pricing or a full creative-ops platform.',
openframeWins: [...commonOpenFrameWins, 'Fair-source codebase you can inspect and self-host'],
competitorWins: [
'Deep Adobe Premiere, Final Cut, and enterprise media workflows',
'Camera to Cloud, DRM, forensic watermarking, and SSO at enterprise scale',
'Mature metadata, collections, and multi-workspace operations',
],
faq: [
{
question: 'Do clients need a Frame.io account?',
answer:
'Not for share-link review. Frame.io supports external reviewers via links without accounts, but internal project management still uses paid member seats.',
},
{
question: 'Why switch from Frame.io to OpenFrame?',
answer:
'If you are a freelancer or small team paying for features you never touch, OpenFrame offers a simpler approval workflow, lower hosted pricing, and a free self-hosted path.',
},
{
question: 'Can OpenFrame replace Frame.io for enterprise MAM?',
answer:
'Not today. OpenFrame is focused on review, versioning, and approvals—not large-scale media asset management or Adobe enterprise controls.',
},
],
visualVariant: 'landing-compare',
relatedSlugs: [
'self-hosted-frame-io-alternative',
'wipster-alternative',
'client-video-approval-tool',
],
}),
{
slug: 'self-hosted-frame-io-alternative',
competitorId: 'frame-io',
pageType: 'use-case',
title: 'Self-Hosted Frame.io Alternative',
metaDescription:
'OpenFrame is a self-hosted Frame.io alternative with timestamped review, approvals, guest links, and Docker deployment for teams that need full data control.',
keywords: [
'self hosted frame.io alternative',
'self-hosted video review',
'open source video review',
],
eyebrow: 'Self-hosted review',
headline: 'Keep client footage on your infrastructure',
subheadline:
'If Frame.ios cloud-only model is the blocker, OpenFrame gives you the same core review loop—comments, versions, approvals, and share links—on hardware you control.',
painTitle: 'Cloud review tools are not always a fit',
painNarrative:
'Some teams cannot upload client masters to US-hosted SaaS by policy, budget, or principle. They still need timestamped review and approvals—not a return to email chaos.',
painBullets: [
'Client contracts restrict third-party cloud storage',
'Per-seat SaaS costs add up across occasional collaborators',
'You want auditability over where footage lives',
'You need a review tool that works air-gapped or on-prem',
],
solutionTitle: 'Self-host without giving up the workflow',
solutionNarrative:
'OpenFrame ships with Docker Compose, PostgreSQL, and S3-compatible storage. Optionally wire up your own Bunny CDN instance for streaming. Run on your server, keep projects private by default, and still send clients a simple browser review link.',
bestForOpenFrame: [
'Teams with Docker ops capacity',
'Privacy-sensitive client work',
'Studios comparing Frame.io to open/fair-source options',
],
bestForCompetitor: [
'Teams needing Adobe enterprise integrations out of the box',
'Studios without any infrastructure maintenance capacity',
'Large distributed teams needing vendor-managed scale',
],
openframeWins: [
'Free self-hosted deployment with full review features',
'Docker Compose setup with PostgreSQL and MinIO',
'Optional Bunny CDN integration for self-hosted streaming',
'Optional hosted cloud if you want zero ops later',
'Guest share links and approval workflow included',
],
competitorWins: [
'Fully managed transcoding and delivery without running your own stack',
'Enterprise security certifications and Adobe ecosystem depth',
'No server maintenance required',
],
featureRows: [
{ label: 'Self-hosting', openframe: 'yes', competitor: 'no' },
{ label: 'Docker deployment', openframe: 'yes', competitor: 'no' },
{ label: 'Guest review links', openframe: 'yes', competitor: 'yes' },
{ label: 'Approval workflow', openframe: 'yes', competitor: 'yes' },
{ label: 'Version compare', openframe: 'yes', competitor: 'yes' },
{ label: 'Enterprise DRM / SSO', openframe: 'no', competitor: 'yes' },
{ label: 'Adobe NLE integrations', openframe: 'no', competitor: 'yes' },
{ label: 'Open/fair-source codebase', openframe: 'yes', competitor: 'no' },
],
pricingRows: [
{ label: 'Self-hosted software cost', openframe: 'Free', competitor: 'Not available' },
{
label: 'Per-member or guest fees',
openframe: '$10/mo flat — no per-seat or guest charges',
competitor: 'From $15/member/mo for team seats',
},
{
label: 'CDN / streaming',
openframe: 'Optional self-hosted Bunny CDN',
competitor: 'Vendor-managed CDN',
},
{ label: 'Data residency', openframe: 'Your servers', competitor: 'Vendor cloud' },
],
pricingFootnote: 'Self-hosting still requires your own compute, storage, and maintenance.',
faq: [
{
question: 'Is OpenFrame really self-hostable?',
answer:
'Yes. The repository includes Docker Compose, migrations on boot, and optional S3-compatible storage. You can also use the hosted plan if you do not want to operate infrastructure.',
},
{
question: 'Can I use a CDN with self-hosted OpenFrame?',
answer:
'Yes. You can integrate your own Bunny CDN instance for streaming on self-hosted deployments, keeping delivery on infrastructure you control.',
},
{
question: 'Do clients still need accounts on self-hosted OpenFrame?',
answer:
'No. Clients can review via share links in the browser without creating an account.',
},
],
visualVariant: 'landing-dashboard',
relatedSlugs: [
'frame-io-alternative',
'video-review-tool-for-freelancers',
'client-video-approval-tool',
],
},
{
slug: 'video-review-tool-for-freelancers',
competitorId: null,
pageType: 'use-case',
title: 'Video Review Tool for Freelancers',
metaDescription:
'OpenFrame is a video review tool for freelancers with timestamped comments, voice notes, guest links, version compare, and affordable hosted or free self-hosted pricing.',
keywords: [
'video review tool for freelancers',
'freelance video review',
'client video feedback',
],
eyebrow: 'For freelancers',
headline: 'Stop chasing timecodes. Start shipping cuts.',
subheadline:
'You do not need enterprise creative ops to get professional client review. OpenFrame gives freelancers one link, one timeline, and a clear approval state.',
painTitle: commonFreelancerPain.painTitle,
painNarrative: commonFreelancerPain.painNarrative,
painBullets: commonFreelancerPain.painBullets,
solutionTitle: 'A freelancer-sized review stack',
solutionNarrative:
'Upload or import a cut, share a link, collect timestamped feedback with text or voice, compare versions, and export a report when the client signs off.',
bestForOpenFrame: openFrameProfile.bestFor,
bestForCompetitor: [
'Full-time post houses needing Adobe-native enterprise tooling',
'Teams already standardized on Frame.io or Dropbox',
'Studios needing storyboard-to-delivery production suites',
],
openframeWins: commonOpenFrameWins,
competitorWins: [
'Mature NLE integrations for large facility workflows',
'Specialized tools for EU-only hosting or voice-first feedback',
'All-in-one suites if you already pay for Vimeo or Dropbox',
],
featureRows: [
{ label: 'Timestamped comments', openframe: 'yes', competitor: 'Varies' },
{ label: 'Voice notes on timeline', openframe: 'yes', competitor: 'Rare' },
{ label: 'Guest review without account', openframe: 'yes', competitor: 'Varies' },
{ label: 'Approval workflow', openframe: 'yes', competitor: 'Varies' },
{ label: 'Self-hosting option', openframe: 'yes', competitor: 'Rare' },
{
label: 'Flat pricing without per-member fees',
openframe: 'yes',
competitor: 'Rare',
},
],
pricingRows: [
{
label: 'Per-member or guest fees',
openframe: '$10/mo flat — no per-seat or guest charges',
competitor: 'Often per-seat or bundled with hosting',
},
{ label: 'Self-hosted', openframe: 'Free', competitor: 'Rare' },
{ label: 'Client accounts', openframe: 'Not required', competitor: 'Varies by tool' },
{ label: 'YouTube imports', openframe: 'Unlimited on hosted', competitor: 'Varies' },
],
faq: [
{
question: 'What makes a good freelancer video review tool?',
answer:
'Clients should review in the browser without friction, every note should land on a timestamp, and you should always know which version is approved.',
},
{
question: 'Can I start free?',
answer: 'Yes. Use the 7-day hosted trial or self-host for free with Docker.',
},
{
question: 'Is OpenFrame only for video?',
answer:
'OpenFrame is video-first: comments, voice notes, annotations, versions, and approvals are built around the timeline.',
},
],
visualVariant: 'landing-dashboard',
relatedSlugs: ['client-video-approval-tool', 'frame-io-alternative', 'wipster-alternative'],
},
{
slug: 'client-video-approval-tool',
competitorId: null,
pageType: 'use-case',
title: 'Client Video Approval Tool',
metaDescription:
'OpenFrame is a client video approval tool with share links, timestamped feedback, approval requests, and PDF/CSV exports for freelancers and small teams.',
keywords: ['client video approval tool', 'video approval software', 'client sign off video'],
eyebrow: 'Client approvals',
headline: 'Get to “approved” without another login wall',
subheadline:
'Clients review in the browser, leave timestamped notes, and approve the exact version you need to ship.',
painTitle: commonClientApprovalPain.painTitle,
painNarrative: commonClientApprovalPain.painNarrative,
painBullets: commonClientApprovalPain.painBullets,
solutionTitle: 'Approvals that clients actually complete',
solutionNarrative:
'Send one link, collect precise feedback on the timeline, request approval from specific reviewers, and keep a record of who signed off on which version.',
bestForOpenFrame: [
'Editors and producers who need formal sign-off',
'Agencies with non-technical clients',
'Teams that export approval records for delivery',
],
bestForCompetitor: [
'Marketing orgs needing multi-asset proofing across PDFs and websites',
'Large teams with compliance-heavy approval chains',
'Studios standardized on incumbent review platforms',
],
openframeWins: [
'Approval requests with pending, approved, and rejected states',
'Guest share links with optional password and expiry',
'Timestamped comments, voice notes, and annotations',
'PDF/CSV exports for delivery documentation',
],
competitorWins: [
'Enterprise proofing suites with reviewer groups and automations',
'Deep Adobe integrations for facility-scale post',
'Voice-first feedback tools for spoken review sessions',
],
featureRows: [
{ label: 'Explicit approval states', openframe: 'yes', competitor: 'partial' },
{ label: 'Per-reviewer approval tracking', openframe: 'yes', competitor: 'partial' },
{ label: 'Guest review without account', openframe: 'yes', competitor: 'partial' },
{ label: 'Password-protected share links', openframe: 'yes', competitor: 'partial' },
{ label: 'Export approval history', openframe: 'yes', competitor: 'partial' },
{ label: 'Multi-asset marketing proofing', openframe: 'no', competitor: 'partial' },
],
pricingRows: [
{
label: 'Per-member or guest fees',
openframe: '$10/mo flat — no per-seat or guest charges',
competitor: 'Varies by platform',
},
{ label: 'Self-hosted', openframe: 'Free', competitor: 'Rare' },
{
label: 'Client seats',
openframe: 'Free via share links',
competitor: 'Often free via links',
},
{ label: 'Export reports', openframe: 'PDF/CSV included', competitor: 'Varies' },
],
faq: [
{
question: 'Do clients need an account to approve a video?',
answer:
'No. Clients open a share link, enter a name if needed, and can approve from the browser.',
},
{
question: 'Can I see who approved which version?',
answer: 'Yes. Approval requests track reviewer decisions per version.',
},
{
question: 'Can I send a password-protected review link?',
answer: 'Yes. Share links support optional password and expiry settings.',
},
],
visualVariant: 'landing-compare',
relatedSlugs: [
'video-review-tool-for-freelancers',
'frame-io-alternative',
'wipster-alternative',
],
},
competitorPage('wipster-alternative', 'wipster', {
title: 'Wipster Alternative for Video Review',
metaDescription:
'Compare OpenFrame vs Wipster for freelancers and small teams. Get guest review links, version compare, approvals, and lower pricing with optional self-hosting.',
keywords: ['wipster alternative', 'wipster vs openframe', 'video review alternative'],
headline: 'Wipster-style review without agency-scale pricing',
subheadline:
'Wipster is a solid video-first review tool. OpenFrame matches the core approval loop while adding voice notes, exports, and a free self-hosted path.',
solutionTitle: 'Same review outcome, simpler economics',
solutionNarrative:
'If you mainly need clients to comment on timecodes, compare versions, and approve a cut, OpenFrame covers that workflow at a lower monthly cost.',
openframeWins: [...commonOpenFrameWins, 'Lower hosted entry price for solo operators'],
competitorWins: [
'Mature NLE review panels for Premiere and After Effects',
'Long track record with agencies and universities',
'Supports audio, PDF, and image review in one place',
],
faq: [
{
question: 'Does Wipster charge for reviewers?',
answer:
'No. Wipster includes unlimited reviewers. OpenFrame also supports guest review links without client seats.',
},
{
question: 'When is Wipster still the better fit?',
answer:
'If you rely heavily on Wipsters NLE panels and agency workflows already in production.',
},
{
question: 'Does OpenFrame support version compare?',
answer: 'Yes. You can compare two versions side by side on one timeline.',
},
],
relatedSlugs: [
'frame-io-alternative',
'client-video-approval-tool',
'dropbox-replay-alternative',
],
}),
competitorPage('dropbox-replay-alternative', 'dropbox-replay', {
title: 'Dropbox Replay Alternative',
metaDescription:
'Compare OpenFrame vs Dropbox Replay for video review. Get a dedicated approval workflow, guest links, and optional self-hosting without Dropbox plan lock-in.',
keywords: ['dropbox replay alternative', 'dropbox replay vs', 'video review without dropbox'],
headline: 'Video review without Dropbox lock-in',
subheadline:
'Dropbox Replay is convenient if your files already live in Dropbox. OpenFrame is a focused review platform that does not require a storage suite to function.',
solutionTitle: 'A review tool, not a storage add-on',
solutionNarrative:
'OpenFrame is built around review, versions, and approvals first—so you are not paying for a file-sync platform just to collect timestamped client notes.',
openframeWins: [
...commonOpenFrameWins,
'No Dropbox plan or Replay Add-On required',
'Dedicated project and approval model',
],
competitorWins: [
'Native if your pipeline already lives in Dropbox',
'NLE integrations tied to Dropbox storage',
'Large file transfer and archive features via Dropbox',
],
faq: [
{
question: 'Is Dropbox Replay free?',
answer:
'Replay is included with limits on most plans. Full usage typically requires a paid Dropbox plan and often the Replay Add-On.',
},
{
question: 'Can OpenFrame replace Replay for NLE markers?',
answer:
'OpenFrame focuses on browser review, approvals, and exports rather than in-editor marker sync.',
},
{
question: 'Do clients need Dropbox accounts?',
answer:
'Not necessarily for all Replay flows, but the product assumes Dropbox storage context. OpenFrame uses standalone share links.',
},
],
relatedSlugs: [
'frame-io-alternative',
'vimeo-review-alternative',
'video-review-tool-for-freelancers',
],
}),
competitorPage('vimeo-review-alternative', 'vimeo-review', {
title: 'Vimeo Review Alternative',
metaDescription:
'Compare OpenFrame vs Vimeo Review. Get dedicated approvals, voice notes, exports, and self-hosting without tying review to Vimeo hosting plans.',
keywords: ['vimeo review alternative', 'vimeo video review alternative'],
headline: 'Review workflow without Vimeo plan lock-in',
subheadline:
'Vimeo Review is convenient when you already host on Vimeo. OpenFrame is a standalone review platform with its own hosted and self-hosted options.',
solutionTitle: 'Decouple review from distribution',
solutionNarrative:
'You should not need a video hosting subscription to run a professional approval loop. OpenFrame works whether your master lives on YouTube, direct upload, or self-hosted storage.',
openframeWins: [
...commonOpenFrameWins,
'Standalone product—not a hosting bundle',
'Unlimited YouTube imports on hosted plan',
],
competitorWins: [
'Built into Vimeo when you already distribute there',
'Premiere integration and Vimeo player polish',
'Review links tied to hosted library and version history',
],
faq: [
{
question: 'Do Vimeo reviewers need accounts?',
answer: 'No for review links. Guests can comment after providing name and email.',
},
{
question: 'Can OpenFrame import YouTube videos?',
answer: 'Yes. Hosted OpenFrame supports unlimited YouTube URL imports.',
},
{
question: 'When is Vimeo Review enough?',
answer: 'If your team already hosts, delivers, and reviews entirely inside Vimeo.',
},
],
visualVariant: 'landing-dashboard',
relatedSlugs: [
'dropbox-replay-alternative',
'video-review-tool-for-freelancers',
'frame-io-alternative',
],
}),
competitorPage('flask-alternative', 'flask', {
title: 'Flask Alternative for Video Feedback',
metaDescription:
'Compare OpenFrame vs Flask for video feedback. Get structured approvals, guest review links, exports, and self-hosting alongside spoken-note workflows.',
keywords: ['flask.do alternative', 'flask video feedback alternative'],
headline: 'Structured approvals beyond spoken feedback',
subheadline:
'Flask is compelling for voice-first review. OpenFrame is for teams that also need formal approvals, exports, guest links, and optional self-hosting.',
solutionTitle: 'Capture feedback and close the approval loop',
solutionNarrative:
'OpenFrame supports voice notes too—but it also tracks approval status per reviewer, compares versions, and exports a handoff record when the cut is cleared.',
openframeWins: [
...commonOpenFrameWins,
'Formal approval requests and status tracking',
'Self-host or use managed hosting',
'$10/mo flat for the whole team — not per seat',
],
competitorWins: [
'Best-in-class spoken feedback that auto-structures into notes',
'Free tier with all features (1 asset at a time, no card)',
'MCP/agent workflow integrations',
'Premiere export and upmarket production focus',
],
pricingRows: [
{
label: 'Team pricing',
openframe: '$10/mo flat — unlimited members and guests',
competitor: 'Free (1 asset) · Pro $15/user/mo (yearly) · Enterprise from 15 seats',
},
{
label: 'Per-guest fees',
openframe: 'None',
competitor: '$0 per guest on Pro',
},
{
label: 'Self-hosted option',
openframe: 'Free (Docker)',
competitor: 'No',
},
{
label: 'Trial',
openframe: '7-day free trial on hosted',
competitor: 'Free plan — no card required',
},
],
faq: [
{
question: 'Does OpenFrame support voice notes?',
answer: 'Yes. Reviewers can leave voice notes anchored to timestamps on the timeline.',
},
{
question: 'How much does Flask cost?',
answer:
'Flask offers a free plan (1 asset at a time, no card), Pro at $15/user/month billed yearly with $0 per guest, and custom enterprise pricing from 15 seats.',
},
{
question: 'When is Flask the better fit?',
answer:
'When your reviewers primarily talk through notes and you want AI-structured feedback capture.',
},
{
question: 'Can clients approve without an account in OpenFrame?',
answer: 'Yes. Share links support guest review and approval flows.',
},
],
visualVariant: 'landing-dashboard',
relatedSlugs: [
'client-video-approval-tool',
'video-review-tool-for-freelancers',
'frame-io-alternative',
],
}),
];
export const comparisonPageMap = Object.fromEntries(
comparisonPages.map((page) => [page.slug, page])
) as Record<string, ComparisonPageDefinition>;
export const comparisonSlugs = comparisonPages.map((page) => page.slug);
export const compareFooterLinks = comparisonPages.map((page) => ({
href: `/${page.slug}`,
label: page.eyebrow,
}));
export function getComparisonPage(slug: string): ComparisonPageDefinition | undefined {
return Object.hasOwn(comparisonPageMap, slug) ? comparisonPageMap[slug] : undefined;
}
export function getCompetitorName(page: ComparisonPageDefinition): string | null {
if (!page.competitorId) return null;
return competitorProfiles[page.competitorId]?.name ?? null;
}
+571
View File
@@ -0,0 +1,571 @@
import type { CompetitorProfile, CompetitorSource } from '@/lib/marketing/comparison-types';
const LAST_CHECKED = '2026-06-14';
export const competitorSources: CompetitorSource[] = [
{
competitor: 'frame-io',
sourceUrl: 'https://frame.io/pricing',
sourceType: 'pricing',
lastChecked: LAST_CHECKED,
claims: [
'Free plan: 2 members, 2GB storage, 2 projects.',
'Pro: $15/member/month with 2TB included + 2TB per additional member.',
'Team: $25/member/month with 3TB included + 2TB per additional member.',
],
caveats: ['Pricing excludes tax. Enterprise is custom.'],
confidence: 'high',
},
{
competitor: 'frame-io',
sourceUrl: 'https://help.frame.io/en/articles/9090642-getting-started-what-is-a-user',
sourceType: 'help',
lastChecked: LAST_CHECKED,
claims: [
'Reviewers can access media via share links without a Frame.io account.',
'Reviewers are free and unlimited for external link-based review.',
'Paid member seats are required for internal project management roles.',
],
caveats: ['Guest users inside the account are limited on some plans.'],
confidence: 'high',
},
{
competitor: 'frame-io',
sourceUrl: 'https://help.frame.io/en/articles/9105242-share-links-explained-for-clients',
sourceType: 'help',
lastChecked: LAST_CHECKED,
claims: [
'Share links support passphrase protection, expiration, download controls, and approval status.',
'Commenting can be disabled per share link.',
],
caveats: ['Some enterprise security features require higher tiers.'],
confidence: 'high',
},
{
competitor: 'wipster',
sourceUrl: 'https://wipster.io/pricing',
sourceType: 'pricing',
lastChecked: LAST_CHECKED,
claims: [
'Light plan: $9.95/month annually or $11.95 monthly with 50GB storage.',
'Team plan: $19.95/user/month annually or $25 monthly starting at 250GB.',
'Unlimited reviewers on all plans.',
],
caveats: ['Enterprise pricing is custom.'],
confidence: 'high',
},
{
competitor: 'wipster',
sourceUrl: 'https://wipster.io/product',
sourceType: 'product',
lastChecked: LAST_CHECKED,
claims: [
'Supports video, image, PDF, and audio review.',
'Version comparison, approval tracking, and NLE panel integrations.',
'Public URL or private email sharing for reviewers.',
],
caveats: ['SSO and org controls are enterprise features.'],
confidence: 'high',
},
{
competitor: 'dropbox-replay',
sourceUrl: 'https://help.dropbox.com/create-upload/dropbox-replay-faq',
sourceType: 'faq',
lastChecked: LAST_CHECKED,
claims: [
'Replay is available on all Dropbox plans but free tiers have 410 file limits.',
'Replay Add-On removes file creation/upload limits.',
'Files uploaded from outside Dropbox count against Dropbox storage.',
],
caveats: ['Replay Add-On requires a paid Dropbox plan.'],
confidence: 'high',
},
{
competitor: 'dropbox-replay',
sourceUrl: 'https://www.dropbox.com/replay',
sourceType: 'product',
lastChecked: LAST_CHECKED,
claims: [
'Replay Add-On is about €9/user/month annually or €11 monthly.',
'Integrates with Premiere Pro, After Effects, Final Cut Pro, DaVinci Resolve, and Avid Pro Tools.',
'Supports version comparison, transcriptions, due dates, and password-protected links on paid add-on.',
],
caveats: ['Requires Dropbox ecosystem buy-in.'],
confidence: 'high',
},
{
competitor: 'krock',
sourceUrl: 'https://krock.io/pricing/',
sourceType: 'pricing',
lastChecked: LAST_CHECKED,
claims: [
'Free: 1 user, 2GB, 2 projects, unlimited reviewers.',
'Pro: $10/user/month with 2TB storage and unlimited projects.',
'Unlimited plan: flat $400/month for large teams.',
],
caveats: ['Storage add-ons are $10/month per TB.'],
confidence: 'high',
},
{
competitor: 'krock',
sourceUrl: 'https://krock.io/help-center/how-to-manage-team-members-collaborators-reviewers/',
sourceType: 'help',
lastChecked: LAST_CHECKED,
claims: [
'Reviewers can access projects via links without paid seats.',
'Includes storyboard, animatic, and production workflow tooling.',
],
caveats: ['Storyboard AI is newer and may not fit every pipeline.'],
confidence: 'high',
},
{
competitor: 'revcut',
sourceUrl: 'https://revcut.io/pricing',
sourceType: 'pricing',
lastChecked: LAST_CHECKED,
claims: [
'Crew: €69/month with 8 seats and 3TB active storage.',
'Studio: €149/month with 15 seats and 5TB active storage.',
'14-day free trial with no credit card required.',
],
caveats: ['Storage is designed for active review cycles, not long-term archiving.'],
confidence: 'high',
},
{
competitor: 'revcut',
sourceUrl: 'https://revcut.io/client-video-review',
sourceType: 'product',
lastChecked: LAST_CHECKED,
claims: [
'Clients review via secure links with name-only access, no account required.',
'Native DaVinci Resolve plugin for marker export.',
'EU-hosted with GDPR-minded positioning.',
],
caveats: ['Premiere/FCP integrations listed as coming later.'],
confidence: 'high',
},
{
competitor: 'vimeo-review',
sourceUrl:
'https://help.vimeo.com/hc/en-us/articles/12426192100113-How-to-use-and-manage-video-review-links',
sourceType: 'help',
lastChecked: LAST_CHECKED,
claims: [
'Review links work for people without a Vimeo account.',
'Supports passwords, expiration, downloads, commenting, and approval status toggles.',
'Guests provide name and email before commenting.',
],
caveats: ['Folder review links require Standard plan or higher.'],
confidence: 'high',
},
{
competitor: 'vimeo-review',
sourceUrl: 'https://vimeo.com/features/video-collaboration',
sourceType: 'product',
lastChecked: LAST_CHECKED,
claims: [
'Review is built into Vimeo hosting with version history and Premiere integration.',
'Time-stamped comments with resolve workflow.',
],
caveats: ['Best fit when you already host video on Vimeo.'],
confidence: 'high',
},
{
competitor: 'vitransfer',
sourceUrl: 'https://www.vitransfer.com/docs',
sourceType: 'docs',
lastChecked: LAST_CHECKED,
claims: [
'AGPL-3.0 self-hosted video review and approval platform.',
'Share links support password, email OTP, guest mode, and approval workflows.',
'Guests are view-only by default; downloads gated until approval.',
],
caveats: ['Requires Docker infrastructure and ongoing maintenance.'],
confidence: 'high',
},
{
competitor: 'freeframe',
sourceUrl: 'https://github.com/Techiebutler/freeframe',
sourceType: 'github',
lastChecked: LAST_CHECKED,
claims: [
'Self-hosted open-source media review for video, image, and audio.',
'Frame-accurate comments, annotations, approvals, version compare, and guest share links.',
'Uses FastAPI backend with Next.js frontend and S3-compatible storage.',
],
caveats: ['Requires external S3, SMTP, and production ops setup.'],
confidence: 'high',
},
{
competitor: 'flask',
sourceUrl: 'https://flask.do/pricing',
sourceType: 'pricing',
lastChecked: LAST_CHECKED,
claims: [
'Free: $0, no card required, all features, 1 asset at a time.',
'Pro: $15/user/month billed yearly; $0 per guest; unlimited uploads.',
'Pro includes original file downloads for 30 days; enterprise from 15 seats.',
],
caveats: ['Pro is billed yearly; enterprise pricing is custom.'],
confidence: 'high',
},
{
competitor: 'flask',
sourceUrl: 'https://flask.do/',
sourceType: 'product',
lastChecked: LAST_CHECKED,
claims: [
'Voice-first feedback: record spoken notes while watching and Flask structures timestamped comments.',
'Supports drawing, references, version compare, Drive import, and Premiere export.',
'Positions upmarket for production teams of 5100 people.',
],
caveats: ['Free tier limits you to one active asset at a time.'],
confidence: 'high',
},
{
competitor: 'aligno',
sourceUrl: 'https://aligno.io/pricing',
sourceType: 'pricing',
lastChecked: LAST_CHECKED,
claims: [
'Pro: $18/month or $180/year flat rate per creator.',
'Clients never need accounts and do not count toward limits.',
'Supports images, PDFs, videos, and webpages with explicit approval states.',
],
caveats: ['Video review is one part of a broader multi-asset approval tool.'],
confidence: 'high',
},
{
competitor: 'videoreview-pro',
sourceUrl: 'https://videoreview.pro/',
sourceType: 'pricing',
lastChecked: LAST_CHECKED,
claims: [
'Free forever with 30 minutes total video storage.',
'Pro: $99/year with 10 hours of storage.',
'All features included on both plans; only storage differs.',
],
caveats: ['Very limited storage compared to production workflows.'],
confidence: 'high',
},
];
export const competitorProfiles: Record<string, CompetitorProfile> = {
'frame-io': {
id: 'frame-io',
name: 'Frame.io',
tagline: 'Enterprise creative operations platform with deep Adobe integration.',
bestFor: [
'Large post-production teams in Adobe Creative Cloud',
'Studios needing enterprise security, DRM, and MAM workflows',
'Teams that need Camera to Cloud and hardware integrations',
],
strengths: [
'Industry-standard Adobe Premiere and Final Cut integrations',
'Mature share-link review with approvals, watermarks, and enterprise controls',
'Transcription, captions, metadata, and broad asset management',
],
limitations: [
'Per-member pricing scales quickly for small teams',
'Heavier platform than freelancers need for simple client approvals',
'No self-hosting option for teams that need full data control',
],
pricingSummary: 'Free (2GB) · Pro $15/member/mo · Team $25/member/mo',
pricingNotes: [
'External reviewers via share links are free.',
'Paid seats are for team members managing projects inside Frame.io.',
],
reviewerAccess:
'Share links allow review without a Frame.io account; internal roles require paid seats.',
selfHosted: false,
sources: [
'https://frame.io/pricing',
'https://help.frame.io/en/articles/9090642-getting-started-what-is-a-user',
],
},
wipster: {
id: 'wipster',
name: 'Wipster',
tagline: 'Video-first review and approval for agencies and production teams.',
bestFor: [
'Agencies managing multiple client review cycles',
'Video teams wanting NLE panel integrations',
'Teams that need unlimited external reviewers',
],
strengths: [
'Unlimited reviewers on every plan',
'Side-by-side version comparison and approval tracking',
'Premiere Pro and After Effects panel integrations',
],
limitations: [
'Per-user pricing on Team plans',
'US-hosted SaaS with no self-host option',
'Broader creative-ops features are lighter than Frame.io enterprise',
],
pricingSummary: 'Light $9.95/mo · Team from $19.95/user/mo',
pricingNotes: ['Free trial with no credit card required.', 'Enterprise is custom.'],
reviewerAccess: 'Reviewers can use public links or email invites without paid seats.',
selfHosted: false,
sources: ['https://wipster.io/pricing', 'https://wipster.io/product'],
},
'dropbox-replay': {
id: 'dropbox-replay',
name: 'Dropbox Replay',
tagline: 'Review and approval add-on inside the Dropbox ecosystem.',
bestFor: [
'Teams already storing and delivering files in Dropbox',
'Editors who want NLE integrations tied to Dropbox storage',
'Lightweight review without adopting a separate platform',
],
strengths: [
'Native integrations with Premiere, After Effects, FCP, Resolve, and Pro Tools',
'Version comparison, transcriptions, and due dates on paid add-on',
'Familiar Dropbox security and link management',
],
limitations: [
'Requires Dropbox plan plus Replay Add-On for full usage',
'Free Replay tiers cap active files at 410 uploads',
'Not a standalone review platform with its own project model',
],
pricingSummary: 'Replay Add-On ~€9/user/mo annually',
pricingNotes: [
'Basic Replay is included but file-limited on most plans.',
'Password protection and transcription require the paid add-on.',
],
reviewerAccess: 'Collaborators review in browser; full value assumes Dropbox account context.',
selfHosted: false,
sources: [
'https://www.dropbox.com/replay',
'https://help.dropbox.com/create-upload/dropbox-replay-faq',
],
},
krock: {
id: 'krock',
name: 'Krock.io',
tagline: 'Creative production workspace with storyboards, animatics, and review.',
bestFor: [
'Animation studios and pre-to-post production pipelines',
'Teams wanting storyboard and animatic review in one tool',
'Studios needing unlimited external reviewers',
],
strengths: [
'Storyboard and animatic tooling upstream of video review',
'Unlimited reviewers on all plans',
'Integrations with Adobe CC, Resolve, FCP, and Slack',
],
limitations: [
'Per-user pricing on Pro plan',
'Broader production tooling may be more than solo freelancers need',
'No self-hosting option',
],
pricingSummary: 'Free · Pro $10/user/mo · Unlimited $400/mo flat',
pricingNotes: ['7-day trial on paid plans.', 'Storage add-ons available.'],
reviewerAccess: 'Guest review links without account creation.',
selfHosted: false,
sources: [
'https://krock.io/pricing/',
'https://krock.io/help-center/how-to-manage-team-members-collaborators-reviewers/',
],
},
revcut: {
id: 'revcut',
name: 'RevCut',
tagline: 'EU-hosted video review built for editors and small post teams.',
bestFor: [
'EU-based editors needing GDPR-minded hosting',
'DaVinci Resolve-first workflows',
'Small teams wanting client review without logins',
],
strengths: [
'Clients review via secure links with name-only access',
'Native DaVinci Resolve plugin',
'Explicit EU hosting and privacy positioning',
],
limitations: [
'No self-hosting option',
'Higher entry pricing than solo freelancer tools',
'Premiere/FCP integrations still limited compared to Frame.io',
],
pricingSummary: 'Crew €69/mo · Studio €149/mo',
pricingNotes: ['14-day free trial.', 'Storage is for active review cycles.'],
reviewerAccess: 'Clients use secure links with optional password; no account required.',
selfHosted: false,
sources: ['https://revcut.io/pricing', 'https://revcut.io/client-video-review'],
},
'vimeo-review': {
id: 'vimeo-review',
name: 'Vimeo Review',
tagline: 'Review tools built into Vimeo video hosting.',
bestFor: [
'Teams already hosting and delivering on Vimeo',
'Creators who want review without a second subscription',
'Workflows centered on Vimeo distribution',
],
strengths: [
'Review links with no Vimeo account required for guests',
'Version history, status labels, and Premiere integration',
'Password and expiration controls on review links',
],
limitations: [
'Tied to Vimeo hosting plans',
'Less focused on standalone approval workflows',
'No self-hosting or open-source path',
],
pricingSummary: 'Bundled with Vimeo Standard+ plans',
pricingNotes: ['Folder review links require Standard plan or higher.'],
reviewerAccess: 'External reviewers use review links without Vimeo accounts.',
selfHosted: false,
sources: [
'https://vimeo.com/features/video-collaboration',
'https://help.vimeo.com/hc/en-us/articles/12426192100113-How-to-use-and-manage-video-review-links',
],
},
vitransfer: {
id: 'vitransfer',
name: 'ViTransfer',
tagline: 'AGPL self-hosted video review with approval workflows.',
bestFor: [
'Teams that want open-source and full infrastructure control',
'Studios with Docker ops capacity',
'Privacy-sensitive client work kept on owned hardware',
],
strengths: [
'No subscription fees beyond your own infrastructure',
'Per-video approvals with download gating',
'Password, email OTP, and guest access modes',
],
limitations: [
'Requires setup, maintenance, and transcoding resources',
'Guest mode is view-only by default',
'Smaller ecosystem than commercial incumbents',
],
pricingSummary: 'Free (self-hosted, AGPL-3.0)',
pricingNotes: ['Storage limited only by your own hardware.'],
reviewerAccess: 'Share links with configurable auth; guest mode is view-only.',
selfHosted: true,
sources: ['https://www.vitransfer.com/docs', 'https://www.vitransfer.com/about'],
},
freeframe: {
id: 'freeframe',
name: 'FreeFrame',
tagline: 'Self-hosted open-source media review platform.',
bestFor: [
'Teams wanting multi-format review on owned infrastructure',
'Organizations with S3 and SMTP already in place',
'Open-source adopters comparing to Frame.io',
],
strengths: [
'Video, image, and audio review in one platform',
'Approvals, version compare, guest share links, and SSE updates',
'Bring-your-own S3, Redis, and PostgreSQL',
],
limitations: [
'Production deployment requires DevOps effort',
'Different stack (FastAPI + Next.js) from OpenFrame',
'Younger project with smaller community',
],
pricingSummary: 'Free (self-hosted, open source)',
pricingNotes: ['Infrastructure and ops costs apply.'],
reviewerAccess: 'Guest commenting via password-protected share links.',
selfHosted: true,
sources: ['https://github.com/Techiebutler/freeframe'],
},
flask: {
id: 'flask',
name: 'Flask',
tagline: 'Voice-first video feedback that structures spoken notes automatically.',
bestFor: [
'Reviewers who prefer talking through feedback instead of typing',
'Production teams of 5100 people',
'Teams exploring AI-assisted feedback workflows',
],
strengths: [
'Records spoken feedback and turns it into timestamped action items',
'Drawing, references, version compare, and Premiere export',
'MCP integration for agent-assisted workflows',
'Free tier with full features (1 asset at a time)',
],
limitations: [
'No public self-host option',
'Pro is $15/user/month billed yearly',
'Free tier limited to one active asset',
'Focused on feedback capture more than formal approval chains',
],
pricingSummary: 'Free (1 asset) · Pro $15/user/mo (yearly) · Enterprise custom',
pricingNotes: ['Pro includes $0 per guest and unlimited uploads.'],
reviewerAccess: '$0 per guest on Pro; free tier for trying without a card.',
selfHosted: false,
sources: ['https://flask.do/pricing', 'https://flask.do/'],
},
aligno: {
id: 'aligno',
name: 'Aligno',
tagline: 'Flat-rate multi-asset approval for designers and agencies.',
bestFor: [
'Designers and agencies reviewing video alongside images and PDFs',
'Solo creators with many clients and no per-reviewer fees',
'Teams wanting explicit approve/request-changes workflow',
],
strengths: [
'Flat per-creator pricing with unlimited free clients',
'Multi-asset review in one workspace',
'Explicit approval states on every version',
],
limitations: [
'Less depth for full-time post-production houses',
'No self-hosting option',
'Video tooling is narrower than dedicated review platforms',
],
pricingSummary: 'Free · Pro $18/mo · Studio custom',
pricingNotes: ['Clients never count toward seat limits.'],
reviewerAccess: 'Clients review via share links without accounts.',
selfHosted: false,
sources: ['https://aligno.io/pricing', 'https://aligno.io/frame-io-alternative'],
},
'videoreview-pro': {
id: 'videoreview-pro',
name: 'VideoReview.pro',
tagline: 'Minimal timestamped video feedback with a generous free tier.',
bestFor: [
'Creators needing basic timestamped comments on a budget',
'Very small projects with limited storage needs',
'Teams testing video review for the first time',
],
strengths: [
'Free forever with all features included',
'Simple upload, share, and comment workflow',
'Password-protected share links',
],
limitations: [
'Only 30 minutes free storage; Pro caps at 10 hours/year',
'No formal approval workflow or version compare',
'No self-hosting or open-source option',
],
pricingSummary: 'Free (30 min) · Pro $99/year (10 hours)',
pricingNotes: ['Storage is the only plan differentiator.'],
reviewerAccess: 'Share links for client review.',
selfHosted: false,
sources: ['https://videoreview.pro/'],
},
};
export const openFrameProfile = {
name: 'OpenFrame',
tagline: 'Fair-source video review and approval with hosted and self-hosted options.',
pricingSummary: 'Self-hosted free · Hosted $10/mo after 7-day trial',
trial: '7-day free trial on hosted plan',
strengths: [
'Timestamped text, voice, image, and drawn annotations',
'Version compare with side-by-side review',
'Approval requests with per-reviewer decisions',
'Guest share links with optional password and expiry',
'PDF/CSV comment exports',
'Unlimited YouTube imports on hosted plan',
'Self-host with Docker or use hosted cloud',
],
bestFor: [
'Freelancers and small teams shipping client cuts',
'Studios that want open/fair-source and optional self-hosting',
'Teams tired of vague email and WhatsApp feedback',
],
};
+91
View File
@@ -0,0 +1,91 @@
export type SourceType =
| 'pricing'
| 'product'
| 'faq'
| 'help'
| 'docs'
| 'comparison'
| 'github'
| 'changelog';
export type Confidence = 'high' | 'medium' | 'low';
export interface CompetitorSource {
competitor: string;
sourceUrl: string;
sourceType: SourceType;
lastChecked: string;
claims: string[];
caveats: string[];
confidence: Confidence;
}
export interface CompetitorProfile {
id: string;
name: string;
tagline: string;
bestFor: string[];
strengths: string[];
limitations: string[];
pricingSummary: string;
pricingNotes: string[];
reviewerAccess: string;
selfHosted: boolean;
sources: string[];
}
export type FeatureStatus = 'yes' | 'no' | 'partial' | 'openframe-only' | 'competitor-only';
export interface FeatureRow {
label: string;
openframe: FeatureStatus | string;
competitor: FeatureStatus | string;
note?: string;
}
export interface PricingRow {
label: string;
openframe: string;
competitor: string;
}
export interface ComparisonFaq {
question: string;
answer: string;
}
export type VisualVariant =
| 'timeline-comments'
| 'version-compare'
| 'approval-workflow'
| 'guest-review'
| 'voice-notes'
| 'landing-compare'
| 'landing-dashboard';
export interface ComparisonPageDefinition {
slug: string;
title: string;
metaDescription: string;
keywords: string[];
competitorId: string | null;
pageType: 'competitor' | 'use-case';
eyebrow: string;
headline: string;
subheadline: string;
painTitle: string;
painNarrative: string;
painBullets: string[];
solutionTitle: string;
solutionNarrative: string;
bestForOpenFrame: string[];
bestForCompetitor: string[];
openframeWins: string[];
competitorWins: string[];
featureRows: FeatureRow[];
pricingRows: PricingRow[];
pricingFootnote?: string;
faq: ComparisonFaq[];
visualVariant: VisualVariant;
relatedSlugs: string[];
}
+106
View File
@@ -0,0 +1,106 @@
import type { Metadata } from 'next';
import { seoConfig } from '@/lib/seo';
export function buildComparisonMetadata({
title,
description,
path,
keywords = [],
}: {
title: string;
description: string;
path: string;
keywords?: string[];
}): Metadata {
const canonicalPath = path.startsWith('/') ? path : `/${path}`;
const pageTitle = title;
const ogTitle = `${pageTitle} | ${seoConfig.name}`;
return {
title: pageTitle,
description,
keywords: [...seoConfig.keywords, ...keywords],
alternates: {
canonical: canonicalPath,
},
openGraph: {
title: ogTitle,
description,
url: `${seoConfig.url}${canonicalPath}`,
images: [
{
url: seoConfig.ogImage,
width: 1888,
height: 1048,
alt: `${pageTitle} | ${seoConfig.name}`,
},
],
},
twitter: {
card: 'summary_large_image',
title: ogTitle,
description,
images: [seoConfig.ogImage],
},
};
}
export function buildComparisonJsonLd({
title,
description,
path,
faq,
}: {
title: string;
description: string;
path: string;
faq: Array<{ question: string; answer: string }>;
}) {
const url = `${seoConfig.url}${path.startsWith('/') ? path : `/${path}`}`;
const structuredData: Array<Record<string, unknown>> = [
{
'@context': 'https://schema.org',
'@type': 'WebPage',
name: title,
description,
url,
isPartOf: {
'@type': 'WebSite',
name: seoConfig.name,
url: seoConfig.url,
},
},
{
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: seoConfig.name,
applicationCategory: 'MultimediaApplication',
operatingSystem: 'Web',
offers: {
'@type': 'Offer',
price: '10',
priceCurrency: 'USD',
description: '7-day free trial, then $10/month hosted plan. Self-hosted option is free.',
},
url: seoConfig.url,
},
];
if (faq.length > 0) {
structuredData.push({
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faq.map((item) => ({
'@type': 'Question',
name: item.question,
acceptedAnswer: {
'@type': 'Answer',
text: item.answer,
},
})),
});
}
return structuredData;
}