public: seo work
This commit is contained in:
@@ -1,41 +1,19 @@
|
||||
import { FaqItem, Faqs } from '@/components/faq';
|
||||
import { Section, SectionHeader } from '@/components/section';
|
||||
import { CompareFaqs } from '@/lib/compare';
|
||||
import Script from 'next/script';
|
||||
import { url } from '@/lib/layout.shared';
|
||||
import type { CompareFaqs } from '@/lib/compare';
|
||||
|
||||
interface CompareFaqProps {
|
||||
faqs: CompareFaqs;
|
||||
pageUrl: string;
|
||||
}
|
||||
|
||||
export function CompareFaq({ faqs, pageUrl }: CompareFaqProps) {
|
||||
const faqJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: faqs.items.map((q) => ({
|
||||
'@type': 'Question',
|
||||
name: q.question,
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: q.answer,
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
export function CompareFaq({ faqs }: CompareFaqProps) {
|
||||
return (
|
||||
<Section className="container">
|
||||
<Script
|
||||
strategy="beforeInteractive"
|
||||
id="compare-faq-schema"
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
|
||||
/>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<SectionHeader
|
||||
className="mb-16"
|
||||
title={faqs.title}
|
||||
description={faqs.intro}
|
||||
title={faqs.title}
|
||||
variant="sm"
|
||||
/>
|
||||
<Faqs>
|
||||
@@ -49,4 +27,3 @@ export function CompareFaq({ faqs, pageUrl }: CompareFaqProps) {
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
import { CtaBanner } from '@/app/(home)/_sections/cta-banner';
|
||||
import { WindowImage } from '@/components/window-image';
|
||||
import {
|
||||
type CompareData,
|
||||
getAllCompareSlugs,
|
||||
getCompareData,
|
||||
} from '@/lib/compare';
|
||||
import { url } from '@/lib/layout.shared';
|
||||
import { getOgImageUrl, getPageMetadata } from '@/lib/metadata';
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import Script from 'next/script';
|
||||
@@ -22,6 +13,11 @@ import { RelatedLinksSection } from './_components/related-links';
|
||||
import { TechnicalComparison } from './_components/technical-comparison';
|
||||
import { UseCases } from './_components/use-cases';
|
||||
import { WhoShouldChoose } from './_components/who-should-choose';
|
||||
import { CtaBanner } from '@/app/(home)/_sections/cta-banner';
|
||||
import { WindowImage } from '@/components/window-image';
|
||||
import { getAllCompareSlugs, getCompareData } from '@/lib/compare';
|
||||
import { url } from '@/lib/layout.shared';
|
||||
import { getOgImageUrl, getPageMetadata } from '@/lib/metadata';
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const slugs = await getAllCompareSlugs();
|
||||
@@ -83,9 +79,7 @@ export default async function ComparePage({
|
||||
|
||||
// Build ToC items
|
||||
const tocItems = [
|
||||
...(data.overview
|
||||
? [{ id: 'overview', label: data.overview.title }]
|
||||
: []),
|
||||
...(data.overview ? [{ id: 'overview', label: data.overview.title }] : []),
|
||||
{ id: 'who-should-choose', label: data.summary_comparison.title },
|
||||
{ id: 'comparison', label: data.highlights.title },
|
||||
{ id: 'features', label: data.feature_comparison.title },
|
||||
@@ -106,19 +100,19 @@ export default async function ComparePage({
|
||||
return (
|
||||
<div>
|
||||
<Script
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
id="compare-schema"
|
||||
strategy="beforeInteractive"
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
/>
|
||||
<CompareHero hero={data.hero} tocItems={tocItems} />
|
||||
|
||||
<div className="container my-16">
|
||||
<WindowImage
|
||||
srcDark="/screenshots/overview-dark.webp"
|
||||
srcLight="/screenshots/overview-light.webp"
|
||||
alt="OpenPanel Dashboard Overview"
|
||||
caption="This is our web analytics dashboard, its an out-of-the-box experience so you can start understanding your traffic and engagement right away."
|
||||
srcDark="/screenshots/overview-dark.webp"
|
||||
srcLight="/screenshots/overview-light.webp"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -130,25 +124,25 @@ export default async function ComparePage({
|
||||
|
||||
<div id="who-should-choose">
|
||||
<WhoShouldChoose
|
||||
summary={data.summary_comparison}
|
||||
competitorName={data.competitor.name}
|
||||
summary={data.summary_comparison}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="container my-16">
|
||||
<WindowImage
|
||||
srcDark="/screenshots/dashboard-dark.webp"
|
||||
srcLight="/screenshots/dashboard-light.webp"
|
||||
alt="OpenPanel Dashboard"
|
||||
caption="Comprehensive analytics dashboard with real-time insights and customizable views."
|
||||
srcDark="/screenshots/dashboard-dark.webp"
|
||||
srcLight="/screenshots/dashboard-light.webp"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="comparison">
|
||||
<ComparisonTable
|
||||
highlights={data.highlights}
|
||||
featureComparison={data.feature_comparison}
|
||||
competitorName={data.competitor.name}
|
||||
featureComparison={data.feature_comparison}
|
||||
highlights={data.highlights}
|
||||
/>
|
||||
</div>
|
||||
<div id="features">
|
||||
@@ -157,26 +151,26 @@ export default async function ComparePage({
|
||||
|
||||
<div className="container my-16">
|
||||
<WindowImage
|
||||
srcDark="/screenshots/realtime-dark.webp"
|
||||
srcLight="/screenshots/realtime-light.webp"
|
||||
alt="OpenPanel Real-time Analytics"
|
||||
caption="Track events in real-time as they happen with instant updates and live monitoring."
|
||||
srcDark="/screenshots/realtime-dark.webp"
|
||||
srcLight="/screenshots/realtime-light.webp"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{data.technical_comparison && (
|
||||
<div id="technical">
|
||||
<TechnicalComparison
|
||||
technical={data.technical_comparison}
|
||||
competitorName={data.competitor.name}
|
||||
technical={data.technical_comparison}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div id="pricing">
|
||||
<PricingSection
|
||||
pricing={data.pricing}
|
||||
competitorName={data.competitor.name}
|
||||
pricing={data.pricing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -192,10 +186,10 @@ export default async function ComparePage({
|
||||
|
||||
<div className="container my-16">
|
||||
<WindowImage
|
||||
srcDark="/screenshots/report-dark.webp"
|
||||
srcLight="/screenshots/report-light.webp"
|
||||
alt="OpenPanel Reports"
|
||||
caption="Generate detailed reports and insights with customizable metrics and visualizations."
|
||||
srcDark="/screenshots/report-dark.webp"
|
||||
srcLight="/screenshots/report-light.webp"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -203,26 +197,26 @@ export default async function ComparePage({
|
||||
<>
|
||||
<div id="benefits">
|
||||
<BenefitsSection
|
||||
benefits={data.benefits_section.benefits}
|
||||
cta={data.benefits_section.cta}
|
||||
description={data.benefits_section.description}
|
||||
label={data.benefits_section.label}
|
||||
title={data.benefits_section.title}
|
||||
description={data.benefits_section.description}
|
||||
cta={data.benefits_section.cta}
|
||||
benefits={data.benefits_section.benefits}
|
||||
/>
|
||||
</div>
|
||||
<div className="container my-16">
|
||||
<WindowImage
|
||||
srcDark="/screenshots/profile-dark.webp"
|
||||
srcLight="/screenshots/profile-light.webp"
|
||||
alt="OpenPanel User Profiles"
|
||||
caption="Deep dive into individual user profiles with complete event history and behavior tracking."
|
||||
srcDark="/screenshots/profile-dark.webp"
|
||||
srcLight="/screenshots/profile-light.webp"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div id="faq">
|
||||
<CompareFaq faqs={data.faqs} pageUrl={pageUrl} />
|
||||
<CompareFaq faqs={data.faqs} />
|
||||
</div>
|
||||
|
||||
{data.related_links && (
|
||||
@@ -230,10 +224,10 @@ export default async function ComparePage({
|
||||
)}
|
||||
|
||||
<CtaBanner
|
||||
title={'Ready to make the switch?'}
|
||||
description="Test OpenPanel free for 30 days, you'll not be charged anything unless you upgrade to a paid plan."
|
||||
ctaText={data.ctas.primary.label}
|
||||
ctaLink={data.ctas.primary.href}
|
||||
ctaText={data.ctas.primary.label}
|
||||
description="Test OpenPanel free for 30 days, you'll not be charged anything unless you upgrade to a paid plan."
|
||||
title={'Ready to make the switch?'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,39 +1,19 @@
|
||||
import { FaqItem, Faqs } from '@/components/faq';
|
||||
import { Section, SectionHeader } from '@/components/section';
|
||||
import type { FeatureFaqs } from '@/lib/features';
|
||||
import Script from 'next/script';
|
||||
|
||||
interface FeatureFaqProps {
|
||||
faqs: FeatureFaqs;
|
||||
}
|
||||
|
||||
export function FeatureFaq({ faqs }: FeatureFaqProps) {
|
||||
const faqJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: faqs.items.map((q) => ({
|
||||
'@type': 'Question',
|
||||
name: q.question,
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: q.answer,
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
return (
|
||||
<Section className="container">
|
||||
<Script
|
||||
strategy="beforeInteractive"
|
||||
id="feature-faq-schema"
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
|
||||
/>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<SectionHeader
|
||||
className="mb-16"
|
||||
title={faqs.title}
|
||||
description={faqs.intro}
|
||||
title={faqs.title}
|
||||
variant="sm"
|
||||
/>
|
||||
<Faqs>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { FaqItem, Faqs } from '@/components/faq';
|
||||
import { Section, SectionHeader } from '@/components/section';
|
||||
import Script from 'next/script';
|
||||
|
||||
const faqData = [
|
||||
{
|
||||
@@ -61,33 +60,13 @@ const faqData = [
|
||||
];
|
||||
|
||||
export function Faq() {
|
||||
const faqJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: faqData.map((q) => ({
|
||||
'@type': 'Question',
|
||||
name: q.question,
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: q.answer,
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
return (
|
||||
<Section className="container">
|
||||
<Script
|
||||
strategy="beforeInteractive"
|
||||
id="faq-schema"
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<SectionHeader
|
||||
className="mb-16"
|
||||
title="FAQ"
|
||||
description="Some of the most common questions we get asked."
|
||||
title="FAQ"
|
||||
/>
|
||||
<Faqs>
|
||||
{faqData.map((faq) => (
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Script from 'next/script';
|
||||
import Markdown from 'react-markdown';
|
||||
import {
|
||||
Accordion,
|
||||
@@ -7,12 +6,22 @@ import {
|
||||
AccordionTrigger,
|
||||
} from './ui/accordion';
|
||||
|
||||
export const Faqs = ({ children }: { children: React.ReactNode }) => (
|
||||
<div itemScope itemType="https://schema.org/FAQPage">
|
||||
export const Faqs = ({
|
||||
children,
|
||||
schema = true,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
schema?: boolean;
|
||||
}) => (
|
||||
<div
|
||||
{...(schema
|
||||
? { itemScope: true, itemType: 'https://schema.org/FAQPage' }
|
||||
: {})}
|
||||
>
|
||||
<Accordion
|
||||
type="single"
|
||||
className="w-full max-w-screen-md self-center rounded-3xl border bg-background-dark [&_button]:px-4 [&_div.answer]:bg-background-light"
|
||||
collapsible
|
||||
className="w-full max-w-screen-md self-center border rounded-3xl [&_button]:px-4 bg-background-dark [&_div.answer]:bg-background-light"
|
||||
type="single"
|
||||
>
|
||||
{children}
|
||||
</Accordion>
|
||||
@@ -27,20 +36,20 @@ export const FaqItem = ({
|
||||
children: string | React.ReactNode;
|
||||
}) => (
|
||||
<AccordionItem
|
||||
value={question}
|
||||
itemScope
|
||||
itemProp="mainEntity"
|
||||
itemType="https://schema.org/Question"
|
||||
className="[&_[role=region]]:px-4"
|
||||
itemProp="mainEntity"
|
||||
itemScope
|
||||
itemType="https://schema.org/Question"
|
||||
value={question}
|
||||
>
|
||||
<AccordionTrigger className="text-left" itemProp="name">
|
||||
{question}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent
|
||||
className="prose"
|
||||
itemProp="acceptedAnswer"
|
||||
itemScope
|
||||
itemType="https://schema.org/Answer"
|
||||
className="prose"
|
||||
>
|
||||
<div itemProp="text">
|
||||
{typeof children === 'string' ? (
|
||||
|
||||
Reference in New Issue
Block a user