mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
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:
@@ -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)} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 frame—no 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>
|
||||
);
|
||||
}
|
||||
@@ -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.io’s 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 Wipster’s 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;
|
||||
}
|
||||
@@ -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 4–10 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 5–100 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 4–10 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 5–100 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',
|
||||
],
|
||||
};
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user