diff --git a/apps/marketing/public/company-logos/codeinterview.png b/apps/marketing/public/company-logos/codeinterview.png new file mode 100644 index 0000000..1836995 Binary files /dev/null and b/apps/marketing/public/company-logos/codeinterview.png differ diff --git a/apps/marketing/public/company-logos/coderbyte.png b/apps/marketing/public/company-logos/coderbyte.png new file mode 100644 index 0000000..d848192 Binary files /dev/null and b/apps/marketing/public/company-logos/coderbyte.png differ diff --git a/apps/marketing/public/company-logos/coderpad.png b/apps/marketing/public/company-logos/coderpad.png new file mode 100644 index 0000000..d0abac5 Binary files /dev/null and b/apps/marketing/public/company-logos/coderpad.png differ diff --git a/apps/marketing/public/company-logos/codesignal.png b/apps/marketing/public/company-logos/codesignal.png new file mode 100644 index 0000000..72a010f Binary files /dev/null and b/apps/marketing/public/company-logos/codesignal.png differ diff --git a/apps/marketing/public/company-logos/codesubmit.png b/apps/marketing/public/company-logos/codesubmit.png new file mode 100644 index 0000000..ffc22ca Binary files /dev/null and b/apps/marketing/public/company-logos/codesubmit.png differ diff --git a/apps/marketing/public/company-logos/hackerrank.jpg b/apps/marketing/public/company-logos/hackerrank.jpg new file mode 100644 index 0000000..ae1e696 Binary files /dev/null and b/apps/marketing/public/company-logos/hackerrank.jpg differ diff --git a/apps/marketing/public/company-logos/karat.png b/apps/marketing/public/company-logos/karat.png new file mode 100644 index 0000000..b12bf44 Binary files /dev/null and b/apps/marketing/public/company-logos/karat.png differ diff --git a/apps/marketing/public/company-logos/leetcode.png b/apps/marketing/public/company-logos/leetcode.png new file mode 100644 index 0000000..01903aa Binary files /dev/null and b/apps/marketing/public/company-logos/leetcode.png differ diff --git a/apps/marketing/public/company-logos/qualified.png b/apps/marketing/public/company-logos/qualified.png new file mode 100644 index 0000000..ec4d87b Binary files /dev/null and b/apps/marketing/public/company-logos/qualified.png differ diff --git a/apps/marketing/public/company-logos/testgorilla.png b/apps/marketing/public/company-logos/testgorilla.png new file mode 100644 index 0000000..47fe517 Binary files /dev/null and b/apps/marketing/public/company-logos/testgorilla.png differ diff --git a/apps/marketing/src/app/(alternatives)/[slug]/page.tsx b/apps/marketing/src/app/(alternatives)/[slug]/page.tsx index 34cf3f5..2fe6d2d 100644 --- a/apps/marketing/src/app/(alternatives)/[slug]/page.tsx +++ b/apps/marketing/src/app/(alternatives)/[slug]/page.tsx @@ -1,13 +1,14 @@ import { Metadata } from 'next'; import { notFound } from 'next/navigation'; import { AlternativePageView } from '@/components/alternative/AlternativePageView'; +import { RoundupPageView } from '@/components/alternative/roundup/RoundupPageView'; import { competitorData } from '@/lib/alternativeConfig'; import { buildBreadcrumbSchema } from '@/lib/breadcrumbs'; +import { roundupData } from '@/lib/roundupConfig'; export function generateStaticParams() { - return Object.keys(competitorData).map((key) => ({ - slug: key, - })); + const slugs = new Set([...Object.keys(competitorData), ...Object.keys(roundupData)]); + return Array.from(slugs).map((slug) => ({ slug })); } interface AlternativeCompetitorPageProps { @@ -18,9 +19,13 @@ export async function generateMetadata({ params, }: AlternativeCompetitorPageProps): Promise { const { slug } = await params; + const roundup = roundupData[slug]; const competitor = competitorData[slug]; - if (!competitor) { + // Prefer the roundup SEO when this slug has been upgraded to a roundup page. + const seo = roundup?.seo ?? competitor?.seo; + + if (!seo) { return { title: 'Not Found', }; @@ -28,17 +33,22 @@ export async function generateMetadata({ const siteUrl = 'https://coderscreen.com'; const pageUrl = `${siteUrl}/${slug}`; + const ogAlt = roundup + ? roundup.seo.title + : competitor + ? `CoderScreen vs ${competitor.displayName}` + : 'CoderScreen'; return { - title: competitor.seo.title, - description: competitor.seo.description, - keywords: competitor.seo.keywords, + title: seo.title, + description: seo.description, + keywords: seo.keywords, alternates: { canonical: pageUrl, }, openGraph: { - title: competitor.seo.title, - description: competitor.seo.description, + title: seo.title, + description: seo.description, url: pageUrl, siteName: 'CoderScreen', type: 'website', @@ -47,14 +57,14 @@ export async function generateMetadata({ url: `${siteUrl}/og-image.png`, width: 1200, height: 630, - alt: `CoderScreen vs ${competitor.displayName}`, + alt: ogAlt, }, ], }, twitter: { card: 'summary_large_image', - title: competitor.seo.title, - description: competitor.seo.description, + title: seo.title, + description: seo.description, images: [`${siteUrl}/og-image.png`], }, robots: { @@ -76,14 +86,20 @@ export default async function AlternativeCompetitorPage({ }: AlternativeCompetitorPageProps) { const { slug } = await params; + const roundup = roundupData[slug]; const competitor = competitorData[slug]; - if (!competitor) { + + if (!roundup && !competitor) { notFound(); } + const breadcrumbLabel = roundup + ? `${roundup.competitorName} Alternatives` + : `${competitor?.displayName} Alternative`; + const breadcrumbSchema = buildBreadcrumbSchema([ { name: 'Home', href: '/' }, - { name: `${competitor.displayName} Alternative`, href: `/${slug}` }, + { name: breadcrumbLabel, href: `/${slug}` }, ]); return ( @@ -93,7 +109,11 @@ export default async function AlternativeCompetitorPage({ // biome-ignore lint/security/noDangerouslySetInnerHtml: needed for SEO schema dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }} /> - + {roundup ? ( + + ) : ( + competitor && + )} ); } diff --git a/apps/marketing/src/components/alternative/roundup/BuyingGuide.tsx b/apps/marketing/src/components/alternative/roundup/BuyingGuide.tsx new file mode 100644 index 0000000..ba5e74b --- /dev/null +++ b/apps/marketing/src/components/alternative/roundup/BuyingGuide.tsx @@ -0,0 +1,28 @@ +import type { RoundupPage } from '@/lib/roundupConfig'; +import { RoundupSectionHeader } from './RoundupSectionHeader'; + +interface BuyingGuideProps { + roundup: RoundupPage; +} + +export const BuyingGuide = ({ roundup }: BuyingGuideProps) => { + return ( +
+ + +
+
+ {roundup.buyingGuide.map((segment) => ( +
+

{segment.segment}

+

{segment.advice}

+
+ ))} +
+
+
+ ); +}; diff --git a/apps/marketing/src/components/alternative/roundup/FeaturesToLookFor.tsx b/apps/marketing/src/components/alternative/roundup/FeaturesToLookFor.tsx new file mode 100644 index 0000000..1ba176e --- /dev/null +++ b/apps/marketing/src/components/alternative/roundup/FeaturesToLookFor.tsx @@ -0,0 +1,46 @@ +import type { RoundupPage } from '@/lib/roundupConfig'; +import { RoundupSectionHeader } from './RoundupSectionHeader'; + +interface FeaturesToLookForProps { + roundup: RoundupPage; +} + +const TH = + 'px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-muted-foreground'; +const TD = 'px-4 py-4 text-sm align-top'; + +export const FeaturesToLookFor = ({ roundup }: FeaturesToLookForProps) => { + return ( +
+ + +
+
+ + + + + + + + + + {roundup.featuresToLookFor.map((row) => ( + + + + + + ))} + +
FeatureWhat it doesWhy it matters
+ {row.feature} + {row.whatItDoes}{row.whyItMatters}
+
+
+
+ ); +}; diff --git a/apps/marketing/src/components/alternative/roundup/RoundupBenefits.tsx b/apps/marketing/src/components/alternative/roundup/RoundupBenefits.tsx new file mode 100644 index 0000000..cb3aa53 --- /dev/null +++ b/apps/marketing/src/components/alternative/roundup/RoundupBenefits.tsx @@ -0,0 +1,27 @@ +import { RiCheckboxCircleLine } from '@remixicon/react'; +import type { RoundupPage } from '@/lib/roundupConfig'; +import { RoundupSectionHeader } from './RoundupSectionHeader'; + +interface RoundupBenefitsProps { + roundup: RoundupPage; +} + +export const RoundupBenefits = ({ roundup }: RoundupBenefitsProps) => { + return ( +
+ + +
+
+ {roundup.benefits.items.map((item) => ( +
+
+ ))} +
+
+
+ ); +}; diff --git a/apps/marketing/src/components/alternative/roundup/RoundupFAQ.tsx b/apps/marketing/src/components/alternative/roundup/RoundupFAQ.tsx new file mode 100644 index 0000000..11cf7e6 --- /dev/null +++ b/apps/marketing/src/components/alternative/roundup/RoundupFAQ.tsx @@ -0,0 +1,55 @@ +'use client'; + +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '@coderscreen/ui/accordion'; +import type { RoundupPage } from '@/lib/roundupConfig'; + +interface RoundupFAQProps { + roundup: RoundupPage; +} + +export const RoundupFAQ = ({ roundup }: RoundupFAQProps) => { + const faqSchema = { + '@context': 'https://schema.org', + '@type': 'FAQPage', + mainEntity: roundup.faq.map((item) => ({ + '@type': 'Question', + name: item.question, + acceptedAnswer: { + '@type': 'Answer', + text: item.answer, + }, + })), + }; + + return ( +
+